Repository: psf/black
Pull request: Fix long case blocks not split into multiple lines
### Description

Fixes #3850 .

> This note has been edited for readability and ease of future reference.

Previously Black doesn't recognize `case` block as one of the syntax blocks need to be wrapped with invisible parentheses, and hence will also not be splitting the `case` blocks into multiple lines even when it's too long. 

Now with (invisible) parentheses correctly wrapped around `case` blocks, the lines are able to be splitted into multiple lines when needed, this also allows the trailing comma rules to be enabled for `case` blocks. Unit test `pattern_matching_long.py` and ``pattern_matching_trailing_comma.py`` has been added to verify these modifications.

At start, a side effect of this update is the altered formatting of `case case:` scenarios, resulting an additional space before the colon (i.e., `case case :`). This is due to Black after this update will treat the second `case` as a distinct block that requires invisible parenthesis. 

Now, a special patch to the `case case:` scenario have been made to preserve consistency of user-visible outputs.

### A bit of discussion on the side effect prior to the special patch

#### Some background

Python 3.10 and later support matching the final case with an undefined pattern (e.g. `_`) as a default when other cases cannot match, this allow code like all following examples to execute and print the "x" without error:

```
# E.g.1 the usual way for default case
x = 1
match x:
    case 10:
        print(1)
    case _:
        print("x")
```

```
# E.g.2 the unusual way 1
x = 1
match x:
    case 10:
        print(1)
    case xxxxxx:
        print("x")
```

```
# E.g.3 the unusual way 2
x = 1
match x:
    case 10:
        print(1)
    case case: # note that here the second "case" is an undefined pattern instead of a Python keyword
        print("x")
```

#### The side effect

After enabling parentheses to be wrapped around `case` blocks (hence also line breaking), the output of this particular corner case in our unit test will be different:
```
match x:
    case case:
        pass
```

Previously, `case` blocks were not considered for wrapping with parentheses, but now Black treats the second occurrence of "case" as an actual `case` block, so **an additional space** will be added between this occurrence and the colon. The underlying representation Black used for the second line is: "case (case ()):").

Hence, the output is now:
```
match x:
    case case :
        pass
```

On the other hand, while the consecutive `case` block now facing this additional space, the consecutive `match` block remains unchanged for unit tests like the one below, since the `match` block have no need of line breaking and was not using invisible parentheses.

```
match match:
    case x:
        pass
```


#### Summary

I was uncertain if it is preferable to add a special patch for this particular `case case:` scenario since this would adds additional complexity to the codebase. Even with Python 3.10+ allowing to treat the second "case" as an undefined pattern instead of a keyword, I would doubt developers will intentionally use a Python keyword as a pattern/variable. 

Nonetheless, considering that the user-visible output should be left as unchanged as possible, a patch have been made to address this.




### Checklist - did you ...

- [x] Add an entry in `CHANGES.md` if necessary?
- [x] Add / update tests if necessary?
- [ ] Add new / update outdated documentation? not necessary




Problem statement:
Line too long: Many cases in case block
**Describe the bug**

Match patterns with lots of `|`-delimited alternatives are not split into multiple lines.

**To Reproduce**

```python
% cat longmatch.py 
match x:
    case "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd":
        pass
% black -t py311 longmatch.py --diff
All done! ✨ 🍰 ✨
1 file would be left unchanged.
```

(Note the line is 142 characters long)

**Expected behavior**

I want it to reformat to:

```python
match x:
    case (
        "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
        | "abcd"
    ):
        pass
```

Black does this already if you manually add parentheses around the whole sequence of alternatives.

**Environment**

- Black's version: 23.7.0
- OS and Python version: macOS

