Repository: psf/black

Issue:
Put blank lines between nested classes in typing stubs
**Describe the style change**

Nested classes in typing stubs should have blank lines between them.

**Examples in the current _Black_ style**

```python
class A:
    class B:
        attribute: int
    class C:
        attribute: int
    def method(self) -> None: ...
    def method2(self) -> None: ...
```

**Desired style**

```python
class A:
    class B:
        attribute: int
    
    class C:
        attribute: int
    
    def method(self) -> None: ...
    def method2(self) -> None: ...
```

**Additional context**

See [this](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ADzAHtdAD2IimZxl1N_WlXnON2nzNApqtITJKWyv-kYFqDWqeQHE04C3aER3b3-cgsNj0lpNheYqG6qe0lFpECGtbCMGFkcYXx9gUJsn3-wZsV43vWzFCgYQ3Kx8VSKLA9_iA-BRMPrCtqG_d69QoGeSVQMRgqO0XtVDYSuN4hmMwAAyU27a2KRJCEAAZcB9AEAAFEwkPaxxGf7AgAAAAAEWVo=) for a live demo.



Discussion:
I'm in favor of making this change, but cc fellow typeshed maintainers @srittau @akuli @hauntsaninja.
Yeah, seems nice.

Somewhat related, the stubs style change I want from black is for top-level code under an if-statement to have the same blank lines as top-level code proper. Example [here](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4AD8AIRdAD2IimZxl1N_WlXnON2nzNApqtITJJee2SMStFF7KJQ8b5tLWLTPF97YwrFIbsO2CJkraBrsaGIFFxZyS8cW-bxi-KzgtfNZt0bUoUnR71oEBfdQhSZgYLHjXKPdI-W9GBft7c-sf-zudh3LWYfAsj7EDjUuebHXI_AiVpD5OuhIJSCPsAC6Ox7BianITQABoAH9AQAAzkjnkrHEZ_sCAAAAAARZWg==)
@hauntsaninja we made some changes around that in #2472, but agree that we should space in your example too. Feel free to open a new issue. We also have an unreleased change in #2736.
I'm fine with this change.

Everyone agrees, so @MarkKoz feel free to go ahead and implement this. Thanks!
Had a quick look at this and, since there's no special consideration given to whether an indented block is within a `class` or an `if`, it seems the work in #2820 and #2796 has partially addressed this issue.
There are however there are a couple other cases that need to be accounted for in both `if` and `class` blocks: [if example](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ALSAP1dAD2IimZxl1N_Wg09_MnT0fr1aHaPLgZAtxfxtPZip7dzNRlF7v2sgiHfOuN_BZi6gq4JW7NR2subjnh92od15GVXvq78CbU1QIOxbtWTbFMZAlI3CQm8wuhzIN4K93b5tX5VLketLdPvUDJTEbrgmMXqouW99cRSviseZOTfXo9TU6iISd_S8qUi7QZQ8OmmUL7s_p-xRwingRAbWg-fSIe_RjGckbMo9LWkn0BlOfzVCQKM7B77gtXsQRg_DsQGdg-eybcCRoNTt0WV4-fKMD8KUuyhMcWti-nZrX9cGcZ56d73Ny7xBT1GbkRL0lt8RkhiBkxeBr-3FTCfmQAAAAAAECE0ddSHHIEAAZkC0wUAAKoU1rGxxGf7AgAAAAAEWVo=) and [class example](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4AMMAQpdAD2IimZxl1N_Wg09-eQRbqt5E7r0HfN6SLgpAvsu5tIweGC6Bhl_D_m3DfDJ8mjcyGOB4TWvVvLwNpt-c6jhMD_S2X8Tn4A4qbv8Vq_ZLPsI-JTZinUtyprSaxIMfw1hA4jvFkcq5ZVsFW0HRwWLtf82m3D8KNrEohBW_c2wI0tlrcnrmYyJlnw8B0rPY0_K-g7g5kUdLhzeLVzM-Xsm_wYsgRBLo4ZwOxTAtj3M_hm7LBtjNGFcdj_enSg-ZfxzPNWFQBn_HfLA7pNm10UXQprc7xmYRA_O0WXFaoExkMJsz0YacepbLKRAsQern7dRm6HPQLbUVlxY_jl3LkV2NplvDk2_4gUG0QDCAAAAeKlFN_a_l_MAAaYCjQYAABpnbm2xxGf7AgAAAAAEWVo=)

