diff --git a/README.rst b/README.rst index 9f3a96d..ddec96a 100644 --- a/README.rst +++ b/README.rst @@ -329,6 +329,12 @@ MIT Change Log ---------- +Unreleased +~~~~~~~~~~ + +* Fix a crash and several test failures on Python 3.12, all relating to the B907 + check. + 23.6.5 ~~~~~~ diff --git a/bugbear.py b/bugbear.py index c5a0174..64d1e70 100644 --- a/bugbear.py +++ b/bugbear.py @@ -1245,36 +1245,34 @@ def myunparse(node: ast.AST) -> str: # pragma: no cover current_mark = None variable = None for value in node.values: - # check for quote mark after pre-marked variable - if ( - current_mark is not None - and variable is not None - and isinstance(value, ast.Constant) - and isinstance(value.value, str) - and value.value[0] == current_mark - ): - self.errors.append( - B907( - variable.lineno, - variable.col_offset, - vars=(myunparse(variable.value),), - ) - ) - current_mark = variable = None - # don't continue with length>1, so we can detect a new pre-mark - # in the same string as a post-mark, e.g. `"{foo}" "{bar}"` - if len(value.value) == 1: + if isinstance(value, ast.Constant) and isinstance(value.value, str): + if not value.value: continue - # detect pre-mark - if ( - isinstance(value, ast.Constant) - and isinstance(value.value, str) - and value.value[-1] in quote_marks - ): - current_mark = value.value[-1] - variable = None - continue + # check for quote mark after pre-marked variable + if ( + current_mark is not None + and variable is not None + and value.value[0] == current_mark + ): + self.errors.append( + B907( + variable.lineno, + variable.col_offset, + vars=(myunparse(variable.value),), + ) + ) + current_mark = variable = None + # don't continue with length>1, so we can detect a new pre-mark + # in the same string as a post-mark, e.g. `"{foo}" "{bar}"` + if len(value.value) == 1: + continue + + # detect pre-mark + if value.value[-1] in quote_marks: + current_mark = value.value[-1] + variable = None + continue # detect variable, if there's a pre-mark if ( diff --git a/tests/b907.py b/tests/b907.py index 2a0f834..adfc629 100644 --- a/tests/b907.py +++ b/tests/b907.py @@ -17,12 +17,12 @@ def foo(): f'a "{foo()}" b' # fmt: off -k = (f'"' # error emitted on this line since all values are assigned the same lineno +k = (f'"' # Error emitted here on = (3, 9) + py312 = sys.version_info >= (3, 12) + + def on_py312(number): + """F-string nodes have column numbers set to 0 on = (3, 9) else "BinOp",)), - B907(45, 0, vars=("foo()",)), - B907(46, 0, vars=("None",)), - B907(47, 0, vars=("..." if sys.version_info >= (3, 9) else "Ellipsis",)), - B907(48, 0, vars=("True",)), - B907(51, 0, vars=("var",)), - B907(52, 0, vars=("var",)), - B907(53, 0, vars=("var",)), - B907(54, 0, vars=("var",)), - B907(57, 0, vars=("var",)), - B907(60, 0, vars=("var",)), - B907(64, 0, vars=("var",)), - B907(66, 0, vars=("var",)), - B907(68, 0, vars=("var",)), + B907(8, on_py312(9), vars=("var",)), + B907(9, on_py312(3), vars=("var",)), + B907(10, on_py312(9), vars=("var",)), + B907(12, on_py312(9), vars=("var",)), + B907(13, on_py312(3), vars=("var",)), + B907(14, on_py312(9), vars=("var",)), + B907(16, on_py312(5), vars=("'hello'",)), + B907(17, on_py312(5), vars=("foo()",)), + # Multiline f-strings have lineno changes as well as colno changes on py312+ + B907(21 if py312 else 20, 7 if py312 else 5, vars=("var",)), + B907(26 if py312 else 25, 7 if py312 else 5, vars=("var",)), + B907(31, on_py312(12), vars=("var",)), + B907(32, on_py312(3), vars=("var",)), + B907(33, on_py312(3), vars=("var",)), + B907(33, on_py312(29), vars=("var2",)), + B907(34, on_py312(3), vars=("var",)), + B907(34, on_py312(15), vars=("var2",)), + B907(35, on_py312(3), vars=("var",)), + B907(35, on_py312(10), vars=("var2",)), + B907(38, on_py312(13), vars=("var2",)), + B907(41, on_py312(3), vars=("var",)), + B907(42, on_py312(3), vars=("var.__str__",)), + B907(43, on_py312(3), vars=("var.__str__.__repr__",)), + B907(44, on_py312(3), vars=("3 + 5" if py39 else "BinOp",)), + B907(45, on_py312(3), vars=("foo()",)), + B907(46, on_py312(3), vars=("None",)), + B907(47, on_py312(3), vars=("..." if py39 else "Ellipsis",)), + B907(48, on_py312(3), vars=("True",)), + B907(51, on_py312(3), vars=("var",)), + B907(52, on_py312(3), vars=("var",)), + B907(53, on_py312(3), vars=("var",)), + B907(54, on_py312(3), vars=("var",)), + B907(57, on_py312(3), vars=("var",)), + B907(60, on_py312(3), vars=("var",)), + B907(64, on_py312(5), vars=("var",)), + B907(66, on_py312(3), vars=("var",)), + B907(68, on_py312(3), vars=("var",)), ) self.assertEqual(errors, expected) @@ -795,13 +800,18 @@ def test_selfclean_test_bugbear(self): class TestFuzz(unittest.TestCase): - @settings(suppress_health_check=[HealthCheck.too_slow]) - @given(from_grammar().map(ast.parse)) - def test_does_not_crash_on_any_valid_code(self, syntax_tree): - # Given any syntatically-valid source code, flake8-bugbear should - # not crash. This tests doesn't check that we do the *right* thing, - # just that we don't crash on valid-if-poorly-styled code! - BugBearVisitor(filename="", lines=[]).visit(syntax_tree) + # TODO: enable this test on py312 once hypothesmith supports py312 + if sys.version_info < (3, 12): + from hypothesis import HealthCheck, given, settings + from hypothesmith import from_grammar + + @settings(suppress_health_check=[HealthCheck.too_slow]) + @given(from_grammar().map(ast.parse)) + def test_does_not_crash_on_any_valid_code(self, syntax_tree): + # Given any syntatically-valid source code, flake8-bugbear should + # not crash. This tests doesn't check that we do the *right* thing, + # just that we don't crash on valid-if-poorly-styled code! + BugBearVisitor(filename="", lines=[]).visit(syntax_tree) def test_does_not_crash_on_site_code(self): # Because the generator isn't perfect, we'll also test on all the code