Hints from issue discussion:
While I was trying to fix this issue, I noticed that the rules for `case` is a bit different than the others: the [trailing comma rules ](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#trailing-commas) will not be activated on `case`

E.g. Black will currently leave this piece of code unchanged despite it contains trailing comma and each of its item is not in seperate lines (see [here](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ACPAGBdAD2IimZxl1N_WmBasjhgIpJRm9D2G66SDLhhnFrSNeMykF1T7_XvdV1ILVbFVrh2v9AAXw2gdeUx-8Jo-bDKXZ1ooMdzNtm7kIEA1AsAChaiMeoJ3hg3MQVQGY_oHvffAABtJu4QBJOyRAABfJABAAAAElkP1rHEZ_sCAAAAAARZWg==))
```
match x:
    case 1, 2,:
        pass
```

Yet, an unintentional side effect of my current attempt would enable the trailing comma on `case` as well, hence the above code will be converted into:
```
match x:
    case (
        1, 
        2,
    ):
        pass
```
I think it is beneficial to have our trailing comma rules consistent across different syntax in general, but I do want to double check if this is indeed the preferable result here and there is no particular reason to do trailing comma for `case` differently.
I agree we should reformat this case as you suggest.

Commit message: Add changes.md

Patch to review:
diff --git a/CHANGES.md b/CHANGES.md
index 5ce37943693..739550cd453 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -12,6 +12,8 @@
 - Fix crash when whitespace followed a backslash before newline in a docstring (#4008)
 
 - Fix crash on formatting code like `await (a ** b)` (#3994)
+- Fix a bug that long `case` blocks will not be split into multiple lines, also enabled
+  general trailing comma rules on `case` blocks (#4024)
 
 ### Preview style
 
diff --git a/src/black/linegen.py b/src/black/linegen.py
index b13b95d9b31..7fba7199772 100644
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -1260,6 +1260,13 @@ def normalize_invisible_parens(
                 child, parens_after=parens_after, mode=mode, features=features
             )
 
+        # Fixes a bug where invisible parens are not properly wrapped around
+        # case blocks.
+        if isinstance(child, Node) and child.type == syms.case_block:
+            normalize_invisible_parens(
+                child, parens_after={"case"}, mode=mode, features=features
+            )
+
         # Add parentheses around long tuple unpacking in assignments.
         if (
             index == 0
diff --git a/tests/data/cases/pattern_matching_extras.py b/tests/data/cases/pattern_matching_extras.py
index 1e1481d7bbe..6c3f84dbe82 100644
--- a/tests/data/cases/pattern_matching_extras.py
+++ b/tests/data/cases/pattern_matching_extras.py
@@ -30,22 +30,6 @@ def func(match: case, case: match) -> case:
             ...
 
 
-match maybe, multiple:
-    case perhaps, 5:
-        pass
-    case perhaps, 6,:
-        pass
-
-
-match more := (than, one), indeed,:
-    case _, (5, 6):
-        pass
-    case [[5], (6)], [7],:
-        pass
-    case _:
-        pass
-
-
 match a, *b, c:
     case [*_]:
         assert "seq" == _
@@ -71,12 +55,12 @@ def func(match: case, case: match) -> case:
     case [a as match]:
         pass
 
-    case case:
+    case anything:
         pass
 
 
 match match:
-    case case:
+    case something:
         pass
 
 
diff --git a/tests/data/cases/pattern_matching_generic.py b/tests/data/cases/pattern_matching_generic.py
index 4b4d45f0bff..1d100f80ce3 100644
--- a/tests/data/cases/pattern_matching_generic.py
+++ b/tests/data/cases/pattern_matching_generic.py
@@ -25,9 +25,9 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
         ]
 
     match match:
-        case case:
+        case something:
             match match:
-                case case:
+                case something:
                     pass
 
     if all(version.is_python2() for version in target_versions):
diff --git a/tests/data/cases/pattern_matching_long.py b/tests/data/cases/pattern_matching_long.py
new file mode 100644
index 00000000000..0ef69e92dbc
--- /dev/null
+++ b/tests/data/cases/pattern_matching_long.py
@@ -0,0 +1,39 @@
+# flags: --minimum-version=3.10
+match x:
+    case "abcd" | "abcd" | "abcd" :
+        pass
+    case "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd" | "abcd":
+        pass
+    case xxxxxxxxxxxxxxxxxxxxxxx:
+        pass
+    case case:
+        pass
+
+# output
+
+match x:
+    case "abcd" | "abcd" | "abcd":
+        pass
+    case (
+        "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+        | "abcd"
+    ):
+        pass
+    case xxxxxxxxxxxxxxxxxxxxxxx:
+        pass
+    case case :
+        pass
+
diff --git a/tests/data/cases/pattern_matching_trailing_comma.py b/tests/data/cases/pattern_matching_trailing_comma.py
new file mode 100644
index 00000000000..5660b0f6a14
--- /dev/null
+++ b/tests/data/cases/pattern_matching_trailing_comma.py
@@ -0,0 +1,39 @@
+# flags: --minimum-version=3.10
+match maybe, multiple:
+    case perhaps, 5:
+        pass
+    case perhaps, 6,:
+        pass
+
+
+match more := (than, one), indeed,:
+    case _, (5, 6):
+        pass
+    case [[5], (6)], [7],:
+        pass
+    case _:
+        pass
+
+
+# output
+
+match maybe, multiple:
+    case perhaps, 5:
+        pass
+    case (
+        perhaps,
+        6,
+    ):
+        pass
+
+
+match more := (than, one), indeed,:
+    case _, (5, 6):
+        pass
+    case (
+        [[5], (6)],
+        [7],
+    ):
+        pass
+    case _:
+        pass
\ No newline at end of file


Code review comments:
diff_hunk: @@ -12,6 +12,8 @@
 - Fix crash when whitespace followed a backslash before newline in a docstring (#4008)
 
 - Fix crash on formatting code like `await (a ** b)` (#3994)
+- Fix a bug that long `case` blocks will not be split into multiple lines, also enabled
path: CHANGES.md
text: @user1:
This needs to be changed only in the preview style.

@author:
I have now added a fix for the `case case:` issue. Since there will be no user-visible format changes, does this still need to be moved to a preview style?

@user1:
Your change changes formatting (in that lines that previously weren't split now are), so it should go into the preview style. (You should add a new entry to the Preview enum and make your change apply only if that preview feature is on.)

@author:
Ohh gotcha, thanks! I've now moved it under preview style!