ruff/crates/ruff_python_formatter/src/options.rs
Charlie Marsh 1811312722
Improve with statement comment handling and expression breaking (#6621)
## Summary

The motivating code here was:

```python
with test as (
    # test
foo):
    pass
```

Which we were formatting as:

```python
with test as
# test
(foo):
    pass
```

`with` statements are oddly difficult. This PR makes a bunch of subtle
modifications and adds a more extensive test suite. For example, we now
only preserve parentheses if there's more than one `WithItem` _or_ a
trailing comma; before, we always preserved.

Our formatting is_not_ the same as Black, but here's a diff of our
formatted code vs. Black's for the `with.py` test suite. The primary
difference is that we tend to break parentheses when they contain
comments rather than move them to the end of the life (this is a
consistent difference that we make across the codebase):

```diff
diff --git a/crates/ruff_python_formatter/foo.py b/crates/ruff_python_formatter/foo.py
index 85e761080..31625c876 100644
--- a/crates/ruff_python_formatter/foo.py
+++ b/crates/ruff_python_formatter/foo.py
@@ -1,6 +1,4 @@
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-), aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
     ...
     # trailing
 
@@ -16,28 +14,33 @@ with (
     # trailing
 
 
-with a, b:  # a  # comma  # c  # colon
+with (
+    a,  # a  # comma
+    b,  # c
+):  # colon
     ...
 
 
 with (
-    a as  # a  # as
-    # own line
-    b,  # b  # comma
+    a as (  # a  # as
+        # own line
+        b
+    ),  # b  # comma
     c,  # c
 ):  # colon
     ...  # body
     # body trailing own
 
-with (
-    a as  # a  # as
+with a as (  # a  # as
     # own line
-    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb  # b
-):
+    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+):  # b
     pass
 
 
-with (a,):  # magic trailing comma
+with (
+    a,
+):  # magic trailing comma
     ...
 
 
@@ -47,6 +50,7 @@ with a:  # should remove brackets
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
     ...
 
+
 with (
     # leading comment
     a
@@ -74,8 +78,7 @@ with (
 with (
     a  # trailing same line comment
     # trailing own line comment
-    as b
-):
+) as b:
     ...
 
 with (
@@ -87,7 +90,9 @@ with (
 with (
     a
     # trailing own line comment
-) as b:  # trailing as same line comment  # trailing b same line comment
+) as (  # trailing as same line comment
+    b
+):  # trailing b same line comment
     ...
 
 with (
@@ -124,18 +129,24 @@ with (  # comment
     ...
 
 with (  # outer comment
-    CtxManager1() as example1,  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ) as example1,
     CtxManager2() as example2,
     CtxManager3() as example3,
 ):
     ...
 
-with CtxManager() as example:  # outer comment
+with (  # outer comment
+    CtxManager()
+) as example:
     ...
 
 with (  # outer comment
     CtxManager()
-) as example, CtxManager2() as example2:  # inner comment
+) as example, (  # inner comment
+    CtxManager2()
+) as example2:
     ...
 
 with (  # outer comment
@@ -145,7 +156,9 @@ with (  # outer comment
     ...
 
 with (  # outer comment
-    (CtxManager1()),  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ),
     CtxManager2(),
 ) as example:
     ...
@@ -179,7 +192,9 @@ with (
 ):
     pass
 
-with a as (b):  # foo
+with a as (  # foo
+    b
+):
     pass
 
 with f(
@@ -209,17 +224,13 @@ with f(
 ) as b, c as d:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d:
     pass
 
 with (
@@ -230,6 +241,8 @@ with (
     pass
 
 with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b,
+    c as d,
+):
     pass
```

Closes https://github.com/astral-sh/ruff/issues/6600.
## Test Plan

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

`cargo test`
2023-08-18 03:30:38 +00:00

206 lines
5.2 KiB
Rust

use ruff_formatter::printer::{LineEnding, PrinterOptions};
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth};
use ruff_python_ast::PySourceType;
use std::path::Path;
use std::str::FromStr;
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct PyFormatOptions {
/// Whether we're in a `.py` file or `.pyi` file, which have different rules.
source_type: PySourceType,
/// Specifies the indent style:
/// * Either a tab
/// * or a specific amount of spaces
#[cfg_attr(feature = "serde", serde(default = "default_indent_style"))]
indent_style: IndentStyle,
/// The preferred line width at which the formatter should wrap lines.
#[cfg_attr(feature = "serde", serde(default = "default_line_width"))]
line_width: LineWidth,
/// The preferred quote style to use (single vs double quotes).
quote_style: QuoteStyle,
/// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)`.
magic_trailing_comma: MagicTrailingComma,
}
fn default_line_width() -> LineWidth {
LineWidth::try_from(88).unwrap()
}
fn default_indent_style() -> IndentStyle {
IndentStyle::Space(4)
}
impl Default for PyFormatOptions {
fn default() -> Self {
Self {
source_type: PySourceType::default(),
indent_style: default_indent_style(),
line_width: default_line_width(),
quote_style: QuoteStyle::default(),
magic_trailing_comma: MagicTrailingComma::default(),
}
}
}
impl PyFormatOptions {
/// Otherwise sets the defaults. Returns none if the extension is unknown
pub fn from_extension(path: &Path) -> Self {
Self::from_source_type(PySourceType::from(path))
}
pub fn from_source_type(source_type: PySourceType) -> Self {
Self {
source_type,
..Self::default()
}
}
pub fn magic_trailing_comma(&self) -> MagicTrailingComma {
self.magic_trailing_comma
}
pub fn quote_style(&self) -> QuoteStyle {
self.quote_style
}
pub fn source_type(&self) -> PySourceType {
self.source_type
}
#[must_use]
pub fn with_quote_style(mut self, style: QuoteStyle) -> Self {
self.quote_style = style;
self
}
#[must_use]
pub fn with_magic_trailing_comma(mut self, trailing_comma: MagicTrailingComma) -> Self {
self.magic_trailing_comma = trailing_comma;
self
}
#[must_use]
pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self {
self.indent_style = indent_style;
self
}
#[must_use]
pub fn with_line_width(mut self, line_width: LineWidth) -> Self {
self.line_width = line_width;
self
}
}
impl FormatOptions for PyFormatOptions {
fn indent_style(&self) -> IndentStyle {
self.indent_style
}
fn line_width(&self) -> LineWidth {
self.line_width
}
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
tab_width: 4,
print_width: self.line_width.into(),
line_ending: LineEnding::LineFeed,
indent_style: self.indent_style,
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
pub enum QuoteStyle {
Single,
#[default]
Double,
}
impl QuoteStyle {
pub const fn as_char(self) -> char {
match self {
QuoteStyle::Single => '\'',
QuoteStyle::Double => '"',
}
}
#[must_use]
pub const fn invert(self) -> QuoteStyle {
match self {
QuoteStyle::Single => QuoteStyle::Double,
QuoteStyle::Double => QuoteStyle::Single,
}
}
}
impl TryFrom<char> for QuoteStyle {
type Error = ();
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
match value {
'\'' => Ok(QuoteStyle::Single),
'"' => Ok(QuoteStyle::Double),
_ => Err(()),
}
}
}
impl FromStr for QuoteStyle {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"\"" | "double" | "Double" => Ok(Self::Double),
"'" | "single" | "Single" => Ok(Self::Single),
// TODO: replace this error with a diagnostic
_ => Err("Value not supported for QuoteStyle"),
}
}
}
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
pub enum MagicTrailingComma {
#[default]
Respect,
Ignore,
}
impl MagicTrailingComma {
pub const fn is_respect(self) -> bool {
matches!(self, Self::Respect)
}
}
impl FromStr for MagicTrailingComma {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"respect" | "Respect" => Ok(Self::Respect),
"ignore" | "Ignore" => Ok(Self::Ignore),
// TODO: replace this error with a diagnostic
_ => Err("Value not supported for MagicTrailingComma"),
}
}
}