Skip to content

Commit

Permalink
Enforce empty lines before classes/functions with sticky leading comm…
Browse files Browse the repository at this point in the history
…ents. (#3302)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
yilei and JelleZijlstra committed Oct 26, 2022
1 parent fbc5136 commit 4abc039
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 45 deletions.
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
86 changes: 75 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"]
original_line: Line
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,
original_line=current_line,
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,27 @@ 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.original_line.is_class
and not slc.previous_block.original_line.opens_block
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 +666,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

0 comments on commit 4abc039

Please sign in to comment.