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

Enforce empty lines before classes/functions with sticky leading comments. #3302

Merged
merged 14 commits into from Oct 26, 2022
Merged
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -14,6 +14,8 @@

<!-- Changes that affect Black's preview style -->

- Enforce empty lines before classes and functions with sticky leading comments (#3302)

### Configuration

<!-- Changes to how Black can be configured -->
Expand Down
22 changes: 14 additions & 8 deletions docs/contributing/reference/reference_classes.rst
Expand Up @@ -11,23 +11,29 @@
.. autoclass:: black.brackets.BracketTracker
:members:

:class:`EmptyLineTracker`
:class:`Line`
-------------

.. autoclass:: black.lines.Line
:members:
:special-members: __str__, __bool__

:class:`LinesBlock`
-------------------------

.. autoclass:: black.EmptyLineTracker
.. autoclass:: black.lines.LinesBlock
:members:

:class:`Line`
-------------
:class:`EmptyLineTracker`
-------------------------

.. autoclass:: black.Line
.. autoclass:: black.lines.EmptyLineTracker
:members:
:special-members: __str__, __bool__

:class:`LineGenerator`
----------------------

.. autoclass:: black.LineGenerator
.. autoclass:: black.linegen.LineGenerator
:show-inheritance:
:members:

Expand All @@ -40,7 +46,7 @@
:class:`Report`
---------------

.. autoclass:: black.Report
.. autoclass:: black.report.Report
:members:
:special-members: __str__

Expand Down
49 changes: 35 additions & 14 deletions docs/the_black_code_style/future_style.md
Expand Up @@ -63,26 +63,47 @@ 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 newlines in the beginning of code blocks
### Improved empty line management

_Black_ will remove newlines in the beginning of new code blocks, i.e. when the
indentation level is increased. For example:
1. _Black_ will remove newlines in the beginning of new code blocks, i.e. when the
indentation level is increased. For example:

```python
def my_func():
```python
def my_func():

print("The line above me will be deleted!")
```
print("The line above me will be deleted!")
```

will be changed to:
will be changed to:

```python
def my_func():
print("The line above me will be deleted!")
```

This new feature will be applied to **all code blocks**: `def`, `class`, `if`,
`for`, `while`, `with`, `case` and `match`.

2. _Black_ will enforce empty lines before classes and functions with leading comments.
For example:

```python
some_var = 1
# Leading sticky comment
def my_func():
...
```

will be changed to:

```python
some_var = 1

```python
def my_func():
print("The line above me will be deleted!")
```

This new feature will be applied to **all code blocks**: `def`, `class`, `if`, `for`,
`while`, `with`, `case` and `match`.
# Leading sticky comment
def my_func():
...
```

### Improved parentheses management

Expand Down
21 changes: 12 additions & 9 deletions src/black/__init__.py
Expand Up @@ -61,7 +61,7 @@
unmask_cell,
)
from black.linegen import LN, LineGenerator, transform_line
from black.lines import EmptyLineTracker, Line
from black.lines import EmptyLineTracker, LinesBlock
from black.mode import (
FUTURE_FLAG_TO_FEATURE,
VERSION_TO_FEATURES,
Expand Down Expand Up @@ -1075,7 +1075,7 @@ def f(

def _format_str_once(src_contents: str, *, mode: Mode) -> str:
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
dst_contents = []
dst_blocks: List[LinesBlock] = []
if mode.target_versions:
versions = mode.target_versions
else:
Expand All @@ -1084,22 +1084,25 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:

normalize_fmt_off(src_node, preview=mode.preview)
lines = LineGenerator(mode=mode)
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
empty_line = Line(mode=mode)
after = 0
elt = EmptyLineTracker(mode=mode)
split_line_features = {
feature
for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
if supports_feature(versions, feature)
}
block: Optional[LinesBlock] = None
for current_line in lines.visit(src_node):
dst_contents.append(str(empty_line) * after)
before, after = elt.maybe_empty_lines(current_line)
dst_contents.append(str(empty_line) * before)
block = elt.maybe_empty_lines(current_line)
dst_blocks.append(block)
for line in transform_line(
current_line, mode=mode, features=split_line_features
):
dst_contents.append(str(line))
block.content_lines.append(str(line))
if dst_blocks:
dst_blocks[-1].after = 0
dst_contents = []
for block in dst_blocks:
dst_contents.extend(block.all_lines())
return "".join(dst_contents)


Expand Down
85 changes: 74 additions & 11 deletions src/black/lines.py
Expand Up @@ -448,6 +448,28 @@ def __bool__(self) -> bool:
return bool(self.leaves or self.comments)


@dataclass
class LinesBlock:
"""Class that holds information about a block of formatted lines.

This is introduced so that the EmptyLineTracker can look behind the standalone
comments and adjust their empty lines for class or def lines.
"""

mode: Mode
previous_block: Optional["LinesBlock"]
is_class: bool # Whether the original line is a class def.
before: int = 0
content_lines: List[str] = field(default_factory=list)
after: int = 0

def all_lines(self) -> List[str]:
empty_line = str(Line(mode=self.mode))
return (
[empty_line * self.before] + self.content_lines + [empty_line * self.after]
)


@dataclass
class EmptyLineTracker:
"""Provides a stateful method that returns the number of potential extra
Expand All @@ -458,33 +480,55 @@ class EmptyLineTracker:
are consumed by `maybe_empty_lines()` and included in the computation.
"""

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

def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
def maybe_empty_lines(self, current_line: Line) -> LinesBlock:
"""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)
previous_after = self.previous_block.after if self.previous_block else 0
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
else before - previous_after
)
self.previous_after = after
block = LinesBlock(
mode=self.mode,
previous_block=self.previous_block,
is_class=current_line.is_class,
before=before,
after=after,
)

# Maintain the semantic_leading_comment state.
if current_line.is_comment:
if self.previous_line is None or (
not self.previous_line.is_decorator
# `or before` means this comment already has an empty line before
and (not self.previous_line.is_comment or before)
and (self.semantic_leading_comment is None or before)
):
self.semantic_leading_comment = block
elif not current_line.is_decorator:
self.semantic_leading_comment = None

self.previous_line = current_line
return before, after
self.previous_block = block
return block

def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
max_allowed = 1
if current_line.depth == 0:
max_allowed = 1 if self.is_pyi else 2
max_allowed = 1 if self.mode.is_pyi else 2
if current_line.leaves:
# Consume the first leaf's extra newlines.
first_leaf = current_line.leaves[0]
Expand All @@ -495,7 +539,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
before = 0
depth = current_line.depth
while self.previous_defs and self.previous_defs[-1] >= depth:
if self.is_pyi:
if self.mode.is_pyi:
assert self.previous_line is not None
if depth and not current_line.is_def and self.previous_line.is_def:
# Empty lines between attributes and methods should be preserved.
Expand Down Expand Up @@ -563,7 +607,7 @@ def _maybe_empty_lines_for_class_or_def(
return 0, 0

if self.previous_line.is_decorator:
if self.is_pyi and current_line.is_stub_class:
if self.mode.is_pyi and current_line.is_stub_class:
# Insert an empty line after a decorated stub class
return 0, 1

Expand All @@ -574,14 +618,26 @@ def _maybe_empty_lines_for_class_or_def(
):
return 0, 0

comment_to_add_newlines: Optional[LinesBlock] = None
if (
self.previous_line.is_comment
and self.previous_line.depth == current_line.depth
and before == 0
):
return 0, 0
slc = self.semantic_leading_comment
if (
Preview.empty_lines_before_class_or_def_with_leading_comments
in current_line.mode
and slc is not None
and slc.previous_block is not None
and not slc.previous_block.is_class
and slc.before <= 1
):
comment_to_add_newlines = slc
else:
return 0, 0

if self.is_pyi:
if self.mode.is_pyi:
if current_line.is_class or self.previous_line.is_class:
if self.previous_line.depth < current_line.depth:
newlines = 0
Expand Down Expand Up @@ -609,6 +665,13 @@ def _maybe_empty_lines_for_class_or_def(
newlines = 0
else:
newlines = 1 if current_line.depth else 2
if comment_to_add_newlines is not None:
previous_block = comment_to_add_newlines.previous_block
if previous_block is not None:
comment_to_add_newlines.before = (
max(comment_to_add_newlines.before, newlines) - previous_block.after
)
newlines = 0
return newlines, 0


Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Expand Up @@ -150,6 +150,7 @@ class Preview(Enum):
"""Individual preview style features."""

annotation_parens = auto()
empty_lines_before_class_or_def_with_leading_comments = auto()
long_docstring_quotes_on_newline = auto()
normalize_docstring_quotes_and_prefixes_properly = auto()
one_element_subscript = auto()
Expand Down