Respect `fmt: skip` for compound statements on single line (#20633)

Closes #11216

Essentially the approach is to implement `Format` for a new struct
`FormatClause` which is just a clause header _and_ its body. We then
have the information we need to see whether there is a skip suppression
comment on the last child in the body and it all fits on one line.
This commit is contained in:
Dylan 2025-11-18 12:02:09 -06:00 committed by GitHub
parent 8dad289062
commit 62343a101a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 989 additions and 322 deletions

View File

@ -0,0 +1,149 @@
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip

View File

@ -7,7 +7,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::preview::is_remove_parens_around_except_types_enabled; use crate::preview::is_remove_parens_around_except_types_enabled;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Copy, Clone, Default)] #[derive(Copy, Clone, Default)]
@ -55,10 +55,8 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::ExceptHandler(item), ClauseHeader::ExceptHandler(item),
dangling_comments,
&format_with(|f: &mut PyFormatter| { &format_with(|f: &mut PyFormatter| {
write!( write!(
f, f,
@ -83,18 +81,14 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
// ``` // ```
Some(Expr::Tuple(tuple)) Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314 if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled( && is_remove_parens_around_except_types_enabled(f.context())
f.context(),
)
&& name.is_none() => && name.is_none() =>
{ {
write!( write!(
f, f,
[ [
space(), space(),
tuple tuple.format().with_options(TupleParentheses::NeverPreserve)
.format()
.with_options(TupleParentheses::NeverPreserve)
] ]
)?; )?;
} }
@ -119,13 +113,10 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
Ok(()) Ok(())
}), }),
), dangling_comments,
clause_body(
body, body,
SuiteKind::other(self.last_suite_in_statement), SuiteKind::other(self.last_suite_in_statement),
dangling_comments )]
),
]
) )
} }
} }

View File

@ -5,7 +5,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::pattern::maybe_parenthesize_pattern; use crate::pattern::maybe_parenthesize_pattern;
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Default)] #[derive(Default)]
@ -46,23 +46,18 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::MatchCase(item), ClauseHeader::MatchCase(item),
dangling_item_comments,
&format_args![ &format_args![
token("case"), token("case"),
space(), space(),
maybe_parenthesize_pattern(pattern, item), maybe_parenthesize_pattern(pattern, item),
format_guard format_guard
], ],
), dangling_item_comments,
clause_body(
body, body,
SuiteKind::other(self.last_suite_in_statement), SuiteKind::other(self.last_suite_in_statement),
dangling_item_comments )]
),
]
) )
} }
} }

View File

