From 317d3dd61218bcc35b20a42403f3c9acc1963be2 Mon Sep 17 00:00:00 2001
From: konsti <konstin@mailbox.org>
Date: Thu, 26 Oct 2023 17:33:26 +0200
Subject: [PATCH] Add test and basic implementation for formatter preview mode
 (#8044)

**Summary** Prepare for the black preview style becoming the black
stable style at the end of the year.

This adds a new test file to compare stable and preview on some relevant
preview options in black, and makes `format_dev` understand the black
preview flag. I've added poetry as a project that uses preview.

I've implemented one specific deviation (collapsing of stub
implementation in non-stub files) which showed up in poetry for testing.
This also improves poetry compatibility from 0.99891 to 0.99919.

Fixes #7440

New compatibility stats:
| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75803 | 1799 | 1647 |
| django | 0.99983 | 2772 | 35 |
| home-assistant | 0.99953 | 10596 | 189 |
| poetry | 0.99919 | 317 | 12 |
| transformers | 0.99963 | 2657 | 332 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99978 | 3669 | 20 |
| warehouse | 0.99969 | 654 | 15 |
| zulip | 0.99970 | 1459 | 22 |
---
 crates/ruff_dev/src/format_dev.rs             |  13 +-
 ...king.options.json => preview.options.json} |   0
 .../assign_breaking.py => preview.py}         |  38 ++-
 crates/ruff_python_formatter/src/cli.rs       |  11 +-
 .../src/statement/clause.rs                   |   4 +-
 .../ruff_python_formatter/tests/fixtures.rs   |  42 ++-
 ...rmat@fmt_on_off__trailing_comments.py.snap |  17 +
 .../tests/snapshots/format@newlines.py.snap   |  23 ++
 ...reaking.py.snap => format@preview.py.snap} | 113 ++++++-
 ...format@statement__class_definition.py.snap |  41 +++
 .../format@statement__function.py.snap        | 163 ++++++++++
 ...ormat@statement__return_annotation.py.snap | 294 ++++++++++++++++++
 .../format@statement__top_level.py.snap       |  53 ++++
 docs/requirements-insiders.txt                |   2 +-
 docs/requirements.txt                         |   2 +-
 scripts/formatter_ecosystem_checks.sh         |   6 +
 16 files changed, 790 insertions(+), 32 deletions(-)
 rename crates/ruff_python_formatter/resources/test/fixtures/ruff/{statement/assign_breaking.options.json => preview.options.json} (100%)
 rename crates/ruff_python_formatter/resources/test/fixtures/ruff/{statement/assign_breaking.py => preview.py} (62%)
 rename crates/ruff_python_formatter/tests/snapshots/{format@statement__assign_breaking.py.snap => format@preview.py.snap} (66%)

diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs
index 191442278fcd7..5589254b05284 100644
--- a/crates/ruff_dev/src/format_dev.rs
+++ b/crates/ruff_dev/src/format_dev.rs
@@ -33,7 +33,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
 use ruff_linter::logging::LogLevel;
 use ruff_linter::settings::types::{FilePattern, FilePatternSet};
 use ruff_python_formatter::{
-    format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
+    format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions,
 };
 use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
 
@@ -871,9 +871,7 @@ struct BlackOptions {
     line_length: NonZeroU16,
     #[serde(alias = "skip-magic-trailing-comma")]
     skip_magic_trailing_comma: bool,
-    #[allow(unused)]
-    #[serde(alias = "force-exclude")]
-    force_exclude: Option<String>,
+    preview: bool,
 }
 
 impl Default for BlackOptions {
@@ -881,7 +879,7 @@ impl Default for BlackOptions {
         Self {
             line_length: NonZeroU16::new(88).unwrap(),
             skip_magic_trailing_comma: false,
-            force_exclude: None,
+            preview: false,
         }
     }
 }
@@ -929,6 +927,11 @@ impl BlackOptions {
             } else {
                 MagicTrailingComma::Respect
             })
