Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardise newlines after module-level docstrings #2996

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -20,6 +20,7 @@
- Remove redundant parentheses around awaited objects (#2991)
- Parentheses around return annotations are now managed (#2990)
- Remove unnecessary parentheses from `with` statements (#2926)
- Require one empty line after module-level docstrings (#2996)

### _Blackd_

Expand Down
4 changes: 1 addition & 3 deletions src/black/__init__.py
Expand Up @@ -1177,15 +1177,13 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:
lines = LineGenerator(mode=mode)
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
empty_line = Line(mode=mode)
after = 0
split_line_features = {
feature
for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
if supports_feature(versions, feature)
}
for current_line in lines.visit(src_node):
dst_contents.append(str(empty_line) * after)
before, after = elt.maybe_empty_lines(current_line)
before = elt.maybe_empty_lines(current_line)
dst_contents.append(str(empty_line) * before)
for line in transform_line(
current_line, mode=mode, features=split_line_features
Expand Down
1 change: 1 addition & 0 deletions src/black/linegen.py
@@ -1,6 +1,7 @@
"""
Generating lines of code.
"""

from functools import partial, wraps
import sys
from typing import Collection, Iterator, List, Optional, Set, Union, cast
Expand Down
117 changes: 73 additions & 44 deletions src/black/lines.py
@@ -1,8 +1,10 @@
from collections import deque
from dataclasses import dataclass, field
import itertools
import sys
from typing import (
Callable,
Deque,
Dict,
Iterator,
List,
Expand Down Expand Up @@ -419,37 +421,50 @@ def __bool__(self) -> bool:
@dataclass
class EmptyLineTracker:
"""Provides a stateful method that returns the number of potential extra
empty lines needed before and after the currently processed line.
empty lines needed before the currently processed line.

Note: this tracker works on lines that haven't been split yet. It assumes
the prefix of the first leaf consists of optional newlines. Those newlines
are consumed by `maybe_empty_lines()` and included in the computation.
"""

is_pyi: bool = False
previous_line: Optional[Line] = None
previous_after: int = 0
previous_defs: List[int] = field(default_factory=list)

def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
# Since we shouldn't need to look back
# more than a couple lines we limit
# this window to the last 3 visited lines
# to keep memory constant when formatting large files.
previous_lines_window: Deque[Line] = field(default_factory=deque)
previous_lines_window_size = 3

def maybe_empty_lines(self, current_line: Line) -> int:
"""Return the number of extra empty lines before and after the `current_line`.

This is for separating `def`, `async def` and `class` with extra empty
lines (two on module-level).
"""
before, after = self._maybe_empty_lines(current_line)
before = self._maybe_empty_lines(current_line)
before = (
# Black should not insert empty lines at the beginning
# of the file
0
if self.previous_line is None
else before - self.previous_after
if not self.previous_lines_window
else before
)
self.previous_after = after
self.previous_line = current_line
return before, after
if (
Preview.module_docstring_newlines in current_line.mode
and len(self.previous_lines_window) == 1
and self.previous_lines_window[-1].is_triple_quoted_string
):
before = 1

self.previous_lines_window.append(current_line)
if len(self.previous_lines_window) > self.previous_lines_window_size:
self.previous_lines_window.popleft()

return before

def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
def _maybe_empty_lines(self, current_line: Line) -> int:
max_allowed = 1
if current_line.depth == 0:
max_allowed = 1 if self.is_pyi else 2
Expand All @@ -464,8 +479,12 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
depth = current_line.depth
while self.previous_defs and self.previous_defs[-1] >= depth:
if self.is_pyi:
assert self.previous_line is not None
if depth and not current_line.is_def and self.previous_line.is_def:
assert self.previous_lines_window
if (
depth
and not current_line.is_def
and self.previous_lines_window[-1].is_def
):
# Empty lines between attributes and methods should be preserved.
before = min(1, before)
elif depth:
Expand Down Expand Up @@ -499,64 +518,74 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
return self._maybe_empty_lines_for_class_or_def(current_line, before)

if (
self.previous_line
and self.previous_line.is_import
self.previous_lines_window
and self.previous_lines_window[-1].is_import
and not current_line.is_import
and depth == self.previous_line.depth
and depth == self.previous_lines_window[-1].depth
):
return (before or 1), 0
return before or 1

if (
self.previous_line
and self.previous_line.is_class
and current_line.is_triple_quoted_string
len(self.previous_lines_window) > 1
and self.previous_lines_window[-2].is_class
and self.previous_lines_window[-1].is_triple_quoted_string
and current_line.depth == self.previous_lines_window[-1].depth
):
return before, 1
return 1

return before, 0
return before

def _maybe_empty_lines_for_class_or_def(
self, current_line: Line, before: int
) -> Tuple[int, int]:
) -> int:
if not current_line.is_decorator:
self.previous_defs.append(current_line.depth)
if self.previous_line is None:
if not self.previous_lines_window:
# Don't insert empty lines before the first line in the file.
return 0, 0
return 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
if self.previous_lines_window[-1].is_decorator:
return 0

return 0, 0
if (
self.is_pyi
and len(self.previous_lines_window) > 1
and self.previous_lines_window[-1].is_stub_class
and self.previous_lines_window[-2].is_decorator
):
# Insert an empty line after a decorated stub class
return 1

if self.previous_line.depth < current_line.depth and (
self.previous_line.is_class or self.previous_line.is_def
if self.previous_lines_window[-1].depth < current_line.depth and (
self.previous_lines_window[-1].is_class
or self.previous_lines_window[-1].is_def
):
return 0, 0
return 0

if (
self.previous_line.is_comment
and self.previous_line.depth == current_line.depth
self.previous_lines_window[-1].is_comment
and self.previous_lines_window[-1].depth == current_line.depth
and before == 0
):
return 0, 0
return 0

if self.is_pyi:
if current_line.is_class or self.previous_line.is_class:
if self.previous_line.depth < current_line.depth:
if current_line.is_class or self.previous_lines_window[-1].is_class:
if self.previous_lines_window[-1].depth < current_line.depth:
newlines = 0
elif self.previous_line.depth > current_line.depth:
elif self.previous_lines_window[-1].depth > current_line.depth:
newlines = 1
elif current_line.is_stub_class and self.previous_line.is_stub_class:
elif (
current_line.is_stub_class
and self.previous_lines_window[-1].is_stub_class
):
# No blank line between classes with an empty body
newlines = 0
else:
newlines = 1
elif (
current_line.is_def or current_line.is_decorator
) and not self.previous_line.is_def:
) and not self.previous_lines_window[-1].is_def:
if current_line.depth:
# In classes empty lines between attributes and methods should
# be preserved.
Expand All @@ -565,13 +594,13 @@ def _maybe_empty_lines_for_class_or_def(
# Blank line between a block of functions (maybe with preceding
# decorators) and a block of non-functions
newlines = 1
elif self.previous_line.depth > current_line.depth:
elif self.previous_lines_window[-1].depth > current_line.depth:
newlines = 1
else:
newlines = 0
else:
newlines = 1 if current_line.depth else 2
return newlines, 0
return newlines


def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Expand Up @@ -147,6 +147,7 @@ class Preview(Enum):
remove_redundant_parens = auto()
one_element_subscript = auto()
annotation_parens = auto()
module_docstring_newlines = auto()


class Deprecated(UserWarning):
Expand Down
1 change: 1 addition & 0 deletions src/black/numerics.py
@@ -1,6 +1,7 @@
"""
Formatting numeric literals.
"""

from blib2to3.pytree import Leaf


Expand Down
1 change: 1 addition & 0 deletions src/black/parsing.py
@@ -1,6 +1,7 @@
"""
Parse Python code and perform AST validation.
"""

import ast
import platform
import sys
Expand Down
1 change: 1 addition & 0 deletions src/black/report.py
@@ -1,6 +1,7 @@
"""
Summarize Black runs to users.
"""

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
Expand Down
1 change: 1 addition & 0 deletions src/black/rusty.py
Expand Up @@ -2,6 +2,7 @@

See https://doc.rust-lang.org/book/ch09-00-error-handling.html.
"""

from typing import Generic, TypeVar, Union


Expand Down
1 change: 1 addition & 0 deletions src/black/trans.py
@@ -1,6 +1,7 @@
"""
String transformers that can split and merge strings.
"""

from abc import ABC, abstractmethod
from collections import defaultdict
from dataclasses import dataclass
Expand Down
25 changes: 25 additions & 0 deletions tests/data/module_docstring_1.py
@@ -0,0 +1,25 @@
"""Single line module-level docstring should be followed by single newline."""




a = 1


"""I'm just a string so should be followed by 2 newlines."""




b = 2

# output
"""Single line module-level docstring should be followed by single newline."""

a = 1


"""I'm just a string so should be followed by 2 newlines."""


b = 2
67 changes: 67 additions & 0 deletions tests/data/module_docstring_2.py
@@ -0,0 +1,67 @@
"""I am a very helpful module docstring.

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
"""




a = 1


"""Look at me I'm a docstring...

............................................................
............................................................
............................................................
............................................................
............................................................
............................................................
............................................................
........................................................NOT!
"""




b = 2

# output
"""I am a very helpful module docstring.

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
"""

a = 1


"""Look at me I'm a docstring...

............................................................
............................................................
............................................................
............................................................
............................................................
............................................................
............................................................
........................................................NOT!
"""


b = 2
3 changes: 2 additions & 1 deletion tests/data/string_quotes.py
@@ -1,4 +1,5 @@
''''''

'\''
'"'
"'"
Expand Down Expand Up @@ -57,8 +58,8 @@
f"\"{a}\"{'hello' * b}\"{c}\""

# output

""""""

"'"
'"'
"'"
Expand Down