@ -5,11 +5,12 @@ use ruff_python_ast::{
StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite,
}; };
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments}; use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis}; use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::verbatim::write_suppressed_clause_header; use crate::verbatim::{verbatim_text, write_suppressed_clause_header};
use crate::{has_skip_comment, prelude::*}; use crate::{has_skip_comment, prelude::*};
/// The header of a compound statement clause. /// The header of a compound statement clause.
@ -36,7 +37,41 @@ pub(crate) enum ClauseHeader<'a> {
OrElse(ElseClause<'a>), OrElse(ElseClause<'a>),
} }
impl ClauseHeader<'_> { impl<'a> ClauseHeader<'a> {
/// Returns the last child in the clause body immediately following this clause header.
///
/// For most clauses, this is the last statement in
/// the primary body. For clauses like `try`, it specifically returns the last child
/// in the `try` body, not the `except`/`else`/`finally` clauses.
///
/// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`]
/// but restricted to the clause.
pub(crate) fn last_child_in_clause(self) -> Option<AnyNodeRef<'a>> {
match self {
ClauseHeader::Class(StmtClassDef { body, .. })
| ClauseHeader::Function(StmtFunctionDef { body, .. })
| ClauseHeader::If(StmtIf { body, .. })
| ClauseHeader::ElifElse(ElifElseClause { body, .. })
| ClauseHeader::Try(StmtTry { body, .. })
| ClauseHeader::MatchCase(MatchCase { body, .. })
| ClauseHeader::For(StmtFor { body, .. })
| ClauseHeader::While(StmtWhile { body, .. })
| ClauseHeader::With(StmtWith { body, .. })
| ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { body, .. })
| ClauseHeader::OrElse(
ElseClause::Try(StmtTry { orelse: body, .. })
| ElseClause::For(StmtFor { orelse: body, .. })
| ElseClause::While(StmtWhile { orelse: body, .. }),
)
| ClauseHeader::TryFinally(StmtTry {
finalbody: body, ..
}) => body.last().map(AnyNodeRef::from),
ClauseHeader::Match(StmtMatch { cases, .. }) => cases
.last()
.and_then(|case| case.body.last().map(AnyNodeRef::from)),
}
}
/// The range from the clause keyword up to and including the final colon. /// The range from the clause keyword up to and including the final colon.
pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> { pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> {
let keyword_range = self.first_keyword_range(source)?; let keyword_range = self.first_keyword_range(source)?;
@ -338,6 +373,28 @@ impl ClauseHeader<'_> {
} }
} }
impl<'a> From<ClauseHeader<'a>> for AnyNodeRef<'a> {
fn from(value: ClauseHeader<'a>) -> Self {
match value {
ClauseHeader::Class(stmt_class_def) => stmt_class_def.into(),
ClauseHeader::Function(stmt_function_def) => stmt_function_def.into(),
ClauseHeader::If(stmt_if) => stmt_if.into(),
ClauseHeader::ElifElse(elif_else_clause) => elif_else_clause.into(),
ClauseHeader::Try(stmt_try) => stmt_try.into(),
ClauseHeader::ExceptHandler(except_handler_except_handler) => {
except_handler_except_handler.into()
}
ClauseHeader::TryFinally(stmt_try) => stmt_try.into(),
ClauseHeader::Match(stmt_match) => stmt_match.into(),
ClauseHeader::MatchCase(match_case) => match_case.into(),
ClauseHeader::For(stmt_for) => stmt_for.into(),
ClauseHeader::While(stmt_while) => stmt_while.into(),
ClauseHeader::With(stmt_with) => stmt_with.into(),
ClauseHeader::OrElse(else_clause) => else_clause.into(),
}
}
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub(crate) enum ElseClause<'a> { pub(crate) enum ElseClause<'a> {
Try(&'a StmtTry), Try(&'a StmtTry),
@ -345,6 +402,16 @@ pub(crate) enum ElseClause<'a> {
While(&'a StmtWhile), While(&'a StmtWhile),
} }
impl<'a> From<ElseClause<'a>> for AnyNodeRef<'a> {
fn from(value: ElseClause<'a>) -> Self {
match value {
ElseClause::Try(stmt_try) => stmt_try.into(),
ElseClause::For(stmt_for) => stmt_for.into(),
ElseClause::While(stmt_while) => stmt_while.into(),
}
}
}
pub(crate) struct FormatClauseHeader<'a, 'ast> { pub(crate) struct FormatClauseHeader<'a, 'ast> {
header: ClauseHeader<'a>, header: ClauseHeader<'a>,
/// How to format the clause header /// How to format the clause header
@ -378,22 +445,6 @@ where
} }
} }
impl<'a> FormatClauseHeader<'a, '_> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> { impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> { fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = self.leading_comments { if let Some((leading_comments, last_node)) = self.leading_comments {
@ -423,13 +474,13 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
} }
} }
pub(crate) struct FormatClauseBody<'a> { struct FormatClauseBody<'a> {
body: &'a Suite, body: &'a Suite,
kind: SuiteKind, kind: SuiteKind,
trailing_comments: &'a [SourceComment], trailing_comments: &'a [SourceComment],
} }
pub(crate) fn clause_body<'a>( fn clause_body<'a>(
body: &'a Suite, body: &'a Suite,
kind: SuiteKind, kind: SuiteKind,
trailing_comments: &'a [SourceComment], trailing_comments: &'a [SourceComment],
@ -465,6 +516,84 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
} }
} }
pub(crate) struct FormatClause<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
header_formatter: Argument<'a, PyFormatContext<'ast>>,
/// Leading comments coming before the branch, together with the previous node, if any. Only relevant
/// for alternate branches.
leading_comments: Option<(&'a [SourceComment], Option<AnyNodeRef<'a>>)>,
/// The trailing comments coming after the colon.
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
}
impl<'a, 'ast> FormatClause<'a, 'ast> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
fn clause_header(&self) -> FormatClauseHeader<'a, 'ast> {
FormatClauseHeader {
header: self.header,
formatter: self.header_formatter,
leading_comments: self.leading_comments,
trailing_colon_comment: self.trailing_colon_comment,
}
}
fn clause_body(&self) -> FormatClauseBody<'a> {
clause_body(self.body, self.kind, self.trailing_colon_comment)
}
}
/// Formats a clause, handling the case where the compound
/// statement lies on a single line with `# fmt: skip` and
/// should be suppressed.
pub(crate) fn clause<'a, 'ast, Content>(
header: ClauseHeader<'a>,
header_formatter: &'a Content,
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
) -> FormatClause<'a, 'ast>
where
Content: Format<PyFormatContext<'ast>>,
{
FormatClause {
header,
header_formatter: Argument::new(header_formatter),
leading_comments: None,
trailing_colon_comment,
body,
kind,
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClause<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
match should_suppress_clause(self, f)? {
SuppressClauseHeader::Yes {
last_child_in_clause,
} => write_suppressed_clause(self, f, last_child_in_clause),
SuppressClauseHeader::No => {
write!(f, [self.clause_header(), self.clause_body()])
}
}
}
}
/// Finds the range of `keyword` starting the search at `start_position`. /// Finds the range of `keyword` starting the search at `start_position`.
/// ///
/// If the start position is at the end of the previous statement, the /// If the start position is at the end of the previous statement, the
@ -587,3 +716,96 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu
} }
} }
} }
fn should_suppress_clause<'a>(
clause: &FormatClause<'a, '_>,
f: &mut Formatter<PyFormatContext<'_>>,
) -> FormatResult<SuppressClauseHeader<'a>> {
let source = f.context().source();
let Some(last_child_in_clause) = clause.header.last_child_in_clause() else {
return Ok(SuppressClauseHeader::No);
};
// Early return if we don't have a skip comment
// to avoid computing header range in the common case
if !has_skip_comment(
f.context().comments().trailing(last_child_in_clause),
source,
) {
return Ok(SuppressClauseHeader::No);
}
let clause_start = clause.header.range(source)?.end();
let clause_range = TextRange::new(clause_start, last_child_in_clause.end());
// Only applies to clauses on a single line
if source.contains_line_break(clause_range) {
return Ok(SuppressClauseHeader::No);
}
Ok(SuppressClauseHeader::Yes {
last_child_in_clause,
})
}
#[cold]
fn write_suppressed_clause(
clause: &FormatClause,
f: &mut Formatter<PyFormatContext<'_>>,
last_child_in_clause: AnyNodeRef,
) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = clause.leading_comments {
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
}
let header = clause.header;
let clause_start = header.first_keyword_range(f.context().source())?.start();
let comments = f.context().comments().clone();
let clause_end = last_child_in_clause.end();
// Write the outer comments and format the node as verbatim
write!(
f,
[
source_position(clause_start),
verbatim_text(TextRange::new(clause_start, clause_end)),
source_position(clause_end),
trailing_comments(comments.trailing(last_child_in_clause)),
hard_line_break()
]
)?;
// We mark comments in the header as formatted as in
// the implementation of [`write_suppressed_clause_header`].
//
// Note that the header may be multi-line and contain
// various comments since we only require that the range
// starting at the _colon_ and ending at the `# fmt: skip`
// fits on one line.
header.visit(&mut |child| {
for comment in comments.leading_trailing(child) {
comment.mark_formatted();
}
comments.mark_verbatim_node_comments_formatted(child);
});
// Similarly we mark the comments in the body as formatted.
// Note that the trailing comments for the last child in the
// body have already been handled above.
for stmt in clause.body {
comments.mark_verbatim_node_comments_formatted(stmt.into());
}
Ok(())
}
enum SuppressClauseHeader<'a> {
No,
Yes {
last_child_in_clause: AnyNodeRef<'a>,
},
}