+            .with_preview(if self.preview {
+                PreviewMode::Enabled
+            } else {
+                PreviewMode::Disabled
+            })
     }
 }
 
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.options.json
similarity index 100%
rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.options.json
rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.options.json
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
similarity index 62%
rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
index 6ea46a0efe403..111dc0cf5e63c 100644
--- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
+++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
@@ -1,7 +1,39 @@
-# Below is black stable style
-# In preview style, black always breaks the right side first
+"""
+Black's `Preview.module_docstring_newlines`
+"""
+first_stmt_after_module_level_docstring = 1
 
-if True:
+
+class CachedRepository:
+    # Black's `Preview.dummy_implementations`
+    def get_release_info(self): ...
+
+
+def raw_docstring():
+
+    r"""Black's `Preview.accept_raw_docstrings`
+        a
+            b
+    """
+    pass
+
+
+def reference_docstring_newlines():
+
+    """A regular docstring for comparison
+        a
+            b
+    """
+    pass
+
+
+class RemoveNewlineBeforeClassDocstring:
+
+    """Black's `Preview.no_blank_line_before_class_docstring`"""
+
+
+def f():
+    """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
         bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
     ] = cccccccc.ccccccccccccc.cccccccc
diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs
index 09140af80689e..57c73abb6b536 100644
--- a/crates/ruff_python_formatter/src/cli.rs
+++ b/crates/ruff_python_formatter/src/cli.rs
@@ -12,7 +12,7 @@ use ruff_python_parser::{parse_ok_tokens, AsMode};
 use ruff_text_size::Ranged;
 
 use crate::comments::collect_comments;
-use crate::{format_module_ast, PyFormatOptions};
+use crate::{format_module_ast, PreviewMode, PyFormatOptions};
 
 #[derive(ValueEnum, Clone, Debug)]
 pub enum Emit {
@@ -24,6 +24,7 @@ pub enum Emit {
 
 #[derive(Parser)]
 #[command(author, version, about, long_about = None)]
+#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways
 pub struct Cli {
     /// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported
     pub files: Vec<PathBuf>,
@@ -34,6 +35,8 @@ pub struct Cli {
     #[clap(long)]
     pub check: bool,
     #[clap(long)]
+    pub preview: bool,
+    #[clap(long)]
     pub print_ir: bool,
     #[clap(long)]
     pub print_comments: bool,
@@ -48,7 +51,11 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Re
     let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")
         .context("Syntax error in input")?;
 
-    let options = PyFormatOptions::from_extension(source_path);
+    let options = PyFormatOptions::from_extension(source_path).with_preview(if cli.preview {
+        PreviewMode::Enabled
+    } else {
+        PreviewMode::Disabled
+    });
 
     let source_code = SourceCode::new(source);
     let formatted = format_module_ast(&module, &comment_ranges, source, options)
diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs
index d7725bca77e29..4afe4358ecf49 100644
--- a/crates/ruff_python_formatter/src/statement/clause.rs
+++ b/crates/ruff_python_formatter/src/statement/clause.rs
@@ -391,7 +391,9 @@ pub(crate) fn clause_body<'a>(
 
 impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
     fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
-        if f.options().source_type().is_stub()
+        // In stable, stubs are only collapsed in stub files, in preview this is consistently
+        // applied everywhere
+        if (f.options().source_type().is_stub() || f.options().preview().is_enabled())
             && contains_only_an_ellipsis(self.body, f.context().comments())
             && self.trailing_comments.is_empty()
         {
diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs
index 6314532afea0d..604d79bbaee9f 100644
--- a/crates/ruff_python_formatter/tests/fixtures.rs
+++ b/crates/ruff_python_formatter/tests/fixtures.rs
@@ -1,5 +1,5 @@
 use ruff_formatter::FormatOptions;
-use ruff_python_formatter::{format_module_source, PyFormatOptions};
+use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
 use similar::TextDiff;
 use std::fmt::{Formatter, Write};
 use std::io::BufReader;
@@ -142,16 +142,40 @@ fn format() {
         } else {
             let printed =
                 format_module_source(&content, options.clone()).expect("Formatting to succeed");
-            let formatted_code = printed.as_code();
+            let formatted = printed.as_code();
 
-            ensure_stability_when_formatting_twice(formatted_code, options, input_path);
+            ensure_stability_when_formatting_twice(formatted, options.clone(), input_path);
 
-            writeln!(
-                snapshot,
-                "## Output\n{}",
-                CodeFrame::new("py", &formatted_code)
-            )
-            .unwrap();
+            // We want to capture the differences in the preview style in our fixtures
+            let options_preview = options.with_preview(PreviewMode::Enabled);
+            let printed_preview = format_module_source(&content, options_preview.clone())
+                .expect("Formatting to succeed");
+            let formatted_preview = printed_preview.as_code();
+
+            ensure_stability_when_formatting_twice(
+                formatted_preview,
+                options_preview.clone(),
+                input_path,
+            );
+
+            if formatted == formatted_preview {
+                writeln!(snapshot, "## Output\n{}", CodeFrame::new("py", &formatted)).unwrap();
+            } else {
+                // Having both snapshots makes it hard to see the difference, so we're keeping only
+                // diff.
+                writeln!(
+                    snapshot,
+                    "## Output\n{}\n## Preview changes\n{}",
+                    CodeFrame::new("py", &formatted),
+                    CodeFrame::new(
+                        "diff",
+                        TextDiff::from_lines(formatted, formatted_preview)
+                            .unified_diff()
+                            .header("Stable", "Preview")
+                    )
+                )
+                .unwrap();
+            }
         }
 
         insta::with_settings!({
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap
index e80951bd3ac25..89308b0d8a99d 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__trailing_comments.py.snap
@@ -93,4 +93,21 @@ def test3  ():
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -21,8 +21,7 @@
+ 
+ 
+ # formatted
+-def test2():
+-    ...
++def test2(): ...
+ 
+ 
+ a = 10
+```
+
+
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap
index 68f3674034ec5..eacf5fe7938d4 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@newlines.py.snap
@@ -549,4 +549,27 @@ if True:
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -245,13 +245,11 @@
+ class Path:
+     if sys.version_info >= (3, 11):
+ 
+-        def joinpath(self):
+-            ...
++        def joinpath(self): ...
+ 
+     # The .open method comes from pathlib.pyi and should be kept in sync.
+     @overload
+-    def open(self):
+-        ...
++    def open(self): ...
+ 
+ 
+ def fakehttp():
+```
+
+
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign_breaking.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
similarity index 66%
rename from crates/ruff_python_formatter/tests/snapshots/format@statement__assign_breaking.py.snap
rename to crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
index affcbc0c35642..b803c6f3fd74b 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign_breaking.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
@@ -1,13 +1,45 @@
 ---
 source: crates/ruff_python_formatter/tests/fixtures.rs
-input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
+input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
 ---
 ## Input
 ```py
-# Below is black stable style
-# In preview style, black always breaks the right side first
+"""
+Black's `Preview.module_docstring_newlines`
+"""
+first_stmt_after_module_level_docstring = 1
 
-if True:
+
+class CachedRepository:
+    # Black's `Preview.dummy_implementations`
+    def get_release_info(self): ...
+
+
+def raw_docstring():
+
+    r"""Black's `Preview.accept_raw_docstrings`
+        a
+            b
+    """
+    pass
+
+
+def reference_docstring_newlines():
+
+    """A regular docstring for comparison
+        a
+            b
+    """
+    pass
+
+
+class RemoveNewlineBeforeClassDocstring:
+
+    """Black's `Preview.no_blank_line_before_class_docstring`"""
+
+
+def f():
+    """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
         bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
     ] = cccccccc.ccccccccccccc.cccccccc
@@ -52,10 +84,41 @@ preview                 = Disabled
 ```
 
 ```py
-# Below is black stable style
-# In preview style, black always breaks the right side first
+"""
+Black's `Preview.module_docstring_newlines`
+"""
+first_stmt_after_module_level_docstring = 1
+
+
+class CachedRepository:
+    # Black's `Preview.dummy_implementations`
+    def get_release_info(self):
+        ...
+
+
+def raw_docstring():
+    r"""Black's `Preview.accept_raw_docstrings`
+    a
+        b
+    """
+    pass
 
-if True:
+
+def reference_docstring_newlines():
+    """A regular docstring for comparison
+    a
+        b
+    """
+    pass
+
+
+class RemoveNewlineBeforeClassDocstring:
+
+    """Black's `Preview.no_blank_line_before_class_docstring`"""
+
+
+def f():
+    """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
         bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
     ] = cccccccc.ccccccccccccc.cccccccc
@@ -100,10 +163,40 @@ preview                 = Enabled
 ```
 
 ```py
-# Below is black stable style
-# In preview style, black always breaks the right side first
+"""
+Black's `Preview.module_docstring_newlines`
+"""
+first_stmt_after_module_level_docstring = 1
+
+
+class CachedRepository:
+    # Black's `Preview.dummy_implementations`
+    def get_release_info(self): ...
+
+
+def raw_docstring():
+    r"""Black's `Preview.accept_raw_docstrings`
+    a
+        b
+    """
+    pass
+
+
+def reference_docstring_newlines():
+    """A regular docstring for comparison
+    a
+        b
+    """
+    pass
+
+
+class RemoveNewlineBeforeClassDocstring:
+
+    """Black's `Preview.no_blank_line_before_class_docstring`"""
+
 
