Repository: psf/black

Issue:
Don't inspect the previous body for blank line rules in stub files
#2736 added a rule that preserves a blank line between attributes and methods. The implementation looks at the previous line instead of the previous statement, so it depends on the last statement in the (potentially arbitrarily nested) body if black inserts a blank line or not ([code](https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L599-L601)). #2783/#3564 fixes this for classes by always inserting a blank line, but this does not apply e.g. to function definitions. This affects e.g. typeshed, which uses `if sys.version_info > (3, x):` in stub files a lot.

I propose to change this to look at the type of the previous statement instead of the previous line.

**Preview style**

```python
import sys

if sys.version_info > (3, 7):
    class Nested1:
        assignment = 1
        def function_definition(self): ...

    def f1(self) -> str: ...

    class Nested2:
        def function_definition(self): ...
        assignment = 1

    def f2(self) -> str: ...

if sys.version_info > (3, 7):
    def nested1():
        assignment = 1
        def function_definition(self): ...

    def f1(self) -> str: ...
    def nested2():
        def function_definition(self): ...
        assignment = 1
    def f2(self) -> str: ...
``` 

**Desired style**

(or a different set of blank line rules that don't inspect the previous function body anymore)

```python
import sys

if sys.version_info > (3, 7):
    class Nested1:
        assignment = 1
        def function_definition(self): ...

    def f1(self) -> str: ...

    class Nested2:
        def function_definition(self): ...
        assignment = 1

    def f2(self) -> str: ...

if sys.version_info > (3, 7):
    def nested1():
        assignment = 1
        def function_definition(self): ...

    def f1(self) -> str: ...
    def nested2():
        def function_definition(self): ...
        assignment = 1

    def f2(self) -> str: ...
``` 




Discussion:
IIUC it doesn't usually matter because there's rarely any statements inside functions in stubs. Interested in opening a PR? :-)

Resolving patch:
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -709,6 +709,17 @@ def _maybe_empty_lines_for_class_or_def(  # noqa: C901
                     newlines = 0
                 else:
                     newlines = 1
+            # Remove case `self.previous_line.depth > current_line.depth` below when
+            # this becomes stable.
+            #
+            # Don't inspect the previous line if it's part of the body of the previous
+            # statement in the same level, we always want a blank line if there's
+            # something with a body preceding.
+            elif (
+                Preview.blank_line_between_nested_and_def_stub_file in current_line.mode
+                and self.previous_line.depth > current_line.depth
+            ):
+                newlines = 1
             elif (
                 current_line.is_def or current_line.is_decorator
             ) and not self.previous_line.is_def:
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -170,6 +170,7 @@ class Preview(Enum):
 
     add_trailing_comma_consistently = auto()
     blank_line_after_nested_stub_class = auto()
+    blank_line_between_nested_and_def_stub_file = auto()
     hex_codes_in_unicode_sequences = auto()
     improved_async_statements_handling = auto()
     multiline_string_handling = auto()


Test patch:
diff --git a/tests/data/miscellaneous/nested_class_stub.pyi b/tests/data/miscellaneous/nested_class_stub.pyi
deleted file mode 100644
--- a/tests/data/miscellaneous/nested_class_stub.pyi
+++ /dev/null
@@ -1,16 +0,0 @@
-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/data/miscellaneous/nested_stub.pyi b/tests/data/miscellaneous/nested_stub.pyi
new file mode 100644
--- /dev/null
+++ b/tests/data/miscellaneous/nested_stub.pyi
@@ -0,0 +1,43 @@
+import sys
+
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+    class Inner:
+        inner_attr: int
+    outer_attr: int
+
+if sys.version_info > (3, 7):
+    if sys.platform == "win32":
+        assignment = 1
+        def function_definition(self): ...
+    def f1(self) -> str: ...
+    if sys.platform != "win32":
+        def function_definition(self): ...
+        assignment = 1
+    def f2(self) -> str: ...
+
+# output
+
+import sys
+
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+
+    class Inner:
+        inner_attr: int
+
+    outer_attr: int
+
+if sys.version_info > (3, 7):
+    if sys.platform == "win32":
+        assignment = 1
+        def function_definition(self): ...
+
+    def f1(self) -> str: ...
+    if sys.platform != "win32":
+        def function_definition(self): ...
+        assignment = 1
+
+    def f2(self) -> str: ...
\ No newline at end of file
diff --git a/tests/test_format.py b/tests/test_format.py
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -194,9 +194,9 @@ def test_stub() -> None:
     assert_format(source, expected, mode)
 
 
-def test_nested_class_stub() -> None:
+def test_nested_stub() -> None:
     mode = replace(DEFAULT_MODE, is_pyi=True, preview=True)
-    source, expected = read_data("miscellaneous", "nested_class_stub.pyi")
+    source, expected = read_data("miscellaneous", "nested_stub.pyi")
     assert_format(source, expected, mode)