View File

@ -8,7 +8,7 @@ use crate::comments::format::{
}; };
use crate::comments::{SourceComment, leading_comments, trailing_comments}; use crate::comments::{SourceComment, leading_comments, trailing_comments};
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Default)] #[derive(Default)]
@ -65,9 +65,8 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
decorators: decorator_list, decorators: decorator_list,
leading_definition_comments, leading_definition_comments,
}, },
clause_header( clause(
ClauseHeader::Class(item), ClauseHeader::Class(item),
trailing_definition_comments,
&format_with(|f| { &format_with(|f| {
write!(f, [token("class"), space(), name.format()])?; write!(f, [token("class"), space(), name.format()])?;
@ -132,8 +131,10 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
Ok(()) Ok(())
}), }),
trailing_definition_comments,
body,
SuiteKind::Class,
), ),
clause_body(body, SuiteKind::Class, trailing_definition_comments),
] ]
)?; )?;

View File

@ -6,7 +6,7 @@ use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::maybe_parenthesize_expression; use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Debug)] #[derive(Debug)]
@ -50,10 +50,8 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::For(item), ClauseHeader::For(item),
trailing_condition_comments,
&format_args![ &format_args![
is_async.then_some(format_args![token("async"), space()]), is_async.then_some(format_args![token("async"), space()]),
token("for"), token("for"),
@ -64,13 +62,10 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
space(), space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
], ],
), trailing_condition_comments,
clause_body(
body, body,
SuiteKind::other(orelse.is_empty()), SuiteKind::other(orelse.is_empty()),
trailing_condition_comments ),]
),
]
)?; )?;
if orelse.is_empty() { if orelse.is_empty() {
@ -84,15 +79,14 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::OrElse(ElseClause::For(item)), ClauseHeader::OrElse(ElseClause::For(item)),
trailing,
&token("else"), &token("else"),
trailing,
orelse,
SuiteKind::other(true),
) )
.with_leading_comments(leading, body.last()), .with_leading_comments(leading, body.last())]
clause_body(orelse, SuiteKind::other(true), trailing),
]
)?; )?;
} }

