From d51aa6a18be678a11b1e3b570059f53fb12ea8f8 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 31 Jan 2024 23:48:58 +0100 Subject: [PATCH] Keep trailing newline in module level docstring --- .../src/other/string_literal.rs | 4 +- .../src/string/docstring.rs | 13 +- ...lack_compatibility@cases__comments.py.snap | 324 ------------------ ...tibility@cases__module_docstring_2.py.snap | 134 -------- .../format@docstring_code_examples.py.snap | 54 +-- .../tests/snapshots/format@preview.py.snap | 15 +- .../format@stub_files__suite.pyi.snap | 7 +- 7 files changed, 60 insertions(+), 491 deletions(-) delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__comments.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__module_docstring_2.py.snap diff --git a/crates/ruff_python_formatter/src/other/string_literal.rs b/crates/ruff_python_formatter/src/other/string_literal.rs index 3071f370986927..4edbb355181371 100644 --- a/crates/ruff_python_formatter/src/other/string_literal.rs +++ b/crates/ruff_python_formatter/src/other/string_literal.rs @@ -1,6 +1,7 @@ use ruff_python_ast::StringLiteral; use ruff_text_size::Ranged; +use crate::context::NodeLevel; use crate::prelude::*; use crate::preview::is_hex_codes_in_unicode_sequences_enabled; use crate::string::{docstring, Quoting, StringPart}; @@ -66,7 +67,8 @@ impl Format> for FormatStringLiteral<'_> { ); if self.layout.is_docstring() { - docstring::format(&normalized, f) + let is_top_level = matches!(f.context().node_level(), NodeLevel::TopLevel(_)); + docstring::format(&normalized, f, is_top_level) } else { normalized.fmt(f) } diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index c6669c776818db..95f13187cece8d 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -5,6 +5,7 @@ use std::{borrow::Cow, collections::VecDeque}; use ruff_python_parser::ParseError; + use {once_cell::sync::Lazy, regex::Regex}; use { ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed}, @@ -102,7 +103,11 @@ use super::{NormalizedString, QuoteChar}; /// line c /// """ /// ``` -pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> FormatResult<()> { +pub(crate) fn format( + normalized: &NormalizedString, + f: &mut PyFormatter, + is_module: bool, +) -> FormatResult<()> { let docstring = &normalized.text; // Black doesn't change the indentation of docstrings that contain an escaped newline @@ -201,6 +206,12 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form let trim_end = docstring .as_ref() .trim_end_matches(|c: char| c.is_whitespace() && c != '\n'); + + // When there is a trailing newline, we want to keep it for module docstrings + if trim_end.ends_with(&['\r', '\n'][..]) && is_module { + hard_line_break().fmt(f)?; + } + if needs_chaperone_space(normalized, trim_end) { space().fmt(f)?; } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__comments.py.snap deleted file mode 100644 index e8db9b2afcf16b..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__comments.py.snap +++ /dev/null @@ -1,324 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/comments.py ---- -## Input - -```python -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -6,8 +6,7 @@ - # Many, many, many lines. - """Module docstring. - --Possibly also many, many lines. --""" -+Possibly also many, many lines.""" - - import os.path - import sys -``` - -## Ruff Output - -```python -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines.""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Output - -```python -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__module_docstring_2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__module_docstring_2.py.snap deleted file mode 100644 index db9b152a3d8c8e..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__module_docstring_2.py.snap +++ /dev/null @@ -1,134 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/module_docstring_2.py ---- -## Input - -```python -"""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, -quis nostrud exercitation ullamco laboris -nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate -velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, -sunt in culpa qui officia deserunt mollit anim id est laborum. -""" - - - - -a = 1 - - -"""Look at me I'm a docstring... - -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -........................................................NOT! -""" - - - - -b = 2 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -9,8 +9,7 @@ - Duis aute irure dolor in reprehenderit in voluptate - velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, --sunt in culpa qui officia deserunt mollit anim id est laborum. --""" -+sunt in culpa qui officia deserunt mollit anim id est laborum.""" - - a = 1 - -``` - -## Ruff Output - -```python -"""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, -quis nostrud exercitation ullamco laboris -nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate -velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, -sunt in culpa qui officia deserunt mollit anim id est laborum.""" - -a = 1 - - -"""Look at me I'm a docstring... - -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -........................................................NOT! -""" - - -b = 2 -``` - -## Black Output - -```python -"""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, -quis nostrud exercitation ullamco laboris -nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate -velit esse cillum dolore eu fugiat nulla pariatur. -Excepteur sint occaecat cupidatat non proident, -sunt in culpa qui officia deserunt mollit anim id est laborum. -""" - -a = 1 - - -"""Look at me I'm a docstring... - -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -............................................................ -........................................................NOT! -""" - - -b = 2 -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap index 2ffc898ef7a913..8ef9af5be102b4 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap @@ -1410,7 +1410,8 @@ This is a module docstring. with code in it. It should be formatted. button.js_on_event(ButtonClick, CustomJS( - code='console.log("JS:Click")'))""" + code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -2797,7 +2798,8 @@ This is a module docstring. with code in it. It should be formatted. button.js_on_event(ButtonClick, CustomJS( - code='console.log("JS:Click")'))""" + code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -4184,7 +4186,8 @@ This is a module docstring. with code in it. It should be formatted. button.js_on_event(ButtonClick, CustomJS( - code='console.log("JS:Click")'))""" + code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -5571,7 +5574,8 @@ This is a module docstring. with code in it. It should be formatted. button.js_on_event(ButtonClick, CustomJS( - code='console.log("JS:Click")'))""" + code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -6955,7 +6959,8 @@ This is a module docstring. with code in it. It should be formatted. button = Button() - button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")'))""" + button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -8306,7 +8311,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -492,10 +492,8 @@ +@@ -493,10 +493,8 @@ Do cool stuff:: if True: @@ -8319,7 +8324,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -970,13 +968,11 @@ +@@ -971,13 +969,11 @@ Do cool stuff. `````` @@ -8372,7 +8377,8 @@ This is a module docstring. with code in it. It should be formatted. button = Button() - button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")'))""" + button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -9723,7 +9729,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -492,10 +492,8 @@ +@@ -493,10 +493,8 @@ Do cool stuff:: if True: @@ -9736,7 +9742,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -970,13 +968,11 @@ +@@ -971,13 +969,11 @@ Do cool stuff. `````` @@ -9789,7 +9795,8 @@ This is a module docstring. with code in it. It should be formatted. button = Button() - button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")'))""" + button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -11149,7 +11156,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -501,10 +501,8 @@ +@@ -502,10 +502,8 @@ Do cool stuff:: if True: @@ -11162,7 +11169,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -979,13 +977,11 @@ +@@ -980,13 +978,11 @@ Do cool stuff. `````` @@ -11215,7 +11222,8 @@ This is a module docstring. with code in it. It should be formatted. button = Button() - button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")'))""" + button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -12566,7 +12574,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -492,10 +492,8 @@ +@@ -493,10 +493,8 @@ Do cool stuff:: if True: @@ -12579,7 +12587,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -970,13 +968,11 @@ +@@ -971,13 +969,11 @@ Do cool stuff. `````` @@ -12634,7 +12642,8 @@ This is a module docstring. with code in it. It should be formatted. button.js_on_event( ButtonClick, CustomJS(code='console.log("JS:Click")') - )""" + ) +""" # The simplest doctest to ensure basic formatting works. @@ -13994,7 +14003,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -503,10 +503,8 @@ +@@ -504,10 +504,8 @@ Do cool stuff:: if True: @@ -14007,7 +14016,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -981,13 +979,11 @@ +@@ -982,13 +980,11 @@ Do cool stuff. `````` @@ -14060,7 +14069,8 @@ This is a module docstring. with code in it. It should be formatted. button = Button() - button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")'))""" + button.js_on_event(ButtonClick, CustomJS(code='console.log("JS:Click")')) +""" # The simplest doctest to ensure basic formatting works. @@ -15411,7 +15421,7 @@ def markdown_skipped_rst_directive(): ```diff --- Stable +++ Preview -@@ -492,10 +492,8 @@ +@@ -493,10 +493,8 @@ Do cool stuff:: if True: @@ -15424,7 +15434,7 @@ def markdown_skipped_rst_directive(): Done. """ -@@ -970,13 +968,11 @@ +@@ -971,13 +969,11 @@ Do cool stuff. `````` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index 155de4722a4a34..3dbe31a6aa70d0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -90,7 +90,8 @@ source_type = Python ```python """ -Black's `Preview.module_docstring_newlines`""" +Black's `Preview.module_docstring_newlines` +""" first_stmt_after_module_level_docstring = 1 @@ -160,9 +161,10 @@ def f(): ```diff --- Stable +++ Preview -@@ -1,12 +1,12 @@ +@@ -1,13 +1,13 @@ + """ + Black's `Preview.module_docstring_newlines` """ - Black's `Preview.module_docstring_newlines`""" + first_stmt_after_module_level_docstring = 1 @@ -175,7 +177,7 @@ def f(): def raw_docstring(): -@@ -26,23 +26,22 @@ +@@ -27,23 +27,22 @@ class RemoveNewlineBeforeClassDocstring: @@ -208,7 +210,7 @@ def f(): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( cccccccc.ccccccccccccc(d).cccccccc + e -@@ -56,9 +55,9 @@ +@@ -57,9 +56,9 @@ + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ) @@ -244,7 +246,8 @@ source_type = Python ```python """ -Black's `Preview.module_docstring_newlines`""" +Black's `Preview.module_docstring_newlines` +""" first_stmt_after_module_level_docstring = 1 diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap index ac53706756944e..cea732f045fe3c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap @@ -159,7 +159,8 @@ class ComplexStatements: ```python """Tests for empty line rules in stub files, mostly inspired by typeshed. The rules are a list of nested exceptions. See also -https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744""" +https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744 +""" import sys from typing import Self, TypeAlias, final @@ -312,7 +313,7 @@ class ComplexStatements: ```diff --- Stable +++ Preview -@@ -109,6 +109,7 @@ +@@ -110,6 +110,7 @@ class InnerClass5: def a(self): ... @@ -320,7 +321,7 @@ class ComplexStatements: field1 = 1 class InnerClass6: -@@ -118,6 +119,7 @@ +@@ -119,6 +120,7 @@ class InnerClass7: def a(self): ...