From f1f8f63fb7739cdb1c75ad4fc96fcc1a1daf8e26 Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 27 Aug 2020 21:54:19 -0400 Subject: [PATCH] Fix empty line handling when formatting typing stubs 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. --- CHANGES.md | 3 ++ docs/change_log.md | 5 +++ src/black/__init__.py | 13 ++++++-- tests/data/force_pyi.py | 67 ++++++++++++++++++++++++++++++++++++++--- tests/test_black.py | 7 +++-- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1c53604d4d5..d93ee1a2e92 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) + - fixed a crash when PWD=/ on POSIX (#1631) ### 20.8b1 diff --git a/docs/change_log.md b/docs/change_log.md index b7337166659..444496188e2 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -9,6 +9,11 @@ - `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) + +- fixed a crash when PWD=/ on POSIX (#1631) + ### 20.8b1 #### _Packaging_ diff --git a/src/black/__init__.py b/src/black/__init__.py index 048e771ce96..3afeac4380c 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1824,7 +1824,11 @@ def _maybe_empty_lines_for_class_or_def( return 0, 0 if self.previous_line.is_decorator: - return 0, 0 + if self.is_pyi and current_line.is_stub_class: + # Insert an empty line after a decorated stub class + return 0, 1 + else: + return 0, 0 if self.previous_line.depth < current_line.depth and ( self.previous_line.is_class or self.previous_line.is_def @@ -1847,8 +1851,11 @@ def _maybe_empty_lines_for_class_or_def( 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 25246c22ca7..e5f92ce5288 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 = 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 = 1 + +def eggs() -> Union[str, int]: ... diff --git a/tests/test_black.py b/tests/test_black.py index bc80c8fca8b..4d054584780 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1527,7 +1527,6 @@ def test_tricky_unicode_symbols(self) -> None: 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: @@ -1540,9 +1539,11 @@ def test_single_file_force_pyi(self) -> None: # 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: