Repository: psf/black

Issue:
Black should enforce zero empty leading lines in function body 
Operating system: playground
Python version: playground
*Black* version: master

Consider following code snippet:
```python
class X:
    
    
    def f(self, x):
        
        
        return x**2
```

When class definition is formatted, empty lines between `class X:` line and first node of the body is forced to be zero.
When function definition is formatted, empty lines between `def f():` line and first node of the body is normalised to either 0 (if input code had 0 empty lines) or 1 (if input code had 1+ empty lines).

While it's preferable to preserve some of the empty lines of input code, doing this for *leading* empty lines of node body makes no sense, as this empty line cannot have any mental meaning in such context.


Discussion:
I would assume this should also apply to classes, and I'd go as far as to say that all blocks should have their leading vertical whitespace removed, for example:

```python
if a:

    b
```

But they could surely be taken care of in separate issues. More like a mental note for now.
That sounds right I can't think of any reason to have blank lines around an increase in indent level, the increase it self is quite visually clear.
It's a bit different with dedents which often end logical blocks.

Resolving patch:
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -168,6 +168,13 @@ def is_triple_quoted_string(self) -> bool:
             and self.leaves[0].value.startswith(('"""', "'''"))
         )
 
+    @property
+    def opens_block(self) -> bool:
+        """Does this line open a new level of indentation."""
+        if len(self.leaves) == 0:
+            return False
+        return self.leaves[-1].type == token.COLON
+
     def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
         """If so, needs to be split before emitting."""
         for leaf in self.leaves:
@@ -513,6 +520,12 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
         ):
             return before, 1
 
+        if (
+            Preview.remove_block_trailing_newline in current_line.mode
+            and self.previous_line
+            and self.previous_line.opens_block
+        ):
+            return 0, 0
         return before, 0
 
     def _maybe_empty_lines_for_class_or_def(
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -148,6 +148,7 @@ class Preview(Enum):
     one_element_subscript = auto()
     annotation_parens = auto()
     long_docstring_quotes_on_newline = auto()
+    remove_block_trailing_newline = auto()
 
 
 class Deprecated(UserWarning):


Test patch:
diff --git a/tests/data/preview/remove_newline_after_code_block_open.py b/tests/data/preview/remove_newline_after_code_block_open.py
new file mode 100644
--- /dev/null
+++ b/tests/data/preview/remove_newline_after_code_block_open.py
@@ -0,0 +1,189 @@
+import random
+
+
+def foo1():
+
+    print("The newline above me should be deleted!")
+
+
+def foo2():
+
+
+
+    print("All the newlines above me should be deleted!")
+
+
+def foo3():
+
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+def foo4():
+
+    # There is a comment here
+
+    print("The newline above me should not be deleted!")
+
+
+class Foo:
+    def bar(self):
+
+        print("The newline above me should be deleted!")
+
+
+for i in range(5):
+
+    print(f"{i}) The line above me should be removed!")
+
+
+for i in range(5):
+
+
+
+    print(f"{i}) The lines above me should be removed!")
+
+
+for i in range(5):
+
+    for j in range(7):
+
+        print(f"{i}) The lines above me should be removed!")
+
+
+if random.randint(0, 3) == 0:
+
+    print("The new line above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+
+
+
+
+    print("The new lines above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    if random.uniform(0, 1) > 0.5:
+        print("Two lines above me are about to be removed!")
+
+
+while True:
+
+    print("The newline above me should be deleted!")
+
+
+while True:
+
+
+
+    print("The newlines above me should be deleted!")
+
+
+while True:
+
+    while False:
+
+        print("The newlines above me should be deleted!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+
+    file.write("The new line above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+
+
+
+    file.write("The new lines above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="r") as read_file:
+
+    with open("/path/to/output_file.txt", mode="w") as write_file:
+
+        write_file.writelines(read_file.readlines())
+
+# output
+
+import random
+
+
+def foo1():
+    print("The newline above me should be deleted!")
+
+
+def foo2():
+    print("All the newlines above me should be deleted!")
+
+
+def foo3():
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+def foo4():
+    # There is a comment here
+
+    print("The newline above me should not be deleted!")
+
+
+class Foo:
+    def bar(self):
+        print("The newline above me should be deleted!")
+
+
+for i in range(5):
+    print(f"{i}) The line above me should be removed!")
+
+
+for i in range(5):
+    print(f"{i}) The lines above me should be removed!")
+
+
+for i in range(5):
+    for j in range(7):
+        print(f"{i}) The lines above me should be removed!")
+
+
+if random.randint(0, 3) == 0:
+    print("The new line above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    print("The new lines above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    if random.uniform(0, 1) > 0.5:
+        print("Two lines above me are about to be removed!")
+
+
+while True:
+    print("The newline above me should be deleted!")
+
+
+while True:
+    print("The newlines above me should be deleted!")
+
+
+while True:
+    while False:
+        print("The newlines above me should be deleted!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+    file.write("The new line above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+    file.write("The new lines above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="r") as read_file:
+    with open("/path/to/output_file.txt", mode="w") as write_file:
+        write_file.writelines(read_file.readlines())
diff --git a/tests/data/preview_310/remove_newline_after match.py b/tests/data/preview_310/remove_newline_after match.py
new file mode 100644
--- /dev/null
+++ b/tests/data/preview_310/remove_newline_after match.py	
@@ -0,0 +1,34 @@
+def http_status(status):
+
+    match status:
+
+        case 400:
+
+            return "Bad request"
+
+        case 401:
+
+            return "Unauthorized"
+
+        case 403:
+
+            return "Forbidden"
+
+        case 404:
+
+            return "Not found"
+
+# output
+def http_status(status):
+    match status:
+        case 400:
+            return "Bad request"
+
+        case 401:
+            return "Unauthorized"
+
+        case 403:
+            return "Forbidden"
+
+        case 404:
+            return "Not found"
\ No newline at end of file
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -1455,7 +1455,6 @@ def test_newline_comment_interaction(self) -> None:
         black.assert_stable(source, output, mode=DEFAULT_MODE)
 
     def test_bpo_2142_workaround(self) -> None:
-
         # https://bugs.python.org/issue2142
 
         source, _ = read_data("miscellaneous", "missing_final_newline")
diff --git a/tests/test_format.py b/tests/test_format.py
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -86,6 +86,13 @@ def test_preview_minimum_python_39_format(filename: str) -> None:
     assert_format(source, expected, mode, minimum_version=(3, 9))
 
 
+@pytest.mark.parametrize("filename", all_data_cases("preview_310"))
+def test_preview_minimum_python_310_format(filename: str) -> None:
+    source, expected = read_data("preview_310", filename)
+    mode = black.Mode(preview=True)
+    assert_format(source, expected, mode, minimum_version=(3, 10))
+
+
 @pytest.mark.parametrize("filename", SOURCES)
 def test_source_is_formatted(filename: str) -> None:
     check_file("", filename, DEFAULT_MODE, data=False)