diff --git a/CHANGES.md b/CHANGES.md index e8d7c0b4e9a..f6ec50fe8ba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Fix bug where module docstrings would be treated as normal strings if preceeded by + comments (#4764) + ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 837aec457b0..c1e88c1cba5 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -35,6 +35,8 @@ Currently, the following features are included in the preview style: types in `except` and `except*` without `as`. See PEP 758 for details. - `normalize_cr_newlines`: Add `\r` style newlines to the potential newlines to normalize file newlines both from and to. +- `fix_module_docstring_detection`: Fix module docstrings being treated as normal + strings if preceeded by comments. (labels/unstable-features)= diff --git a/src/black/lines.py b/src/black/lines.py index 21e6cec571d..b3fdd4ae3a3 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -559,15 +559,20 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: before, after = self._maybe_empty_lines(current_line) previous_after = self.previous_block.after if self.previous_block else 0 before = max(0, before - previous_after) - if ( + if Preview.fix_module_docstring_detection in self.mode: # Always have one empty line after a module docstring - self.previous_block - and self.previous_block.previous_block is None - and len(self.previous_block.original_line.leaves) == 1 - and self.previous_block.original_line.is_docstring - and not (current_line.is_class or current_line.is_def) - ): - before = 1 + if self._line_is_module_docstring(current_line): + before = 1 + else: + if ( + # Always have one empty line after a module docstring + self.previous_block + and self.previous_block.previous_block is None + and len(self.previous_block.original_line.leaves) == 1 + and self.previous_block.original_line.is_docstring + and not (current_line.is_class or current_line.is_def) + ): + before = 1 block = LinesBlock( mode=self.mode, @@ -595,6 +600,22 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: self.previous_block = block return block + def _line_is_module_docstring(self, current_line: Line) -> bool: + previous_block = self.previous_block + if not previous_block: + return False + if ( + len(previous_block.original_line.leaves) != 1 + or not previous_block.original_line.is_docstring + or current_line.is_class + or current_line.is_def + ): + return False + while previous_block := previous_block.previous_block: + if not previous_block.original_line.is_comment: + return False + return True + def _maybe_empty_lines(self, current_line: Line) -> tuple[int, int]: # noqa: C901 max_allowed = 1 if current_line.depth == 0: diff --git a/src/black/mode.py b/src/black/mode.py index 85a205949dc..9927f73c4a7 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -236,6 +236,7 @@ class Preview(Enum): # except* without as. See PEP 758 for details. remove_parens_around_except_types = auto() normalize_cr_newlines = auto() + fix_module_docstring_detection = auto() UNSTABLE_FEATURES: set[Preview] = { diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 549e0e8049f..8ea19908570 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -88,7 +88,8 @@ "fix_fmt_skip_in_one_liners", "wrap_comprehension_in", "remove_parens_around_except_types", - "normalize_cr_newlines" + "normalize_cr_newlines", + "fix_module_docstring_detection" ] }, "description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features." diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 36e865cdb46..06b3790b115 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -9,6 +9,7 @@ how this parsing engine works. """ + from collections.abc import Callable, Iterator from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Optional, Union, cast diff --git a/tests/data/cases/module_docstring_after_comment.py b/tests/data/cases/module_docstring_after_comment.py new file mode 100644 index 00000000000..6a755b36dd2 --- /dev/null +++ b/tests/data/cases/module_docstring_after_comment.py @@ -0,0 +1,22 @@ +# flags: --preview +#!/python + +# regression test for #4762 +""" +docstring +""" +from __future__ import annotations + +import os + +# output +#!/python + +# regression test for #4762 +""" +docstring +""" + +from __future__ import annotations + +import os