-if True:
+def f():
+    """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
         bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
     ] = cccccccc.ccccccccccccc.cccccccc
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap
index 752a76c22eb31..d555951aa2b01 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap
@@ -499,4 +499,45 @@ class QuerySet(AltersData):
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -28,8 +28,7 @@
+     pass
+ 
+ 
+-class Test((Aaaa)):
+-    ...
++class Test((Aaaa)): ...
+ 
+ 
+ class Test(
+@@ -159,20 +158,17 @@
+ 
+ @dataclass
+ # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
+-class AltCLIPOutput(ModelOutput):
+-    ...
++class AltCLIPOutput(ModelOutput): ...
+ 
+ 
+ @dataclass
+-class AltCLIPOutput:  # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
+-    ...
++class AltCLIPOutput: ...  # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
+ 
+ 
+ @dataclass
+ class AltCLIPOutput(
+     # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
+-):
+-    ...
++): ...
+ 
+ 
+ class TestTypeParams[
+```
+
+
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap
index 391948f2ed453..a5ef8fe28fab8 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap
@@ -996,4 +996,167 @@ def default_arg_comments2(  #
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -2,8 +2,7 @@
+ def test(
+     # comment
+     # another
+-):
+-    ...
++): ...
+ 
+ 
+ # Argument empty line spacing
+@@ -12,8 +11,7 @@
+     a,
+     # another
+     b,
+-):
+-    ...
++): ...
+ 
+ 
+ ### Different function argument wrappings
+@@ -57,8 +55,7 @@
+     b,
+     # comment
+     *args,
+-):
+-    ...
++): ...
+ 
+ 
+ def kwarg_with_leading_comments(
+@@ -66,8 +63,7 @@
+     b,
+     # comment
+     **kwargs,
+-):
+-    ...
++): ...
+ 
+ 
+ def argument_with_long_default(
+@@ -75,8 +71,7 @@
+     b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
+     + [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff],
+     h=[],
+-):
+-    ...
++): ...
+ 
+ 
+ def argument_with_long_type_annotation(
+@@ -85,12 +80,10 @@
+     | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+     | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3],
+     h=[],
+-):
+-    ...
++): ...
+ 
+ 
+-def test():
+-    ...
++def test(): ...
+ 
+ 
+ # Type parameter empty line spacing
+@@ -99,8 +92,7 @@
+     A,
+     # another
+     B,
+-]():
+-    ...
++](): ...
+ 
+ 
+ # Type parameter comments
+@@ -159,8 +151,7 @@
+ 
+ 
+ # Comment
+-def with_leading_comment():
+-    ...
++def with_leading_comment(): ...
+ 
+ 
+ # Comment that could be mistaken for a trailing comment of the function declaration when
+@@ -192,8 +183,7 @@
+ # Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989
+ def foo(
+     b=3 + 2,  # comment
+-):
+-    ...
++): ...
+ 
+ 
+ # Comments on the slash or the star, both of which don't have a node
+@@ -454,8 +444,7 @@
+ def f(
+     # first
+     # second
+-):
+-    ...
++): ...
+ 
+ 
+ def f(  # first
+@@ -475,8 +464,7 @@
+     # first
+     b,
+     # second
+-):
+-    ...
++): ...
+ 
+ 
+ def f(  # first
+@@ -484,8 +472,7 @@
+     # second
+     b,
+     # third
+-):
+-    ...
++): ...
+ 
+ 
+ def f(  # first
+@@ -494,8 +481,7 @@
+     # third
+     b,
+     # fourth
+-):
+-    ...
++): ...
+ 
+ 
+ def f(  # first
+@@ -522,17 +508,14 @@
+     a,
+     # third
+     /,  # second
+-):
+-    ...
++): ...
+ 
+ 
+ # Walrus operator in return type.
+-def this_is_unusual() -> (please := no):
+-    ...
++def this_is_unusual() -> (please := no): ...
+ 
+ 
+-def this_is_unusual(x) -> (please := no):
+-    ...
++def this_is_unusual(x) -> (please := no): ...
+ 
+ 
+ # Regression test for: https://github.com/astral-sh/ruff/issues/7465
+```
+
+
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__return_annotation.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__return_annotation.py.snap
index 2f8f6b710a718..1f331852019ec 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@statement__return_annotation.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__return_annotation.py.snap
@@ -544,4 +544,298 @@ def process_board_action(
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -7,8 +7,7 @@
+     start: int | None = None,
+     num: int | None = None,
+ ) -> (  # type: ignore[override]
+-):
+-    ...
++): ...
+ 
+ 
+ def zrevrangebylex(
+@@ -20,8 +19,7 @@
+     num: int | None = None,
+ ) -> (  # type: ignore[override]
+     # comment
+-):
+-    ...
++): ...
+ 
+ 
+ def zrevrangebylex(
+@@ -33,8 +31,7 @@
+     num: int | None = None,
+ ) -> (  # type: ignore[override]
+     1
+-):
+-    ...
++): ...
+ 
+ 
+ def zrevrangebylex(
+@@ -47,8 +44,7 @@
+ ) -> (  # type: ignore[override]
+     1,
+     2,
+-):
+-    ...
++): ...
+ 
+ 
+ def zrevrangebylex(
+@@ -60,14 +56,12 @@
+     num: int | None = None,
+ ) -> (  # type: ignore[override]
+     (1, 2)
+-):
+-    ...
++): ...
+ 
+ 
+ def handleMatch(  # type: ignore[override] # https://github.com/python/mypy/issues/10197
+     self, m: Match[str], data: str
+-) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
+-    ...
++) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: ...
+ 
+ 
+ def double(
+@@ -95,50 +89,44 @@
+ # function arguments break here with a single argument; we do not.)
+ def f(
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+-    ...
++) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
+ 
+ 
+ def f(
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a
+-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+-    ...
++) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
+ 
+ 
+ def f(
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-) -> a:
+-    ...
++) -> a: ...
+ 
+ 
+ def f(
+     a
+-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+-    ...
++) -> (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++): ...
+ 
+ 
+ def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> (
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    ...
++): ...
+ 
+ 
+ def f[
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-]() -> a:
+-    ...
++]() -> a: ...
+ 
+ 
+ def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+-    ...
++) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
+ 
+ 
+ def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
+     aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-) -> a:
+-    ...
++) -> a: ...
+ 
+ 
+ # Breaking return type annotations. Black adds parentheses if the parameters are
+@@ -147,137 +135,126 @@
+     Set[
+         "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+     ]
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     Set[
+         "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+     ]
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     Set[
+         "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+     ]
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     Set[
+         "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+     ]
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+ ) -> Set[
+     "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+-]:
+-    ...
++]: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+ ) -> Set[
+     "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+-]:
+-    ...
++]: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     *args
+ ) -> Set[
+     "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+-]:
+-    ...
++]: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(  # foo
+ ) -> Set[
+     "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+-]:
+-    ...
++]: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     # bar
+ ) -> Set[
+     "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+-]:
+-    ...
++]: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
+-    ...
++) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
+-    ...
++) -> (
++    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
++): ...
+ 
+ 
+-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X + Y + foooooooooooooooooooooooooooooooooooo():
+-    ...
++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
++    X + Y + foooooooooooooooooooooooooooooooooooo()
++): ...
+ 
+ 
+-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X + Y + foooooooooooooooooooooooooooooooooooo():
+-    ...
++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
++    x
++) -> X + Y + foooooooooooooooooooooooooooooooooooo(): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     X and Y and foooooooooooooooooooooooooooooooooooo()
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+-) -> X and Y and foooooooooooooooooooooooooooooooooooo():
+-    ...
++) -> X and Y and foooooooooooooooooooooooooooooooooooo(): ...
+ 
+ 
+-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X | Y | foooooooooooooooooooooooooooooooooooo():
+-    ...
++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
++    X | Y | foooooooooooooooooooooooooooooooooooo()
++): ...
+ 
+ 
+-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X | Y | foooooooooooooooooooooooooooooooooooo():
+-    ...
++def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
++    x
++) -> X | Y | foooooooooooooooooooooooooooooooooooo(): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+     X | Y | foooooooooooooooooooooooooooooooooooo()  # comment
+-):
+-    ...
++): ...
+ 
+ 
+ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+     x
+ ) -> (
+     X | Y | foooooooooooooooooooooooooooooooooooo()  # comment
+-):
+-    ...
++): ...
+ 
+ 
+ def double() -> (
+```
+
+
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.py.snap
index 735d40d806f28..7549ab861ed48 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.py.snap
@@ -115,4 +115,57 @@ def quuz():
 ```
 
 
+## Preview changes
+```diff
+--- Stable
++++ Preview
+@@ -12,25 +12,20 @@
+     pass
+ 
+ 
+-class Del(expr_context):
+-    ...
++class Del(expr_context): ...
+ 
+ 
+-class Load(expr_context):
+-    ...
++class Load(expr_context): ...
+ 
+ 
+ # Some comment.
+-class Other(expr_context):
+-    ...
++class Other(expr_context): ...
+ 
+ 
+-class Store(expr_context):
+-    ...
++class Store(expr_context): ...
+ 
+ 
+-class Foo(Bar):
+-    ...
++class Foo(Bar): ...
+ 
+ 
+ class Baz(Qux):
+@@ -49,12 +44,10 @@
+         pass
+ 
+ 
+-def bar():
+-    ...
++def bar(): ...
+ 
+ 
+-def baz():
+-    ...
++def baz(): ...
+ 
+ 
+ def quux():
+```
+
+
 
diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt
index f9a72033886d4..4b0124b6c1a7e 100644
--- a/docs/requirements-insiders.txt
+++ b/docs/requirements-insiders.txt
@@ -1,4 +1,4 @@
 PyYAML==6.0
-black==23.3.0
+black==23.10.0
 mkdocs==1.5.0
 git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@38c0b8187325c3bab386b666daf3518ac036f2f4
diff --git a/docs/requirements.txt b/docs/requirements.txt
index b5cb285e069c7..f852e10f92686 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
 PyYAML==6.0
-black==23.3.0
+black==23.10.0
 mkdocs==1.5.0
 mkdocs-material==9.1.18
diff --git a/scripts/formatter_ecosystem_checks.sh b/scripts/formatter_ecosystem_checks.sh
index 7d0fd78876139..ec8d41dcbdf81 100755
--- a/scripts/formatter_ecosystem_checks.sh
+++ b/scripts/formatter_ecosystem_checks.sh
@@ -70,6 +70,12 @@ if [ ! -d "$dir/cpython/.git" ]; then
 fi
 git -C "$dir/cpython" checkout -q b75186f69edcf54615910a5cd707996144163ef7
 
+# poetry itself
+if [ ! -d "$dir/poetry/.git" ]; then
+  git clone --filter=tree:0 https://github.com/python-poetry/poetry "$dir/poetry"
+fi
+git -C "$dir/poetry" checkout -q 611033a7335f3c8e2b74dd58688fb9021cf84a5b
+
 # Uncomment if you want to update the hashes
 #for i in "$dir"/*/; do git -C "$i" switch main && git -C "$i" pull; done
 #for i in "$dir"/*/; do echo "# $(basename "$i") $(git -C "$i" rev-parse HEAD)"; done