Repository: psf/black

Issue:
Preview mode crash with `--fast` and walrus in except clause
```
$ black --fast -c '''
try: pass
except (a := 1) as (b := why_does_this_exist): pass
'''
try:
    pass
except (a := 1) as (b := why_does_this_exist):
    pass
$ black --preview --fast -c '''
try: pass
except (a := 1) as (b := why_does_this_exist): pass
'''

try: pass
except (a := 1) as (b := why_does_this_exist): pass
error: cannot format <string>: Cannot parse: 3:9: except a := 1 as (b := why_does_this_exist):
$ black --version
black, 22.12.0 (compiled: yes)
Python (CPython) 3.9.14
```

Similar to #3419, found this in CI for #3418. This needs `--fast` because CPython actually rejects walrus here (good call), but the grammar allows it.

This one probably comes from #2939, cc @jpy-git again.
Fix crash in return annotation with walrus
Fixes #3419

<!-- Hello! Thanks for submitting a PR. To help make things go a bit more
     smoothly we would appreciate that you go through this template. -->

### Description

<!-- Good things to put here include: reasoning for the change (please link
     any relevant issues!), any noteworthy (or hacky) choices to be aware of,
     or what the problem resolved here looked like ... we won't mind a ranty
     story :) -->

### Checklist - did you ...

<!-- If any of the following items aren't relevant for your contribution
     please still tick them so we know you've gone through the checklist.

    All user-facing changes should get an entry. Otherwise, signal to us
    this should get the magical label to silence the CHANGELOG entry check.
    Tests are required for bugfixes and new features. Documentation changes
    are necessary for formatting and most enhancement changes. -->

- [ ] Add an entry in `CHANGES.md` if necessary?
- [ ] Add / update tests if necessary?
- [ ] Add new / update outdated documentation?

<!-- Just as a reminder, everyone in all psf/black spaces including PRs
     must follow the PSF Code of Conduct (link below).

     Finally, once again thanks for your time and effort. If you have any
     feedback in regards to your experience contributing here, please
     let us know!

     Helpful links:

      PSF COC: https://www.python.org/psf/conduct/
      Contributing docs: https://black.readthedocs.io/en/latest/contributing/index.html
      Chat on Python Discord: https://discord.gg/RtVdv86PrH -->



Discussion:

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- 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,


Test patch:
diff --git a/tests/data/py_310/pattern_matching_extras.py b/tests/data/py_310/pattern_matching_extras.py
--- 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
--- 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.