mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:09:22 +00:00

## 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`
206 lines
5.2 KiB
Rust
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"),
|
|
}
|
|
}
|
|
}
|