diff --git a/CHANGES.md b/CHANGES.md index 2fa0cb41b38..3295042dbd3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,8 @@ entry (#3393) - `with` statements that contain two context managers will be consistently wrapped in parentheses (#3589) +- For stubs, enforce one blank line after a nested class with a body other than just + `...` (#3564) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 4b57d1f0ea8..fb3cf698e4b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -501,7 +501,7 @@ class EmptyLineTracker: mode: Mode previous_line: Optional[Line] = None previous_block: Optional[LinesBlock] = None - previous_defs: List[int] = field(default_factory=list) + previous_defs: List[Line] = field(default_factory=list) semantic_leading_comment: Optional[LinesBlock] = None def maybe_empty_lines(self, current_line: Line) -> LinesBlock: @@ -557,12 +557,18 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: else: before = 0 depth = current_line.depth - while self.previous_defs and self.previous_defs[-1] >= depth: + while self.previous_defs and self.previous_defs[-1].depth >= depth: 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. before = min(1, before) + elif ( + Preview.blank_line_after_nested_stub_class in self.mode + and self.previous_defs[-1].is_class + and not self.previous_defs[-1].is_stub_class + ): + before = 1 elif depth: before = 0 else: @@ -616,7 +622,7 @@ def _maybe_empty_lines_for_class_or_def( self, current_line: Line, before: int ) -> Tuple[int, int]: if not current_line.is_decorator: - self.previous_defs.append(current_line.depth) + self.previous_defs.append(current_line) if self.previous_line is None: # Don't insert empty lines before the first line in the file. return 0, 0 diff --git a/src/black/mode.py b/src/black/mode.py index d70388916da..c40610d5783 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -154,6 +154,7 @@ class Preview(Enum): """Individual preview style features.""" add_trailing_comma_consistently = auto() + blank_line_after_nested_stub_class = auto() hex_codes_in_unicode_sequences = auto() multiline_string_handling = auto() prefer_splitting_right_hand_side_of_assignments = auto() diff --git a/tests/data/miscellaneous/nested_class_stub.pyi b/tests/data/miscellaneous/nested_class_stub.pyi new file mode 100644 index 00000000000..daf281b517b --- /dev/null +++ b/tests/data/miscellaneous/nested_class_stub.pyi @@ -0,0 +1,16 @@ +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + class Inner: + inner_attr: int + outer_attr: int + +# output +class Outer: + class InnerStub: ... + outer_attr_after_inner_stub: int + + class Inner: + inner_attr: int + + outer_attr: int diff --git a/tests/test_format.py b/tests/test_format.py index ab849aac9a3..3a6cbc9e2e9 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -186,6 +186,12 @@ def test_stub() -> None: assert_format(source, expected, mode) +def test_nested_class_stub() -> None: + mode = replace(DEFAULT_MODE, is_pyi=True, preview=True) + source, expected = read_data("miscellaneous", "nested_class_stub.pyi") + assert_format(source, expected, mode) + + def test_power_op_newline() -> None: # requires line_length=0 source, expected = read_data("miscellaneous", "power_op_newline")