mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00
Hug multiline-strings preview style (#9243)
This commit is contained in:
parent
6be73322da
commit
ac02d3aedd
16 changed files with 590 additions and 215 deletions
|
@ -1472,6 +1472,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
|
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
|
||||||
|
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
|
||||||
|
fits.state.line_width > fits.options().line_width.into()
|
||||||
|
&& !args.measure_mode().allows_text_overflow()
|
||||||
|
}
|
||||||
|
|
||||||
let indent = std::mem::take(&mut self.state.pending_indent);
|
let indent = std::mem::take(&mut self.state.pending_indent);
|
||||||
self.state.line_width +=
|
self.state.line_width +=
|
||||||
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
|
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
|
||||||
|
@ -1493,7 +1498,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||||
return Fits::No;
|
return Fits::No;
|
||||||
}
|
}
|
||||||
match args.measure_mode() {
|
match args.measure_mode() {
|
||||||
MeasureMode::FirstLine => return Fits::Yes,
|
MeasureMode::FirstLine => {
|
||||||
|
return if exceeds_width(self, args) {
|
||||||
|
Fits::No
|
||||||
|
} else {
|
||||||
|
Fits::Yes
|
||||||
|
};
|
||||||
|
}
|
||||||
MeasureMode::AllLines
|
MeasureMode::AllLines
|
||||||
| MeasureMode::AllLinesAllowTextOverflow => {
|
| MeasureMode::AllLinesAllowTextOverflow => {
|
||||||
self.state.line_width = 0;
|
self.state.line_width = 0;
|
||||||
|
@ -1511,9 +1522,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state.line_width > self.options().line_width.into()
|
if exceeds_width(self, args) {
|
||||||
&& !args.measure_mode().allows_text_overflow()
|
|
||||||
{
|
|
||||||
return Fits::No;
|
return Fits::No;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
crates/ruff_python_formatter/resources/test/fixtures/ruff/multiline_string_deviations.py
vendored
Normal file
43
crates/ruff_python_formatter/resources/test/fixtures/ruff/multiline_string_deviations.py
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# This file documents the deviations for formatting multiline strings with black.
|
||||||
|
|
||||||
|
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||||
|
# Can get unreadable if the arguments split
|
||||||
|
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||||
|
# line. Let's consider adding this later.
|
||||||
|
# ```python
|
||||||
|
# call(
|
||||||
|
# 3,
|
||||||
|
# "dogsay",
|
||||||
|
# textwrap.dedent(
|
||||||
|
# """dove
|
||||||
|
# coo""" % "cowabunga",
|
||||||
|
# more,
|
||||||
|
# and_more,
|
||||||
|
# "aaaaaaa",
|
||||||
|
# "bbbbbbbbb",
|
||||||
|
# "cccccccc",
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
# ```
|
||||||
|
call(3, "dogsay", textwrap.dedent("""dove
|
||||||
|
coo""" % "cowabunga"))
|
||||||
|
|
||||||
|
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||||
|
path.write_text(textwrap.dedent("""\
|
||||||
|
A triple-quoted string
|
||||||
|
actually leveraging the textwrap.dedent functionality
|
||||||
|
that ends in a trailing newline,
|
||||||
|
representing e.g. file contents.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||||
|
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||||
|
# issues when the lambda has comments.
|
||||||
|
# Let's keep this as a known deviation for now.
|
||||||
|
generated_readme = lambda project_name: """
|
||||||
|
{}
|
||||||
|
|
||||||
|
<Add content here!>
|
||||||
|
""".strip().format(project_name)
|
|
@ -394,12 +394,12 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
operand.leading_binary_comments().map(leading_comments),
|
operand.leading_binary_comments().map(leading_comments),
|
||||||
leading_comments(comments.leading(&string_constant)),
|
leading_comments(comments.leading(string_constant)),
|
||||||
// Call `FormatStringContinuation` directly to avoid formatting
|
// Call `FormatStringContinuation` directly to avoid formatting
|
||||||
// the implicitly concatenated string with the enclosing group
|
// the implicitly concatenated string with the enclosing group
|
||||||
// because the group is added by the binary like formatting.
|
// because the group is added by the binary like formatting.
|
||||||
FormatStringContinuation::new(&string_constant),
|
FormatStringContinuation::new(&string_constant),
|
||||||
trailing_comments(comments.trailing(&string_constant)),
|
trailing_comments(comments.trailing(string_constant)),
|
||||||
operand.trailing_binary_comments().map(trailing_comments),
|
operand.trailing_binary_comments().map(trailing_comments),
|
||||||
line_suffix_boundary(),
|
line_suffix_boundary(),
|
||||||
]
|
]
|
||||||
|
@ -413,12 +413,12 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
leading_comments(comments.leading(&string_constant)),
|
leading_comments(comments.leading(string_constant)),
|
||||||
// Call `FormatStringContinuation` directly to avoid formatting
|
// Call `FormatStringContinuation` directly to avoid formatting
|
||||||
// the implicitly concatenated string with the enclosing group
|
// the implicitly concatenated string with the enclosing group
|
||||||
// because the group is added by the binary like formatting.
|
// because the group is added by the binary like formatting.
|
||||||
FormatStringContinuation::new(&string_constant),
|
FormatStringContinuation::new(&string_constant),
|
||||||
trailing_comments(comments.trailing(&string_constant)),
|
trailing_comments(comments.trailing(string_constant)),
|
||||||
]
|
]
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ use ruff_python_ast::ExprBinOp;
|
||||||
|
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::expression::binary_like::BinaryLike;
|
use crate::expression::binary_like::BinaryLike;
|
||||||
use crate::expression::expr_string_literal::is_multiline_string;
|
|
||||||
use crate::expression::has_parentheses;
|
use crate::expression::has_parentheses;
|
||||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::string::AnyString;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprBinOp;
|
pub struct FormatExprBinOp;
|
||||||
|
@ -35,13 +35,13 @@ impl NeedsParentheses for ExprBinOp {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if parent.is_expr_await() {
|
if parent.is_expr_await() {
|
||||||
OptionalParentheses::Always
|
OptionalParentheses::Always
|
||||||
} else if let Some(literal_expr) = self.left.as_literal_expr() {
|
} else if let Some(string) = AnyString::from_expression(&self.left) {
|
||||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||||
if !literal_expr.is_implicit_concatenated()
|
if !string.is_implicit_concatenated()
|
||||||
&& is_multiline_string(literal_expr.into(), context.source())
|
&& string.is_multiline(context.source())
|
||||||
&& has_parentheses(&self.right, context).is_some()
|
&& has_parentheses(&self.right, context).is_some()
|
||||||
&& !context.comments().has_dangling(self)
|
&& !context.comments().has_dangling(self)
|
||||||
&& !context.comments().has(literal_expr)
|
&& !context.comments().has(string)
|
||||||
&& !context.comments().has(self.right.as_ref())
|
&& !context.comments().has(self.right.as_ref())
|
||||||
{
|
{
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
|
|
|
@ -2,7 +2,6 @@ use ruff_python_ast::AnyNodeRef;
|
||||||
use ruff_python_ast::ExprBytesLiteral;
|
use ruff_python_ast::ExprBytesLiteral;
|
||||||
|
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::expression::expr_string_literal::is_multiline_string;
|
|
||||||
use crate::expression::parentheses::{
|
use crate::expression::parentheses::{
|
||||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +40,7 @@ impl NeedsParentheses for ExprBytesLiteral {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
} else if is_multiline_string(self.into(), context.source()) {
|
} else if AnyString::Bytes(self).is_multiline(context.source()) {
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
|
|
|
@ -4,10 +4,10 @@ use ruff_python_ast::{CmpOp, ExprCompare};
|
||||||
|
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::expression::binary_like::BinaryLike;
|
use crate::expression::binary_like::BinaryLike;
|
||||||
use crate::expression::expr_string_literal::is_multiline_string;
|
|
||||||
use crate::expression::has_parentheses;
|
use crate::expression::has_parentheses;
|
||||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::string::AnyString;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprCompare;
|
pub struct FormatExprCompare;
|
||||||
|
@ -37,11 +37,11 @@ impl NeedsParentheses for ExprCompare {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if parent.is_expr_await() {
|
if parent.is_expr_await() {
|
||||||
OptionalParentheses::Always
|
OptionalParentheses::Always
|
||||||
} else if let Some(literal_expr) = self.left.as_literal_expr() {
|
} else if let Some(string) = AnyString::from_expression(&self.left) {
|
||||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||||
if !literal_expr.is_implicit_concatenated()
|
if !string.is_implicit_concatenated()
|
||||||
&& is_multiline_string(literal_expr.into(), context.source())
|
&& string.is_multiline(context.source())
|
||||||
&& !context.comments().has(literal_expr)
|
&& !context.comments().has(string)
|
||||||
&& self.comparators.first().is_some_and(|right| {
|
&& self.comparators.first().is_some_and(|right| {
|
||||||
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use memchr::memchr2;
|
|
||||||
|
|
||||||
use ruff_python_ast::{AnyNodeRef, ExprFString};
|
use ruff_python_ast::{AnyNodeRef, ExprFString};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
@ -50,10 +48,10 @@ impl NeedsParentheses for ExprFString {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
} else if memchr2(b'\n', b'\r', context.source()[self.range].as_bytes()).is_none() {
|
} else if AnyString::FString(self).is_multiline(context.source()) {
|
||||||
OptionalParentheses::BestFit
|
|
||||||
} else {
|
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
|
} else {
|
||||||
|
OptionalParentheses::BestFit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use ruff_formatter::FormatRuleWithOptions;
|
use ruff_formatter::FormatRuleWithOptions;
|
||||||
use ruff_python_ast::{AnyNodeRef, ExprStringLiteral};
|
use ruff_python_ast::{AnyNodeRef, ExprStringLiteral};
|
||||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
|
||||||
|
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::expression::parentheses::{
|
use crate::expression::parentheses::{
|
||||||
|
@ -8,7 +7,7 @@ use crate::expression::parentheses::{
|
||||||
};
|
};
|
||||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::{AnyString, FormatStringContinuation, StringPrefix, StringQuotes};
|
use crate::string::{AnyString, FormatStringContinuation};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprStringLiteral {
|
pub struct FormatExprStringLiteral {
|
||||||
|
@ -80,24 +79,10 @@ impl NeedsParentheses for ExprStringLiteral {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
} else if is_multiline_string(self.into(), context.source()) {
|
} else if AnyString::String(self).is_multiline(context.source()) {
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_multiline_string(expr: AnyNodeRef, source: &str) -> bool {
|
|
||||||
if expr.is_expr_string_literal() || expr.is_expr_bytes_literal() {
|
|
||||||
let contents = &source[expr.range()];
|
|
||||||
let prefix = StringPrefix::parse(contents);
|
|
||||||
let quotes =
|
|
||||||
StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]);
|
|
||||||
|
|
||||||
quotes.is_some_and(StringQuotes::is_triple)
|
|
||||||
&& memchr::memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,11 +17,14 @@ use crate::context::{NodeLevel, WithNodeLevel};
|
||||||
use crate::expression::expr_generator_exp::is_generator_parenthesized;
|
use crate::expression::expr_generator_exp::is_generator_parenthesized;
|
||||||
use crate::expression::expr_tuple::is_tuple_parenthesized;
|
use crate::expression::expr_tuple::is_tuple_parenthesized;
|
||||||
use crate::expression::parentheses::{
|
use crate::expression::parentheses::{
|
||||||
is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses,
|
is_expression_parenthesized, optional_parentheses, parenthesized, HuggingStyle,
|
||||||
OptionalParentheses, Parentheses, Parenthesize,
|
NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
|
use crate::preview::{
|
||||||
|
is_hug_parens_with_braces_and_square_brackets_enabled, is_multiline_string_handling_enabled,
|
||||||
|
};
|
||||||
|
use crate::string::AnyString;
|
||||||
|
|
||||||
mod binary_like;
|
mod binary_like;
|
||||||
pub(crate) mod expr_attribute;
|
pub(crate) mod expr_attribute;
|
||||||
|
@ -126,7 +129,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
|
||||||
let node_comments = comments.leading_dangling_trailing(expression);
|
let node_comments = comments.leading_dangling_trailing(expression);
|
||||||
if !node_comments.has_leading() && !node_comments.has_trailing() {
|
if !node_comments.has_leading() && !node_comments.has_trailing() {
|
||||||
parenthesized("(", &format_expr, ")")
|
parenthesized("(", &format_expr, ")")
|
||||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
.with_hugging(is_expression_huggable(expression, f.context()))
|
||||||
.fmt(f)
|
.fmt(f)
|
||||||
} else {
|
} else {
|
||||||
format_with_parentheses_comments(expression, &node_comments, f)
|
format_with_parentheses_comments(expression, &node_comments, f)
|
||||||
|
@ -444,7 +447,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||||
OptionalParentheses::Never => match parenthesize {
|
OptionalParentheses::Never => match parenthesize {
|
||||||
Parenthesize::IfBreaksOrIfRequired => {
|
Parenthesize::IfBreaksOrIfRequired => {
|
||||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
||||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
.with_indent(is_expression_huggable(expression, f.context()).is_none())
|
||||||
.fmt(f)
|
.fmt(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1084,7 +1087,7 @@ pub(crate) fn has_own_parentheses(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the expression can hug directly to enclosing parentheses, as in Black's
|
/// Returns `true` if the expression can hug directly to enclosing parentheses, as in Black's
|
||||||
/// `hug_parens_with_braces_and_square_brackets` preview style behavior.
|
/// `hug_parens_with_braces_and_square_brackets` or `multiline_string_handling` preview styles behavior.
|
||||||
///
|
///
|
||||||
/// For example, in preview style, given:
|
/// For example, in preview style, given:
|
||||||
/// ```python
|
/// ```python
|
||||||
|
@ -1110,11 +1113,10 @@ pub(crate) fn has_own_parentheses(
|
||||||
/// ]
|
/// ]
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> bool {
|
pub(crate) fn is_expression_huggable(
|
||||||
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
|
expr: &Expr,
|
||||||
return false;
|
context: &PyFormatContext,
|
||||||
}
|
) -> Option<HuggingStyle> {
|
||||||
|
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Tuple(_)
|
Expr::Tuple(_)
|
||||||
| Expr::List(_)
|
| Expr::List(_)
|
||||||
|
@ -1122,18 +1124,14 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) ->
|
||||||
| Expr::Dict(_)
|
| Expr::Dict(_)
|
||||||
| Expr::ListComp(_)
|
| Expr::ListComp(_)
|
||||||
| Expr::SetComp(_)
|
| Expr::SetComp(_)
|
||||||
| Expr::DictComp(_) => true,
|
| Expr::DictComp(_) => is_hug_parens_with_braces_and_square_brackets_enabled(context)
|
||||||
|
.then_some(HuggingStyle::Always),
|
||||||
|
|
||||||
Expr::Starred(ast::ExprStarred { value, .. }) => matches!(
|
Expr::Starred(ast::ExprStarred { value, .. }) => is_expression_huggable(value, context),
|
||||||
value.as_ref(),
|
|
||||||
Expr::Tuple(_)
|
Expr::StringLiteral(string) => is_huggable_string(AnyString::String(string), context),
|
||||||
| Expr::List(_)
|
Expr::BytesLiteral(bytes) => is_huggable_string(AnyString::Bytes(bytes), context),
|
||||||
| Expr::Set(_)
|
Expr::FString(fstring) => is_huggable_string(AnyString::FString(fstring), context),
|
||||||
| Expr::Dict(_)
|
|
||||||
| Expr::ListComp(_)
|
|
||||||
| Expr::SetComp(_)
|
|
||||||
| Expr::DictComp(_)
|
|
||||||
),
|
|
||||||
|
|
||||||
Expr::BoolOp(_)
|
Expr::BoolOp(_)
|
||||||
| Expr::NamedExpr(_)
|
| Expr::NamedExpr(_)
|
||||||
|
@ -1147,18 +1145,28 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) ->
|
||||||
| Expr::YieldFrom(_)
|
| Expr::YieldFrom(_)
|
||||||
| Expr::Compare(_)
|
| Expr::Compare(_)
|
||||||
| Expr::Call(_)
|
| Expr::Call(_)
|
||||||
| Expr::FString(_)
|
|
||||||
| Expr::Attribute(_)
|
| Expr::Attribute(_)
|
||||||
| Expr::Subscript(_)
|
| Expr::Subscript(_)
|
||||||
| Expr::Name(_)
|
| Expr::Name(_)
|
||||||
| Expr::Slice(_)
|
| Expr::Slice(_)
|
||||||
| Expr::IpyEscapeCommand(_)
|
| Expr::IpyEscapeCommand(_)
|
||||||
| Expr::StringLiteral(_)
|
|
||||||
| Expr::BytesLiteral(_)
|
|
||||||
| Expr::NumberLiteral(_)
|
| Expr::NumberLiteral(_)
|
||||||
| Expr::BooleanLiteral(_)
|
| Expr::BooleanLiteral(_)
|
||||||
| Expr::NoneLiteral(_)
|
| Expr::NoneLiteral(_)
|
||||||
| Expr::EllipsisLiteral(_) => false,
|
| Expr::EllipsisLiteral(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `string` is a multiline string that is not implicitly concatenated.
|
||||||
|
fn is_huggable_string(string: AnyString, context: &PyFormatContext) -> Option<HuggingStyle> {
|
||||||
|
if !is_multiline_string_handling_enabled(context) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !string.is_implicit_concatenated() && string.is_multiline(context.source()) {
|
||||||
|
Some(HuggingStyle::IfFirstLineFits)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ where
|
||||||
FormatParenthesized {
|
FormatParenthesized {
|
||||||
left,
|
left,
|
||||||
comments: &[],
|
comments: &[],
|
||||||
indent: true,
|
hug: None,
|
||||||
content: Argument::new(content),
|
content: Argument::new(content),
|
||||||
right,
|
right,
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ where
|
||||||
pub(crate) struct FormatParenthesized<'content, 'ast> {
|
pub(crate) struct FormatParenthesized<'content, 'ast> {
|
||||||
left: &'static str,
|
left: &'static str,
|
||||||
comments: &'content [SourceComment],
|
comments: &'content [SourceComment],
|
||||||
indent: bool,
|
hug: Option<HuggingStyle>,
|
||||||
content: Argument<'content, PyFormatContext<'ast>>,
|
content: Argument<'content, PyFormatContext<'ast>>,
|
||||||
right: &'static str,
|
right: &'static str,
|
||||||
}
|
}
|
||||||
|
@ -158,8 +158,11 @@ impl<'content, 'ast> FormatParenthesized<'content, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to indent the content within the parentheses.
|
/// Whether to indent the content within the parentheses.
|
||||||
pub(crate) fn with_indent(self, indent: bool) -> FormatParenthesized<'content, 'ast> {
|
pub(crate) fn with_hugging(
|
||||||
FormatParenthesized { indent, ..self }
|
self,
|
||||||
|
hug: Option<HuggingStyle>,
|
||||||
|
) -> FormatParenthesized<'content, 'ast> {
|
||||||
|
FormatParenthesized { hug, ..self }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,17 +170,41 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
|
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
|
||||||
let current_level = f.context().node_level();
|
let current_level = f.context().node_level();
|
||||||
|
|
||||||
let content = format_with(|f| {
|
let indented = format_with(|f| {
|
||||||
group(&format_with(|f| {
|
let content = Arguments::from(&self.content);
|
||||||
dangling_open_parenthesis_comments(self.comments).fmt(f)?;
|
if self.comments.is_empty() {
|
||||||
if self.indent || !self.comments.is_empty() {
|
match self.hug {
|
||||||
soft_block_indent(&Arguments::from(&self.content)).fmt(f)?;
|
None => group(&soft_block_indent(&content)).fmt(f),
|
||||||
} else {
|
Some(HuggingStyle::Always) => content.fmt(f),
|
||||||
Arguments::from(&self.content).fmt(f)?;
|
Some(HuggingStyle::IfFirstLineFits) => {
|
||||||
|
// It's not immediately obvious how the below IR works to only indent the content if the first line exceeds the configured line width.
|
||||||
|
// The trick is the first group that doesn't wrap `self.content`.
|
||||||
|
// * The group doesn't wrap `self.content` because we need to assume that `self.content`
|
||||||
|
// contains a hard line break and hard-line-breaks always expand the enclosing group.
|
||||||
|
// * The printer decides that a group fits if its content (in this case a `soft_line_break` that has a width of 0 and is guaranteed to fit)
|
||||||
|
// and the content coming after the group in expanded mode (`self.content`) fits on the line.
|
||||||
|
// The content coming after fits if the content up to the first soft or hard line break (or the end of the document) fits.
|
||||||
|
//
|
||||||
|
// This happens to be right what we want. The first group should add an indent and a soft line break if the content of `self.content`
|
||||||
|
// up to the first line break exceeds the configured line length, but not otherwise.
|
||||||
|
let indented = f.group_id("indented_content");
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
group(&indent(&soft_line_break())).with_group_id(Some(indented)),
|
||||||
|
indent_if_group_breaks(&content, indented),
|
||||||
|
if_group_breaks(&soft_line_break()).with_group_id(Some(indented))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
} else {
|
||||||
}))
|
group(&format_args![
|
||||||
.fmt(f)
|
dangling_open_parenthesis_comments(self.comments),
|
||||||
|
soft_block_indent(&content),
|
||||||
|
])
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let inner = format_with(|f| {
|
let inner = format_with(|f| {
|
||||||
|
@ -186,12 +213,12 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||||
// This ensures that expanding this parenthesized expression does not expand the optional parentheses group.
|
// This ensures that expanding this parenthesized expression does not expand the optional parentheses group.
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[fits_expanded(&content)
|
[fits_expanded(&indented)
|
||||||
.with_condition(Some(Condition::if_group_fits_on_line(group_id)))]
|
.with_condition(Some(Condition::if_group_fits_on_line(group_id)))]
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// It's not necessary to wrap the content if it is not inside of an optional_parentheses group.
|
// It's not necessary to wrap the content if it is not inside of an optional_parentheses group.
|
||||||
content.fmt(f)
|
indented.fmt(f)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -201,6 +228,20 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatParenthesized<'_, 'ast> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum HuggingStyle {
|
||||||
|
/// Always hug the content (never indent).
|
||||||
|
Always,
|
||||||
|
|
||||||
|
/// Hug the content if the content up to the first line break fits into the configured line length. Otherwise indent the content.
|
||||||
|
///
|
||||||
|
/// This is different from [`HuggingStyle::Always`] in that it doesn't indent if the content contains a hard line break, and the content up to that hard line break fits into the configured line length.
|
||||||
|
///
|
||||||
|
/// This style is used for formatting multiline strings that, by definition, always break. The idea is to
|
||||||
|
/// only hug a multiline string if its content up to the first line breaks exceeds the configured line length.
|
||||||
|
IfFirstLineFits,
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps an expression in parentheses only if it still does not fit after expanding all expressions that start or end with
|
/// Wraps an expression in parentheses only if it still does not fit after expanding all expressions that start or end with
|
||||||
/// a parentheses (`()`, `[]`, `{}`).
|
/// a parentheses (`()`, `[]`, `{}`).
|
||||||
pub(crate) fn optional_parentheses<'content, 'ast, Content>(
|
pub(crate) fn optional_parentheses<'content, 'ast, Content>(
|
||||||
|
|
|
@ -6,10 +6,11 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
use crate::comments::SourceComment;
|
use crate::comments::SourceComment;
|
||||||
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
|
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
|
||||||
use crate::expression::is_expression_huggable;
|
use crate::expression::is_expression_huggable;
|
||||||
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
|
use crate::expression::parentheses::{
|
||||||
|
empty_parenthesized, parenthesized, HuggingStyle, Parentheses,
|
||||||
|
};
|
||||||
use crate::other::commas;
|
use crate::other::commas;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatArguments;
|
pub struct FormatArguments;
|
||||||
|
@ -107,7 +108,7 @@ impl FormatNodeRule<Arguments> for FormatArguments {
|
||||||
// )
|
// )
|
||||||
// ```
|
// ```
|
||||||
parenthesized("(", &group(&all_arguments), ")")
|
parenthesized("(", &group(&all_arguments), ")")
|
||||||
.with_indent(!is_argument_huggable(item, f.context()))
|
.with_hugging(is_arguments_huggable(item, f.context()))
|
||||||
.with_dangling_comments(dangling_comments)
|
.with_dangling_comments(dangling_comments)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -177,29 +178,23 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source:
|
||||||
///
|
///
|
||||||
/// Hugging should only be applied to single-argument collections, like lists, or starred versions
|
/// Hugging should only be applied to single-argument collections, like lists, or starred versions
|
||||||
/// of those collections.
|
/// of those collections.
|
||||||
fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
|
fn is_arguments_huggable(item: &Arguments, context: &PyFormatContext) -> Option<HuggingStyle> {
|
||||||
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the lone argument or `**kwargs` keyword.
|
// Find the lone argument or `**kwargs` keyword.
|
||||||
let arg = match (item.args.as_slice(), item.keywords.as_slice()) {
|
let arg = match (item.args.as_slice(), item.keywords.as_slice()) {
|
||||||
([arg], []) => arg,
|
([arg], []) => arg,
|
||||||
([], [keyword]) if keyword.arg.is_none() && !context.comments().has(keyword) => {
|
([], [keyword]) if keyword.arg.is_none() && !context.comments().has(keyword) => {
|
||||||
&keyword.value
|
&keyword.value
|
||||||
}
|
}
|
||||||
_ => return false,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the expression itself isn't huggable, then we can't hug it.
|
// If the expression itself isn't huggable, then we can't hug it.
|
||||||
if !is_expression_huggable(arg, context) {
|
let hugging_style = is_expression_huggable(arg, context)?;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the expression has leading or trailing comments, then we can't hug it.
|
// If the expression has leading or trailing comments, then we can't hug it.
|
||||||
let comments = context.comments().leading_dangling_trailing(arg);
|
let comments = context.comments().leading_dangling_trailing(arg);
|
||||||
if comments.has_leading() || comments.has_trailing() {
|
if comments.has_leading() || comments.has_trailing() {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = context.options();
|
let options = context.options();
|
||||||
|
@ -208,8 +203,8 @@ fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
|
||||||
if options.magic_trailing_comma().is_respect()
|
if options.magic_trailing_comma().is_respect()
|
||||||
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), item.end()), options, context)
|
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), item.end()), options, context)
|
||||||
{
|
{
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
Some(hugging_style)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,3 +62,8 @@ pub(crate) const fn is_dummy_implementations_enabled(context: &PyFormatContext)
|
||||||
pub(crate) const fn is_hex_codes_in_unicode_sequences_enabled(context: &PyFormatContext) -> bool {
|
pub(crate) const fn is_hex_codes_in_unicode_sequences_enabled(context: &PyFormatContext) -> bool {
|
||||||
context.is_preview()
|
context.is_preview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the [`multiline_string_handling`](https://github.com/astral-sh/ruff/issues/8896) preview style is enabled.
|
||||||
|
pub(crate) const fn is_multiline_string_handling_enabled(context: &PyFormatContext) -> bool {
|
||||||
|
context.is_preview()
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
use memchr::memchr2;
|
||||||
|
|
||||||
use ruff_formatter::{format_args, write};
|
use ruff_formatter::{format_args, write};
|
||||||
use ruff_python_ast::AnyNodeRef;
|
use ruff_python_ast::AnyNodeRef;
|
||||||
|
@ -29,7 +30,7 @@ pub(crate) enum Quoting {
|
||||||
|
|
||||||
/// Represents any kind of string expression. This could be either a string,
|
/// Represents any kind of string expression. This could be either a string,
|
||||||
/// bytes or f-string.
|
/// bytes or f-string.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) enum AnyString<'a> {
|
pub(crate) enum AnyString<'a> {
|
||||||
String(&'a ExprStringLiteral),
|
String(&'a ExprStringLiteral),
|
||||||
Bytes(&'a ExprBytesLiteral),
|
Bytes(&'a ExprBytesLiteral),
|
||||||
|
@ -50,7 +51,7 @@ impl<'a> AnyString<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the string is implicitly concatenated.
|
/// Returns `true` if the string is implicitly concatenated.
|
||||||
pub(crate) fn is_implicit_concatenated(&self) -> bool {
|
pub(crate) fn is_implicit_concatenated(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
|
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||||
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
|
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||||
|
@ -59,7 +60,7 @@ impl<'a> AnyString<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the quoting to be used for this string.
|
/// Returns the quoting to be used for this string.
|
||||||
fn quoting(&self, locator: &Locator<'_>) -> Quoting {
|
fn quoting(self, locator: &Locator<'_>) -> Quoting {
|
||||||
match self {
|
match self {
|
||||||
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
||||||
Self::FString(f_string) => f_string_quoting(f_string, locator),
|
Self::FString(f_string) => f_string_quoting(f_string, locator),
|
||||||
|
@ -67,7 +68,7 @@ impl<'a> AnyString<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
||||||
fn parts(&self, quoting: Quoting) -> Vec<AnyStringPart<'a>> {
|
fn parts(self, quoting: Quoting) -> Vec<AnyStringPart<'a>> {
|
||||||
match self {
|
match self {
|
||||||
Self::String(ExprStringLiteral { value, .. }) => value
|
Self::String(ExprStringLiteral { value, .. }) => value
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -94,6 +95,24 @@ impl<'a> AnyString<'a> {
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_multiline(self, source: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
AnyString::String(_) | AnyString::Bytes(_) => {
|
||||||
|
let contents = &source[self.range()];
|
||||||
|
let prefix = StringPrefix::parse(contents);
|
||||||
|
let quotes = StringQuotes::parse(
|
||||||
|
&contents[TextRange::new(prefix.text_len(), contents.text_len())],
|
||||||
|
);
|
||||||
|
|
||||||
|
quotes.is_some_and(StringQuotes::is_triple)
|
||||||
|
&& memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
||||||
|
}
|
||||||
|
AnyString::FString(fstring) => {
|
||||||
|
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ranged for AnyString<'_> {
|
impl Ranged for AnyString<'_> {
|
||||||
|
@ -116,6 +135,12 @@ impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<AnyString<'a>> for AnyNodeRef<'a> {
|
||||||
|
fn from(value: AnyString<'a>) -> Self {
|
||||||
|
AnyNodeRef::from(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
||||||
fn from(value: &AnyString<'a>) -> Self {
|
fn from(value: &AnyString<'a>) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
|
@ -187,7 +187,7 @@ this_will_also_become_one_line = ( # comment
|
||||||
```diff
|
```diff
|
||||||
--- Black
|
--- Black
|
||||||
+++ Ruff
|
+++ Ruff
|
||||||
@@ -1,95 +1,138 @@
|
@@ -1,46 +1,69 @@
|
||||||
-"""cow
|
-"""cow
|
||||||
+(
|
+(
|
||||||
+ """cow
|
+ """cow
|
||||||
|
@ -271,41 +271,21 @@ this_will_also_become_one_line = ( # comment
|
||||||
+ ),
|
+ ),
|
||||||
)
|
)
|
||||||
textwrap.dedent("""A one-line triple-quoted string.""")
|
textwrap.dedent("""A one-line triple-quoted string.""")
|
||||||
-textwrap.dedent("""A two-line triple-quoted string
|
textwrap.dedent("""A two-line triple-quoted string
|
||||||
-since it goes to the next line.""")
|
@@ -54,18 +77,24 @@
|
||||||
-textwrap.dedent("""A three-line triple-quoted string
|
|
||||||
+textwrap.dedent(
|
|
||||||
+ """A two-line triple-quoted string
|
|
||||||
+since it goes to the next line."""
|
|
||||||
+)
|
|
||||||
+textwrap.dedent(
|
|
||||||
+ """A three-line triple-quoted string
|
|
||||||
that not only goes to the next line
|
|
||||||
-but also goes one line beyond.""")
|
|
||||||
-textwrap.dedent("""\
|
|
||||||
+but also goes one line beyond."""
|
|
||||||
+)
|
|
||||||
+textwrap.dedent(
|
|
||||||
+ """\
|
|
||||||
A triple-quoted string
|
|
||||||
actually leveraging the textwrap.dedent functionality
|
|
||||||
that ends in a trailing newline,
|
that ends in a trailing newline,
|
||||||
representing e.g. file contents.
|
representing e.g. file contents.
|
||||||
-""")
|
""")
|
||||||
-path.write_text(textwrap.dedent("""\
|
-path.write_text(textwrap.dedent("""\
|
||||||
+"""
|
|
||||||
+)
|
|
||||||
+path.write_text(
|
+path.write_text(
|
||||||
+ textwrap.dedent(
|
+ textwrap.dedent("""\
|
||||||
+ """\
|
|
||||||
A triple-quoted string
|
A triple-quoted string
|
||||||
actually leveraging the textwrap.dedent functionality
|
actually leveraging the textwrap.dedent functionality
|
||||||
that ends in a trailing newline,
|
that ends in a trailing newline,
|
||||||
representing e.g. file contents.
|
representing e.g. file contents.
|
||||||
-"""))
|
-"""))
|
||||||
-path.write_text(textwrap.dedent("""\
|
-path.write_text(textwrap.dedent("""\
|
||||||
+"""
|
+""")
|
||||||
+ )
|
|
||||||
+)
|
+)
|
||||||
+path.write_text(
|
+path.write_text(
|
||||||
+ textwrap.dedent(
|
+ textwrap.dedent(
|
||||||
|
@ -319,29 +299,9 @@ this_will_also_become_one_line = ( # comment
|
||||||
+ )
|
+ )
|
||||||
+)
|
+)
|
||||||
# Another use case
|
# Another use case
|
||||||
-data = yaml.load("""\
|
data = yaml.load("""\
|
||||||
+data = yaml.load(
|
|
||||||
+ """\
|
|
||||||
a: 1
|
a: 1
|
||||||
b: 2
|
@@ -85,11 +114,13 @@
|
||||||
-""")
|
|
||||||
+"""
|
|
||||||
+)
|
|
||||||
data = yaml.load(
|
|
||||||
"""\
|
|
||||||
a: 1
|
|
||||||
b: 2
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
-data = yaml.load("""\
|
|
||||||
+data = yaml.load(
|
|
||||||
+ """\
|
|
||||||
a: 1
|
|
||||||
b: 2
|
|
||||||
-""")
|
|
||||||
+"""
|
|
||||||
+)
|
|
||||||
|
|
||||||
MULTILINE = """
|
MULTILINE = """
|
||||||
foo
|
foo
|
||||||
""".replace("\n", "")
|
""".replace("\n", "")
|
||||||
|
@ -356,7 +316,7 @@ this_will_also_become_one_line = ( # comment
|
||||||
parser.usage += """
|
parser.usage += """
|
||||||
Custom extra help summary.
|
Custom extra help summary.
|
||||||
|
|
||||||
@@ -156,16 +199,24 @@
|
@@ -156,16 +187,24 @@
|
||||||
10 LOAD_CONST 0 (None)
|
10 LOAD_CONST 0 (None)
|
||||||
12 RETURN_VALUE
|
12 RETURN_VALUE
|
||||||
""" % (_C.__init__.__code__.co_firstlineno + 1,)
|
""" % (_C.__init__.__code__.co_firstlineno + 1,)
|
||||||
|
@ -387,36 +347,7 @@ this_will_also_become_one_line = ( # comment
|
||||||
[
|
[
|
||||||
"""cow
|
"""cow
|
||||||
moos""",
|
moos""",
|
||||||
@@ -177,28 +228,32 @@
|
@@ -198,7 +237,7 @@
|
||||||
|
|
||||||
|
|
||||||
def dastardly_default_value(
|
|
||||||
- cow: String = json.loads("""this
|
|
||||||
+ cow: String = json.loads(
|
|
||||||
+ """this
|
|
||||||
is
|
|
||||||
quite
|
|
||||||
the
|
|
||||||
dastadardly
|
|
||||||
-value!"""),
|
|
||||||
+value!"""
|
|
||||||
+ ),
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
-print(f"""
|
|
||||||
+print(
|
|
||||||
+ f"""
|
|
||||||
This {animal}
|
|
||||||
moos and barks
|
|
||||||
{animal} say
|
|
||||||
-""")
|
|
||||||
+"""
|
|
||||||
+)
|
|
||||||
msg = f"""The arguments {bad_arguments} were passed in.
|
|
||||||
Please use `--build-option` instead,
|
|
||||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -425,7 +356,7 @@ this_will_also_become_one_line = ( # comment
|
||||||
|
|
||||||
this_will_stay_on_three_lines = (
|
this_will_stay_on_three_lines = (
|
||||||
"a" # comment
|
"a" # comment
|
||||||
@@ -206,4 +261,6 @@
|
@@ -206,4 +245,6 @@
|
||||||
"c"
|
"c"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -506,32 +437,24 @@ call(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
textwrap.dedent("""A one-line triple-quoted string.""")
|
textwrap.dedent("""A one-line triple-quoted string.""")
|
||||||
textwrap.dedent(
|
textwrap.dedent("""A two-line triple-quoted string
|
||||||
"""A two-line triple-quoted string
|
since it goes to the next line.""")
|
||||||
since it goes to the next line."""
|
textwrap.dedent("""A three-line triple-quoted string
|
||||||
)
|
|
||||||
textwrap.dedent(
|
|
||||||
"""A three-line triple-quoted string
|
|
||||||
that not only goes to the next line
|
that not only goes to the next line
|
||||||
but also goes one line beyond."""
|
but also goes one line beyond.""")
|
||||||
)
|
textwrap.dedent("""\
|
||||||
textwrap.dedent(
|
|
||||||
"""\
|
|
||||||
A triple-quoted string
|
A triple-quoted string
|
||||||
actually leveraging the textwrap.dedent functionality
|
actually leveraging the textwrap.dedent functionality
|
||||||
that ends in a trailing newline,
|
that ends in a trailing newline,
|
||||||
representing e.g. file contents.
|
representing e.g. file contents.
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
path.write_text(
|
path.write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent("""\
|
||||||
"""\
|
|
||||||
A triple-quoted string
|
A triple-quoted string
|
||||||
actually leveraging the textwrap.dedent functionality
|
actually leveraging the textwrap.dedent functionality
|
||||||
that ends in a trailing newline,
|
that ends in a trailing newline,
|
||||||
representing e.g. file contents.
|
representing e.g. file contents.
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
)
|
)
|
||||||
path.write_text(
|
path.write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
|
@ -544,24 +467,20 @@ path.write_text(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Another use case
|
# Another use case
|
||||||
data = yaml.load(
|
data = yaml.load("""\
|
||||||
"""\
|
|
||||||
a: 1
|
a: 1
|
||||||
b: 2
|
b: 2
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
data = yaml.load(
|
data = yaml.load(
|
||||||
"""\
|
"""\
|
||||||
a: 1
|
a: 1
|
||||||
b: 2
|
b: 2
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
data = yaml.load(
|
data = yaml.load("""\
|
||||||
"""\
|
|
||||||
a: 1
|
a: 1
|
||||||
b: 2
|
b: 2
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
MULTILINE = """
|
MULTILINE = """
|
||||||
foo
|
foo
|
||||||
|
@ -668,26 +587,22 @@ barks""",
|
||||||
|
|
||||||
|
|
||||||
def dastardly_default_value(
|
def dastardly_default_value(
|
||||||
cow: String = json.loads(
|
cow: String = json.loads("""this
|
||||||
"""this
|
|
||||||
is
|
is
|
||||||
quite
|
quite
|
||||||
the
|
the
|
||||||
dastadardly
|
dastadardly
|
||||||
value!"""
|
value!"""),
|
||||||
),
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
print(
|
print(f"""
|
||||||
f"""
|
|
||||||
This {animal}
|
This {animal}
|
||||||
moos and barks
|
moos and barks
|
||||||
{animal} say
|
{animal} say
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
msg = f"""The arguments {bad_arguments} were passed in.
|
msg = f"""The arguments {bad_arguments} were passed in.
|
||||||
Please use `--build-option` instead,
|
Please use `--build-option` instead,
|
||||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||||
|
|
|
@ -8209,6 +8209,42 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -480,10 +480,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -958,13 +956,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 6
|
### Output 6
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -9577,6 +9613,42 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -480,10 +480,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -958,13 +956,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 7
|
### Output 7
|
||||||
```
|
```
|
||||||
indent-style = tab
|
indent-style = tab
|
||||||
|
@ -10954,6 +11026,42 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -489,10 +489,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -967,13 +965,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 8
|
### Output 8
|
||||||
```
|
```
|
||||||
indent-style = tab
|
indent-style = tab
|
||||||
|
@ -12322,6 +12430,42 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -480,10 +480,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -958,13 +956,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 9
|
### Output 9
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -13699,6 +13843,42 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -489,10 +489,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -967,13 +965,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 10
|
### Output 10
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -15067,4 +15247,40 @@ def markdown_skipped_rst_directive():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -480,10 +480,8 @@
|
||||||
|
Do cool stuff::
|
||||||
|
|
||||||
|
if True:
|
||||||
|
- cool_stuff(
|
||||||
|
- '''
|
||||||
|
- hiya'''
|
||||||
|
- )
|
||||||
|
+ cool_stuff('''
|
||||||
|
+ hiya''')
|
||||||
|
|
||||||
|
Done.
|
||||||
|
"""
|
||||||
|
@@ -958,13 +956,11 @@
|
||||||
|
Do cool stuff.
|
||||||
|
|
||||||
|
``````
|
||||||
|
- do_something(
|
||||||
|
- '''
|
||||||
|
+ do_something('''
|
||||||
|
```
|
||||||
|
did i trick you?
|
||||||
|
```
|
||||||
|
- '''
|
||||||
|
- )
|
||||||
|
+ ''')
|
||||||
|
``````
|
||||||
|
|
||||||
|
Done.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/multiline_string_deviations.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```python
|
||||||
|
# This file documents the deviations for formatting multiline strings with black.
|
||||||
|
|
||||||
|
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||||
|
# Can get unreadable if the arguments split
|
||||||
|
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||||
|
# line. Let's consider adding this later.
|
||||||
|
# ```python
|
||||||
|
# call(
|
||||||
|
# 3,
|
||||||
|
# "dogsay",
|
||||||
|
# textwrap.dedent(
|
||||||
|
# """dove
|
||||||
|
# coo""" % "cowabunga",
|
||||||
|
# more,
|
||||||
|
# and_more,
|
||||||
|
# "aaaaaaa",
|
||||||
|
# "bbbbbbbbb",
|
||||||
|
# "cccccccc",
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
# ```
|
||||||
|
call(3, "dogsay", textwrap.dedent("""dove
|
||||||
|
coo""" % "cowabunga"))
|
||||||
|
|
||||||
|
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||||
|
path.write_text(textwrap.dedent("""\
|
||||||
|
A triple-quoted string
|
||||||
|
actually leveraging the textwrap.dedent functionality
|
||||||
|
that ends in a trailing newline,
|
||||||
|
representing e.g. file contents.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||||
|
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||||
|
# issues when the lambda has comments.
|
||||||
|
# Let's keep this as a known deviation for now.
|
||||||
|
generated_readme = lambda project_name: """
|
||||||
|
{}
|
||||||
|
|
||||||
|
<Add content here!>
|
||||||
|
""".strip().format(project_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
```python
|
||||||
|
# This file documents the deviations for formatting multiline strings with black.
|
||||||
|
|
||||||
|
# Black hugs the parentheses for `%` usages -> convert to fstring.
|
||||||
|
# Can get unreadable if the arguments split
|
||||||
|
# This could be solved by using `best_fitting` to try to format the arguments on a single
|
||||||
|
# line. Let's consider adding this later.
|
||||||
|
# ```python
|
||||||
|
# call(
|
||||||
|
# 3,
|
||||||
|
# "dogsay",
|
||||||
|
# textwrap.dedent(
|
||||||
|
# """dove
|
||||||
|
# coo""" % "cowabunga",
|
||||||
|
# more,
|
||||||
|
# and_more,
|
||||||
|
# "aaaaaaa",
|
||||||
|
# "bbbbbbbbb",
|
||||||
|
# "cccccccc",
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
# ```
|
||||||
|
call(
|
||||||
|
3,
|
||||||
|
"dogsay",
|
||||||
|
textwrap.dedent(
|
||||||
|
"""dove
|
||||||
|
coo"""
|
||||||
|
% "cowabunga"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||||
|
path.write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
A triple-quoted string
|
||||||
|
actually leveraging the textwrap.dedent functionality
|
||||||
|
that ends in a trailing newline,
|
||||||
|
representing e.g. file contents.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Black avoids parenthesizing the following lambda. We could potentially support
|
||||||
|
# this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes
|
||||||
|
# issues when the lambda has comments.
|
||||||
|
# Let's keep this as a known deviation for now.
|
||||||
|
generated_readme = (
|
||||||
|
lambda project_name: """
|
||||||
|
{}
|
||||||
|
|
||||||
|
<Add content here!>
|
||||||
|
""".strip().format(project_name)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -31,14 +31,12 @@
|
||||||
|
|
||||||
|
# Black applies the hugging recursively. We don't (consistent with the hugging style).
|
||||||
|
path.write_text(
|
||||||
|
- textwrap.dedent(
|
||||||
|
- """\
|
||||||
|
+ textwrap.dedent("""\
|
||||||
|
A triple-quoted string
|
||||||
|
actually leveraging the textwrap.dedent functionality
|
||||||
|
that ends in a trailing newline,
|
||||||
|
representing e.g. file contents.
|
||||||
|
-"""
|
||||||
|
- )
|
||||||
|
+""")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue