From 7bb859eda7bdd1ff0f7bccf060c0410e6047aae3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 19:04:08 -0800 Subject: [PATCH 1/3] Start to unify docstrings --- src/black/linegen.py | 2 +- src/black/lines.py | 9 +++++++-- src/black/nodes.py | 3 +++ src/black/output.py | 4 ++-- src/black/strings.py | 2 ++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 073672a5ae7..a74afcd1554 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -475,7 +475,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote = quote_char * quote_len # It's invalid to put closing single-character quotes on a new line. - if self.mode and quote_len == 3: + if quote_len == 3: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. diff --git a/src/black/lines.py b/src/black/lines.py index 4050f819757..f43417d58fe 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -209,6 +209,11 @@ def is_triple_quoted_string(self) -> bool: return True return False + @property + def is_docstring(self) -> bool: + """Is the line a docstring?""" + return bool(self) and is_docstring(self.leaves[0]) + @property def is_chained_assignment(self) -> bool: """Is the line a chained assignment""" @@ -576,7 +581,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and 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_triple_quoted_string + and self.previous_block.original_line.is_docstring and not (current_line.is_class or current_line.is_def) ): before = 1 @@ -683,7 +688,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if ( self.previous_line and self.previous_line.is_class - and current_line.is_triple_quoted_string + and current_line.is_docstring ): if Preview.no_blank_line_before_class_docstring in current_line.mode: return 0, 1 diff --git a/src/black/nodes.py b/src/black/nodes.py index de53f8e36a3..22d925c2754 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -539,6 +539,9 @@ def is_docstring(leaf: Leaf) -> bool: if set(prefix).intersection("bBfF"): return False + if not leaf.parent.prev_sibling: + return True + if prev_siblings_are( leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] ): diff --git a/src/black/output.py b/src/black/output.py index 7c7dd0fe14e..a8dd4e3aee2 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -45,8 +45,8 @@ def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str: b_nb = json.loads(b) diff_lines = [ diff( - "".join(a_nb["cells"][cell_number]["source"]) + "\n", - "".join(b_nb["cells"][cell_number]["source"]) + "\n", + """""".join(a_nb["cells"][cell_number]["source"]) + "\n", + """""".join(b_nb["cells"][cell_number]["source"]) + "\n", f"{a_name}:cell_{cell_number}", f"{b_name}:cell_{cell_number}", ) diff --git a/src/black/strings.py b/src/black/strings.py index 0d30f09ed11..0e0f968824b 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -63,6 +63,8 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: ) else: lines.append(line) + if s.endswith("\n"): + lines.append("") return lines From 7ee8e6f960f25b21fdfb1c72d46a4906280f8ab9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 19:17:09 -0800 Subject: [PATCH 2/3] Fix implementation --- CHANGES.md | 1 + src/black/linegen.py | 2 +- src/black/lines.py | 8 +++++--- src/black/mode.py | 1 + src/black/nodes.py | 11 +++++++++-- src/black/output.py | 4 ++-- tests/data/cases/module_docstring_2.py | 2 ++ 7 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa0d2494f67..403b2246470 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ - Allow empty lines at the beginning of all blocks, except immediately before a docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) +- Format module docstrings the same as class and function docstrings (#4095) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index a74afcd1554..2ffb7571b7b 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -422,7 +422,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) - if is_docstring(leaf) and not re.search(r"\\\s*\n", leaf.value): + if is_docstring(leaf, self.mode) and not re.search(r"\\\s*\n", leaf.value): # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. if self.mode.string_normalization: diff --git a/src/black/lines.py b/src/black/lines.py index f43417d58fe..86fc2110d91 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -196,7 +196,7 @@ def is_class_paren_empty(self) -> bool: ) @property - def is_triple_quoted_string(self) -> bool: + def _is_triple_quoted_string(self) -> bool: """Is the line a triple quoted string?""" if not self or self.leaves[0].type != token.STRING: return False @@ -212,7 +212,9 @@ def is_triple_quoted_string(self) -> bool: @property def is_docstring(self) -> bool: """Is the line a docstring?""" - return bool(self) and is_docstring(self.leaves[0]) + if Preview.format_module_docstring not in self.mode: + return self._is_triple_quoted_string + return bool(self) and is_docstring(self.leaves[0], self.mode) @property def is_chained_assignment(self) -> bool: @@ -699,7 +701,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: is_empty_first_line_ok = ( Preview.allow_empty_first_line_in_block in current_line.mode and ( - not is_docstring(current_line.leaves[0]) + not is_docstring(current_line.leaves[0], current_line.mode) or ( self.previous_line and self.previous_line.leaves[0] diff --git a/src/black/mode.py b/src/black/mode.py index 9df19618363..55bface6365 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,6 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() + format_module_docstring = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index 22d925c2754..a9505db9b07 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -531,7 +531,7 @@ def is_arith_like(node: LN) -> bool: } -def is_docstring(leaf: Leaf) -> bool: +def is_docstring(leaf: Leaf, mode: Mode) -> bool: if leaf.type != token.STRING: return False @@ -539,7 +539,14 @@ def is_docstring(leaf: Leaf) -> bool: if set(prefix).intersection("bBfF"): return False - if not leaf.parent.prev_sibling: + if ( + Preview.format_module_docstring in mode + and leaf.parent + and leaf.parent.type == syms.simple_stmt + and not leaf.parent.prev_sibling + and leaf.parent.parent + and leaf.parent.parent.type == syms.file_input + ): return True if prev_siblings_are( diff --git a/src/black/output.py b/src/black/output.py index a8dd4e3aee2..7c7dd0fe14e 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -45,8 +45,8 @@ def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str: b_nb = json.loads(b) diff_lines = [ diff( - """""".join(a_nb["cells"][cell_number]["source"]) + "\n", - """""".join(b_nb["cells"][cell_number]["source"]) + "\n", + "".join(a_nb["cells"][cell_number]["source"]) + "\n", + "".join(b_nb["cells"][cell_number]["source"]) + "\n", f"{a_name}:cell_{cell_number}", f"{b_name}:cell_{cell_number}", ) diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index e1f81b4d76b..1cc9aea9aea 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -1,6 +1,7 @@ # flags: --preview """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, @@ -38,6 +39,7 @@ # output """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, From 8e926808aa0a79930d96f4f46a176fe3be407ce9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 19:25:36 -0800 Subject: [PATCH 3/3] Found another behavior change --- src/black/lines.py | 2 +- src/black/mode.py | 2 +- src/black/nodes.py | 2 +- tests/data/cases/preview_no_blank_line_before_docstring.py | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/black/lines.py b/src/black/lines.py index 86fc2110d91..e80914de4d1 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -212,7 +212,7 @@ def _is_triple_quoted_string(self) -> bool: @property def is_docstring(self) -> bool: """Is the line a docstring?""" - if Preview.format_module_docstring not in self.mode: + if Preview.unify_docstring_detection not in self.mode: return self._is_triple_quoted_string return bool(self) and is_docstring(self.leaves[0], self.mode) diff --git a/src/black/mode.py b/src/black/mode.py index 55bface6365..6600f9dad75 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,7 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() - format_module_docstring = auto() + unify_docstring_detection = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index a9505db9b07..4f7a88e6ace 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -540,7 +540,7 @@ def is_docstring(leaf: Leaf, mode: Mode) -> bool: return False if ( - Preview.format_module_docstring in mode + Preview.unify_docstring_detection in mode and leaf.parent and leaf.parent.type == syms.simple_stmt and not leaf.parent.prev_sibling diff --git a/tests/data/cases/preview_no_blank_line_before_docstring.py b/tests/data/cases/preview_no_blank_line_before_docstring.py index 303035a7efb..faeaa1e46e4 100644 --- a/tests/data/cases/preview_no_blank_line_before_docstring.py +++ b/tests/data/cases/preview_no_blank_line_before_docstring.py @@ -29,6 +29,9 @@ class MultilineDocstringsAsWell: and on so many lines... """ +class SingleQuotedDocstring: + + "I'm a docstring but I don't even get triple quotes." # output @@ -57,3 +60,7 @@ class MultilineDocstringsAsWell: and on so many lines... """ + + +class SingleQuotedDocstring: + "I'm a docstring but I don't even get triple quotes."