diff --git a/AUTHORS.md b/AUTHORS.md index 8aa6263313e..faa2b05840f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -148,6 +148,7 @@ Multiple contributions by: - [Rishikesh Jha](mailto:rishijha424@gmail.com) - [Rupert Bedford](mailto:rupert@rupertb.com) - Russell Davis +- [Sagi Shadur](mailto:saroad2@gmail.com) - [RĂ©mi Verschelde](mailto:rverschelde@gmail.com) - [Sami Salonen](mailto:sakki@iki.fi) - [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) diff --git a/CHANGES.md b/CHANGES.md index a6b6594b57a..7001271087a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Remove redundant parentheses around awaited objects (#2991) - Parentheses around return annotations are now managed (#2990) - Remove unnecessary parentheses from `with` statements (#2926) +- Remove trailing newlines after code block open (#3035) ### _Blackd_ diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 2ec2c0333a5..8d159e9b0a2 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -49,3 +49,28 @@ plain strings. User-made splits are respected when they do not exceed the line l limit. Line continuation backslashes are converted into parenthesized strings. Unnecessary parentheses are stripped. The stability and status of this feature is tracked in [this issue](https://github.com/psf/black/issues/2188). + +### Removing trailing newlines after code block open + +_Black_ will remove trailing newlines after code block openings. That means that the +following code: + +```python +def my_func(): + + print("The line above me will be deleted!") + + print("But the line above me won't!") +``` + +Will be changed to: + +```python +def my_func(): + print("The line above me will be deleted!") + + print("But the line above me won't!") +``` + +This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`, +`while`, `with`, `case` and `match`. diff --git a/src/black/lines.py b/src/black/lines.py index e455a507539..8b591c324a5 100644 --- 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 index bf79f6a3148..896c516df79 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -150,6 +150,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): 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 index 00000000000..ef2e5c2f6f5 --- /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 index 00000000000..f7bcfbf27a2 --- /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 index 02a707e8996..8adcaed5ef8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1461,7 +1461,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 index 005a5771c2b..a8a922d17db 100644 --- 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)