View File

@ -4,7 +4,7 @@ use crate::comments::format::{
use crate::expression::maybe_parenthesize_expression; use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::expression::parentheses::{Parentheses, Parenthesize};
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::stmt_class_def::FormatDecorators;
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
use ruff_formatter::write; use ruff_formatter::write;
@ -60,12 +60,13 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
decorators: decorator_list, decorators: decorator_list,
leading_definition_comments, leading_definition_comments,
}, },
clause_header( clause(
ClauseHeader::Function(item), ClauseHeader::Function(item),
trailing_definition_comments,
&format_with(|f| format_function_header(f, item)), &format_with(|f| format_function_header(f, item)),
trailing_definition_comments,
body,
SuiteKind::Function,
), ),
clause_body(body, SuiteKind::Function, trailing_definition_comments),
] ]
)?; )?;

View File

@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression; use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Default)] #[derive(Default)]
@ -26,22 +26,17 @@ impl FormatNodeRule<StmtIf> for FormatStmtIf {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::If(item), ClauseHeader::If(item),
trailing_colon_comment,
&format_args![ &format_args![
token("if"), token("if"),
space(), space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
], ],
), trailing_colon_comment,
clause_body(
body, body,
SuiteKind::other(elif_else_clauses.is_empty()), SuiteKind::other(elif_else_clauses.is_empty()),
trailing_colon_comment )]
),
]
)?; )?;
let mut last_node = body.last().unwrap().into(); let mut last_node = body.last().unwrap().into();
@ -81,9 +76,8 @@ pub(crate) fn format_elif_else_clause(
write!( write!(
f, f,
[ [
clause_header( clause(
ClauseHeader::ElifElse(item), ClauseHeader::ElifElse(item),
trailing_colon_comment,
&format_with(|f: &mut PyFormatter| { &format_with(|f: &mut PyFormatter| {
f.options() f.options()
.source_map_generation() .source_map_generation()
@ -103,9 +97,11 @@ pub(crate) fn format_elif_else_clause(
token("else").fmt(f) token("else").fmt(f)
} }
}), }),
trailing_colon_comment,
body,
suite_kind,
) )
.with_leading_comments(leading_comments, last_node), .with_leading_comments(leading_comments, last_node),
clause_body(body, suite_kind, trailing_colon_comment),
f.options() f.options()
.source_map_generation() .source_map_generation()
.is_enabled() .is_enabled()

View File

@ -9,7 +9,7 @@ use crate::other::except_handler_except_handler::{
ExceptHandlerKind, FormatExceptHandlerExceptHandler, ExceptHandlerKind, FormatExceptHandlerExceptHandler,
}; };
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
use crate::statement::{FormatRefWithRule, Stmt}; use crate::statement::{FormatRefWithRule, Stmt};
@ -154,15 +154,14 @@ fn format_case<'a>(
write!( write!(
f, f,
[ [clause(
clause_header(header, trailing_case_comments, &token(kind.keyword())) header,
.with_leading_comments(leading_case_comments, previous_node), &token(kind.keyword()),
clause_body( trailing_case_comments,
body, body,
SuiteKind::other(last_suite_in_statement), SuiteKind::other(last_suite_in_statement),
trailing_case_comments )
), .with_leading_comments(leading_case_comments, previous_node),]
]
)?; )?;
(Some(last), rest) (Some(last), rest)
} else { } else {

View File

@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression; use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Default)] #[derive(Default)]
@ -33,22 +33,17 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::While(item), ClauseHeader::While(item),
trailing_condition_comments,
&format_args![ &format_args![
token("while"), token("while"),
space(), space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
] ],
), trailing_condition_comments,
clause_body(
body, body,
SuiteKind::other(orelse.is_empty()), SuiteKind::other(orelse.is_empty()),
trailing_condition_comments )]
),
]
)?; )?;
if !orelse.is_empty() { if !orelse.is_empty() {
@ -60,15 +55,14 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::OrElse(ElseClause::While(item)), ClauseHeader::OrElse(ElseClause::While(item)),
&token("else"),
trailing, trailing,
&token("else") orelse,
SuiteKind::other(true),
) )
.with_leading_comments(leading, body.last()), .with_leading_comments(leading, body.last()),]
clause_body(orelse, SuiteKind::other(true), trailing),
]
)?; )?;
} }

