From c0a8e4224360a6917dcb5d889b08d5fdcfaf7c2d Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 10 Sep 2020 16:21:37 -0400 Subject: [PATCH] Fix empty line handling when formatting typing stubs (#1646) Black used to erroneously remove all empty lines between non-function code and decorators when formatting typing stubs. Now a single empty line is enforced. I chose for putting empty lines around decorated classes that have empty bodies since removing empty lines around such classes would cause a formatting issue that seems to be impossible to fix. For example: ``` class A: ... @some_decorator class B: ... class C: ... class D: ... @some_other_decorator def foo(): -> None: ... ``` It is easy to enforce no empty lines between class A, B, and C. Just return 0, 0 for a line that is a decorator and precedes an stub class. Fortunately before this commit, empty lines after that class would be removed already. Now let's look at the empty line between class D and function foo. In this case, there should be an empty line there since it's class code next to function code. The problem is that when deciding to add X empty lines before a decorator, you can't tell whether it's before a class or a function. If the decorator is before a function, then an empty line is needed, while no empty lines are needed when the decorator is before a class. So even though I personally prefer no empty lines around decorated classes, I had to go the other way surrounding decorated classes with empty lines. Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 ++ docs/change_log.md | 3 ++ src/black/__init__.py | 11 +++++-- tests/data/force_pyi.py | 67 ++++++++++++++++++++++++++++++++++++++--- tests/test_black.py | 7 +++-- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 52c8016..59d9320 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,9 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer removes all empty lines between non-function code and decorators + when formatting typing stubs. Now `Black` enforces a single empty line. (#1646) + - `Black` no longer adds an incorrect space after a parenthesized assignment expression in if/while statements (#1655) diff --git a/docs/change_log.md b/docs/change_log.md index cc5015f..e183ca5 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -9,6 +9,9 @@ - `Black` now respects `--skip-string-normalization` when normalizing multiline docstring quotes (#1637) +- `Black` no longer removes all empty lines between non-function code and decorators + when formatting typing stubs. Now `Black` enforces a single empty line. (#1646) + - `Black` no longer adds an incorrect space after a parenthesized assignment expression in if/while statements (#1655) diff --git a/src/black/__init__.py b/src/black/__init__.py index bfb7712..9e18a7d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1834,6 +1834,10 @@ class EmptyLineTracker: return 0, 0 if self.previous_line.is_decorator: + if self.is_pyi and current_line.is_stub_class: + # Insert an empty line after a decorated stub class + return 0, 1 + return 0, 0 if self.previous_line.depth < current_line.depth and ( @@ -1857,8 +1861,11 @@ class EmptyLineTracker: newlines = 0 else: newlines = 1 - elif current_line.is_def and not self.previous_line.is_def: - # Blank line between a block of functions and a block of non-functions + elif ( + current_line.is_def or current_line.is_decorator + ) and not self.previous_line.is_def: + # Blank line between a block of functions (maybe with preceding + # decorators) and a block of non-functions newlines = 1 else: newlines = 0 diff --git a/tests/data/force_pyi.py b/tests/data/force_pyi.py index 25246c2..07ed93c 100644 --- a/tests/data/force_pyi.py +++ b/tests/data/force_pyi.py @@ -1,6 +1,65 @@ -def f(): ... +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... -def g(): ... # output -def f(): ... -def g(): ... + +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/tests/test_black.py b/tests/test_black.py index edcf720..e928dc0 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1574,7 +1574,6 @@ class BlackTestCase(unittest.TestCase): black.assert_stable(source, actual, DEFAULT_MODE) def test_single_file_force_pyi(self) -> None: - reg_mode = DEFAULT_MODE pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: @@ -1587,9 +1586,11 @@ class BlackTestCase(unittest.TestCase): # verify cache with --pyi is separate pyi_cache = black.read_cache(pyi_mode) self.assertIn(path, pyi_cache) - normal_cache = black.read_cache(reg_mode) + normal_cache = black.read_cache(DEFAULT_MODE) self.assertNotIn(path, normal_cache) - self.assertEqual(actual, expected) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(contents, actual) + black.assert_stable(contents, actual, pyi_mode) @event_loop() def test_multi_file_force_pyi(self) -> None: -- 2.39.5