For stubs, it feels like making any indented block consistent with the top level achieves the goal of this issue and #2784.

Although one place I can see `class` and `if` differing is the gap between attributes and methods ([example](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4AG8ANZdAD2IimZxl1N_Wg099vr4I5e0ptgzBAsMLDl-PKjRPmTsVee_Gl2LTmYVE_LgQ1r0esmv29sC36oeMWGdezcePd_lo2QIG7ws6rzcyuq8cl9nnhwyEpRGyZ02KOeCs0UYORW7pzW3b1bod1qJBp_5TbrhpqJiNyqF0yfsjIwJvbBRd5HWgiB6GDiBBGxgKYXV1-utv84xQBcUajNkd24RZntUwbn3rtjmuUGNL8WPs98ZpFe90vGKCL-PXu1lvrdInOWxR-C9JVH5QUhWGT7Llv4amGGhSgAAAABDTLkZ1FJP8AAB8gG9AwAAYW0xS7HEZ_sCAAAAAARZWg==)) since this may introduce a lot of changes in typeshed.

Resolving patch:
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -501,7 +501,7 @@ class EmptyLineTracker:
     mode: Mode
     previous_line: Optional[Line] = None
     previous_block: Optional[LinesBlock] = None
-    previous_defs: List[int] = field(default_factory=list)
+    previous_defs: List[Line] = field(default_factory=list)
     semantic_leading_comment: Optional[LinesBlock] = None
 
     def maybe_empty_lines(self, current_line: Line) -> LinesBlock:
@@ -557,12 +557,18 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
         else:
             before = 0
         depth = current_line.depth
-        while self.previous_defs and self.previous_defs[-1] >= depth:
+        while self.previous_defs and self.previous_defs[-1].depth >= depth:
             if self.mode.is_pyi:
                 assert self.previous_line is not None
                 if depth and not current_line.is_def and self.previous_line.is_def:
                     # Empty lines between attributes and methods should be preserved.
                     before = min(1, before)
+                elif (
+                    Preview.blank_line_after_nested_stub_class in self.mode
+                    and self.previous_defs[-1].is_class
+                    and not self.previous_defs[-1].is_stub_class
+                ):
+                    before = 1
                 elif depth:
                     before = 0
                 else:
@@ -616,7 +622,7 @@ def _maybe_empty_lines_for_class_or_def(
         self, current_line: Line, before: int
     ) -> Tuple[int, int]:
         if not current_line.is_decorator:
-            self.previous_defs.append(current_line.depth)
+            self.previous_defs.append(current_line)
         if self.previous_line is None:
             # Don't insert empty lines before the first line in the file.
             return 0, 0
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -154,6 +154,7 @@ class Preview(Enum):
     """Individual preview style features."""
 
     add_trailing_comma_consistently = auto()
+    blank_line_after_nested_stub_class = auto()
     hex_codes_in_unicode_sequences = auto()
     multiline_string_handling = auto()
     prefer_splitting_right_hand_side_of_assignments = auto()


Test patch:
diff --git a/tests/data/miscellaneous/nested_class_stub.pyi b/tests/data/miscellaneous/nested_class_stub.pyi
new file mode 100644
--- /dev/null
+++ b/tests/data/miscellaneous/nested_class_stub.pyi
@@ -0,0 +1,16 @@
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+    class Inner:
+        inner_attr: int
+    outer_attr: int
+
+# output
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+
+    class Inner:
+        inner_attr: int
+
+    outer_attr: int
diff --git a/tests/test_format.py b/tests/test_format.py
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -186,6 +186,12 @@ def test_stub() -> None:
     assert_format(source, expected, mode)
 
 
+def test_nested_class_stub() -> None:
+    mode = replace(DEFAULT_MODE, is_pyi=True, preview=True)
+    source, expected = read_data("miscellaneous", "nested_class_stub.pyi")
+    assert_format(source, expected, mode)
+
+
 def test_power_op_newline() -> None:
     # requires line_length=0
     source, expected = read_data("miscellaneous", "power_op_newline")