Skip to content

Commit

Permalink
Fix empty line handling when formatting typing stubs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ichard26 committed Aug 29, 2020
1 parent 2b75f88 commit f1f8f63
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 10 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/change_log.md
Expand Up @@ -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_
Expand Down
13 changes: 10 additions & 3 deletions src/black/__init__.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
67 changes: 63 additions & 4 deletions 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]: ...
7 changes: 4 additions & 3 deletions tests/test_black.py
Expand Up @@ -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:
Expand All @@ -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:
Expand Down

0 comments on commit f1f8f63

Please sign in to comment.