Skip to content

Commit

Permalink
Hug multiline-strings preview style
Browse files Browse the repository at this point in the history
Signed-off-by: Micha Reiser <micha@reiser.io>
  • Loading branch information
MichaReiser committed Dec 27, 2023
1 parent 2951339 commit 494e4d2
Show file tree
Hide file tree
Showing 12 changed files with 471 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion crates/ruff_python_formatter/src/expression/expr_bin_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl NeedsParentheses for ExprBinOp {
} else if let Some(literal_expr) = self.left.as_literal_expr() {
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
if !literal_expr.is_implicit_concatenated()
&& is_multiline_string(literal_expr.into(), context.source())
&& is_multiline_string(literal_expr, context.source())
&& has_parentheses(&self.right, context).is_some()
&& !context.comments().has_dangling(self)
&& !context.comments().has(literal_expr)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprBytesLiteral;
use ruff_python_ast::{AnyNodeRef, LiteralExpressionRef};

use crate::comments::SourceComment;
use crate::expression::expr_string_literal::is_multiline_string;
Expand Down Expand Up @@ -41,7 +41,7 @@ impl NeedsParentheses for ExprBytesLiteral {
) -> OptionalParentheses {
if self.value.is_implicit_concatenated() {
OptionalParentheses::Multiline
} else if is_multiline_string(self.into(), context.source()) {
} else if is_multiline_string(LiteralExpressionRef::BytesLiteral(self), context.source()) {
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl NeedsParentheses for ExprCompare {
} else if let Some(literal_expr) = self.left.as_literal_expr() {
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
if !literal_expr.is_implicit_concatenated()
&& is_multiline_string(literal_expr.into(), context.source())
&& is_multiline_string(literal_expr, context.source())
&& !context.comments().has(literal_expr)
&& self.comparators.first().is_some_and(|right| {
has_parentheses(right, context).is_some() && !context.comments().has(right)
Expand Down
10 changes: 7 additions & 3 deletions crates/ruff_python_formatter/src/expression/expr_f_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ impl NeedsParentheses for ExprFString {
) -> OptionalParentheses {
if self.value.is_implicit_concatenated() {
OptionalParentheses::Multiline
} else if memchr2(b'\n', b'\r', context.source()[self.range].as_bytes()).is_none() {
OptionalParentheses::BestFit
} else {
} else if is_multiline_fstring(self, context.source()) {
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
}
}
}
Expand Down Expand Up @@ -82,3 +82,7 @@ pub(crate) fn f_string_quoting(f_string: &ExprFString, locator: &Locator) -> Quo
Quoting::CanChange
}
}

pub(crate) fn is_multiline_fstring(f_string: &ExprFString, source: &str) -> bool {
memchr2(b'\n', b'\r', source[f_string.range].as_bytes()).is_some()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::{AnyNodeRef, ExprStringLiteral};
use ruff_python_ast::{AnyNodeRef, ExprStringLiteral, LiteralExpressionRef};
use ruff_text_size::{Ranged, TextLen, TextRange};

use crate::comments::SourceComment;
Expand Down Expand Up @@ -80,16 +80,16 @@ impl NeedsParentheses for ExprStringLiteral {
) -> OptionalParentheses {
if self.value.is_implicit_concatenated() {
OptionalParentheses::Multiline
} else if is_multiline_string(self.into(), context.source()) {
} else if is_multiline_string(LiteralExpressionRef::StringLiteral(self), context.source()) {
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
}
}
}

pub(super) fn is_multiline_string(expr: AnyNodeRef, source: &str) -> bool {
if expr.is_expr_string_literal() || expr.is_expr_bytes_literal() {
pub(crate) fn is_multiline_string(expr: LiteralExpressionRef, source: &str) -> bool {
if expr.is_string_literal() || expr.is_bytes_literal() {
let contents = &source[expr.range()];
let prefix = StringPrefix::parse(contents);
let quotes =
Expand Down
51 changes: 29 additions & 22 deletions crates/ruff_python_formatter/src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ use ruff_formatter::{
use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parentheses_iterator;
use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor};
use ruff_python_ast::{AnyNodeRef, Expr, ExpressionRef, Operator};
use ruff_python_ast::{AnyNodeRef, Expr, ExpressionRef, LiteralExpressionRef, Operator};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged;

use crate::builders::parenthesize_if_expands;
use crate::comments::{leading_comments, trailing_comments, LeadingDanglingTrailingComments};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::expr_f_string::is_multiline_fstring;
use crate::expression::expr_generator_exp::is_generator_parenthesized;
use crate::expression::expr_string_literal::is_multiline_string;
use crate::expression::expr_tuple::is_tuple_parenthesized;
use crate::expression::parentheses::{
is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses,
OptionalParentheses, Parentheses, Parenthesize,
};
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,
};

mod binary_like;
pub(crate) mod expr_attribute;
Expand Down Expand Up @@ -1084,7 +1088,7 @@ pub(crate) fn has_own_parentheses(
}

/// 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:
/// ```python
Expand All @@ -1111,29 +1115,35 @@ pub(crate) fn has_own_parentheses(
/// )
/// ```
pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> bool {
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
return false;
}

match expr {
Expr::Tuple(_)
| Expr::List(_)
| Expr::Set(_)
| Expr::Dict(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_) => true,

Expr::Starred(ast::ExprStarred { value, .. }) => matches!(
value.as_ref(),
Expr::Tuple(_)
| Expr::List(_)
| Expr::Set(_)
| Expr::Dict(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
),
| Expr::DictComp(_) => is_hug_parens_with_braces_and_square_brackets_enabled(context),

Expr::Starred(ast::ExprStarred { value, .. }) => is_expression_huggable(value, context),

Expr::StringLiteral(string) => {
is_multiline_string_handling_enabled(context)
&& !string.value.is_implicit_concatenated()
&& is_multiline_string(
LiteralExpressionRef::StringLiteral(string),
context.source(),
)
}
Expr::BytesLiteral(bytes) => {
is_multiline_string_handling_enabled(context)
&& !bytes.value.is_implicit_concatenated()
&& is_multiline_string(LiteralExpressionRef::BytesLiteral(bytes), context.source())
}
Expr::FString(fstring) => {
is_multiline_string_handling_enabled(context)
&& !fstring.value.is_implicit_concatenated()
&& is_multiline_fstring(fstring, context.source())
}

Expr::BoolOp(_)
| Expr::NamedExpr(_)
Expand All @@ -1147,14 +1157,11 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) ->
| Expr::YieldFrom(_)
| Expr::Compare(_)
| Expr::Call(_)
| Expr::FString(_)
| Expr::Attribute(_)
| Expr::Subscript(_)
| Expr::Name(_)
| Expr::Slice(_)
| Expr::IpyEscapeCommand(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_)
| Expr::NoneLiteral(_)
Expand Down
5 changes: 0 additions & 5 deletions crates/ruff_python_formatter/src/other/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use crate::expression::is_expression_huggable;
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
use crate::other::commas;
use crate::prelude::*;
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;

#[derive(Default)]
pub struct FormatArguments;
Expand Down Expand Up @@ -178,10 +177,6 @@ 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
/// of those collections.
fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
return false;
}

// Find the lone argument or `**kwargs` keyword.
let arg = match (item.args.as_slice(), item.keywords.as_slice()) {
([arg], []) => arg,
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_python_formatter/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ pub(crate) const fn is_module_docstring_newlines_enabled(context: &PyFormatConte
pub(crate) const fn is_dummy_implementations_enabled(context: &PyFormatContext) -> bool {
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()
}
Loading

0 comments on commit 494e4d2

Please sign in to comment.