View File

@ -13,7 +13,7 @@ use crate::expression::parentheses::{
use crate::other::commas; use crate::other::commas;
use crate::other::with_item::WithItemLayout; use crate::other::with_item::WithItemLayout;
use crate::prelude::*; use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind; use crate::statement::suite::SuiteKind;
#[derive(Default)] #[derive(Default)]
@ -46,10 +46,8 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
write!( write!(
f, f,
[ [clause(
clause_header(
ClauseHeader::With(with_stmt), ClauseHeader::With(with_stmt),
colon_comments,
&format_with(|f| { &format_with(|f| {
write!( write!(
f, f,
@ -88,9 +86,8 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
WithItemsLayout::ParenthesizeIfExpands => { WithItemsLayout::ParenthesizeIfExpands => {
parenthesize_if_expands(&format_with(|f| { parenthesize_if_expands(&format_with(|f| {
let mut joiner = f.join_comma_separated( let mut joiner =
with_stmt.body.first().unwrap().start(), f.join_comma_separated(with_stmt.body.first().unwrap().start());
);
for item in &with_stmt.items { for item in &with_stmt.items {
joiner.entry_with_line_separator( joiner.entry_with_line_separator(
@ -120,9 +117,8 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
WithItemsLayout::Parenthesized => parenthesized( WithItemsLayout::Parenthesized => parenthesized(
"(", "(",
&format_with(|f: &mut PyFormatter| { &format_with(|f: &mut PyFormatter| {
let mut joiner = f.join_comma_separated( let mut joiner =
with_stmt.body.first().unwrap().start(), f.join_comma_separated(with_stmt.body.first().unwrap().start());
);
for item in &with_stmt.items { for item in &with_stmt.items {
joiner.entry( joiner.entry(
@ -142,10 +138,11 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
.with_dangling_comments(parenthesized_comments) .with_dangling_comments(parenthesized_comments)
.fmt(f), .fmt(f),
} }
}) }),
), colon_comments,
clause_body(&with_stmt.body, SuiteKind::other(true), colon_comments) &with_stmt.body,
] SuiteKind::other(true),
)]
) )
} }
} }

View File

@ -20,24 +20,16 @@ b = [c for c in "A very long string that would normally generate some kind of co
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -1,8 +1,14 @@ @@ -1,8 +1,10 @@
-def foo(): return "mock" # fmt: skip def foo(): return "mock" # fmt: skip
-if True: print("yay") # fmt: skip
-for i in range(10): print(i) # fmt: skip
+def foo():
+ return "mock" # fmt: skip
+ +
+ +
+if True: if True: print("yay") # fmt: skip
+ print("yay") # fmt: skip for i in range(10): print(i) # fmt: skip
+for i in range(10):
+ print(i) # fmt: skip
-j = 1 # fmt: skip -j = 1 # fmt: skip
-while j < 10: j += 1 # fmt: skip
+j = 1 # fmt: skip +j = 1 # fmt: skip
+while j < 10: while j < 10: j += 1 # fmt: skip
+ j += 1 # fmt: skip
-b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip -b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
+b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip +b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
@ -46,18 +38,14 @@ b = [c for c in "A very long string that would normally generate some kind of co
## Ruff Output ## Ruff Output
```python ```python
def foo(): def foo(): return "mock" # fmt: skip
return "mock" # fmt: skip
if True: if True: print("yay") # fmt: skip
print("yay") # fmt: skip for i in range(10): print(i) # fmt: skip
for i in range(10):
print(i) # fmt: skip
j = 1 # fmt: skip j = 1 # fmt: skip
while j < 10: while j < 10: j += 1 # fmt: skip
j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
``` ```

View File

@ -0,0 +1,341 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py
---
## Input
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```
## Output
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else:
print("this")
while foo(): print(i) # fmt: skip
else:
print("this")
# again but only the second skip
for i in range(2):
print(i)
else: print("this") # fmt: skip
while foo():
print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it:
foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```

View File

@ -1,7 +1,6 @@
--- ---
source: crates/ruff_python_formatter/tests/fixtures.rs source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/docstrings.py input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/docstrings.py
snapshot_kind: text
--- ---
## Input ## Input
```python ```python