diff --git a/CHANGES.md b/CHANGES.md index ba71ee6c99d..50c0f5df050 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Fix a crash in preview style with assert + parenthesized string (#3415) +- Fix crashes in preview style with walrus operators used in function return annotations + and except clauses (#3423) - Do not put the closing quotes in a docstring on a separate line, even if the line is too long (#3430) - Long values in dict literals are now wrapped in parentheses; correspondingly diff --git a/src/black/linegen.py b/src/black/linegen.py index fe6ea11c501..2e75bc94506 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1268,6 +1268,8 @@ def maybe_make_parens_invisible_in_atom( syms.expr_stmt, syms.assert_stmt, syms.return_stmt, + syms.except_clause, + syms.funcdef, # these ones aren't useful to end users, but they do please fuzzers syms.for_stmt, syms.del_stmt, diff --git a/tests/data/py_310/pattern_matching_extras.py b/tests/data/py_310/pattern_matching_extras.py index 9f6907f7575..0242d264e5b 100644 --- a/tests/data/py_310/pattern_matching_extras.py +++ b/tests/data/py_310/pattern_matching_extras.py @@ -114,6 +114,6 @@ def func(match: case, case: match) -> case: match bar1: case Foo( - normal=x, perhaps=[list, {an: d, dict: 1.0}] as y, otherwise=something, q=t as u + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u ): pass diff --git a/tests/util.py b/tests/util.py index d65c2e651ae..967d576fafe 100644 --- a/tests/util.py +++ b/tests/util.py @@ -2,6 +2,7 @@ import sys import unittest from contextlib import contextmanager +from dataclasses import replace from functools import partial from pathlib import Path from typing import Any, Iterator, List, Optional, Tuple @@ -56,6 +57,10 @@ def _assert_format_equal(expected: str, actual: str) -> None: assert actual == expected +class FormatFailure(Exception): + """Used to wrap failures when assert_format() runs in an extra mode.""" + + def assert_format( source: str, expected: str, @@ -70,12 +75,57 @@ def assert_format( safety guards so they don't just crash with a SyntaxError. Please note this is separate from TargetVerson Mode configuration. """ + _assert_format_inner( + source, expected, mode, fast=fast, minimum_version=minimum_version + ) + + # For both preview and non-preview tests, ensure that Black doesn't crash on + # this code, but don't pass "expected" because the precise output may differ. + try: + _assert_format_inner( + source, + None, + replace(mode, preview=not mode.preview), + fast=fast, + minimum_version=minimum_version, + ) + except Exception as e: + text = "non-preview" if mode.preview else "preview" + raise FormatFailure( + f"Black crashed formatting this case in {text} mode." + ) from e + # Similarly, setting line length to 1 is a good way to catch + # stability bugs. But only in non-preview mode because preview mode + # currently has a lot of line length 1 bugs. + try: + _assert_format_inner( + source, + None, + replace(mode, preview=False, line_length=1), + fast=fast, + minimum_version=minimum_version, + ) + except Exception as e: + raise FormatFailure( + "Black crashed formatting this case with line-length set to 1." + ) from e + + +def _assert_format_inner( + source: str, + expected: Optional[str] = None, + mode: black.Mode = DEFAULT_MODE, + *, + fast: bool = False, + minimum_version: Optional[Tuple[int, int]] = None, +) -> None: actual = black.format_str(source, mode=mode) - _assert_format_equal(expected, actual) + if expected is not None: + _assert_format_equal(expected, actual) # It's not useful to run safety checks if we're expecting no changes anyway. The # assertion right above will raise if reality does actually make changes. This just # avoids wasted CPU cycles. - if not fast and source != expected: + if not fast and source != actual: # Unfortunately the AST equivalence check relies on the built-in ast module # being able to parse the code being formatted. This doesn't always work out # when checking modern code on older versions.