diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py new file mode 100644 index 0000000000..9f56e06e4a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py @@ -0,0 +1,120 @@ + +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." % (module, paths) +) + +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." + % + # comment + (module, paths) +) + +# Only important in parenthesized context because implicit string continuation otherwise doesn't expand +"The app module %r has multiple filesystem locations (%r); " "you must configure this app with an AppConfig subclass " "with a 'path' class attribute." % ( + module, + paths, +) + +("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "cccccccccccccccccccccccccccccccccccccccccccc" % (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d)) +("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "cccccccccccccccccccccccccccccccccccccccccccc" % aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) + +def test(): + return ( + "\n%(modified_count)s %(identifier)s %(action)s" + "%(destination)s%(unmodified)s%(post_processed)s." + ) % { + "modified_count": modified_count, + "identifier": "static file" + ("" if modified_count == 1 else "s"), + "action": "symlinked" if self.symlink else "copied", + "destination": (" to '%s'" % destination_path if destination_path else ""), + "unmodified": ( + ", %s unmodified" % unmodified_count if collected["unmodified"] else "" + ), + "post_processed": ( + collected["post_processed"] + and ", %s post-processed" % post_processed_count + or "" + ), + } + +# trailing expression comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + # Python 3.11 uses fully qualified test name in the output. + % (".test_foo" if PY311 else ""), + ) + +# dangling operator comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + % # Python 3.11 uses fully qualified test name in the output. + (".test_foo" if PY311 else ""), + ) + +# Black keeps as many operands as fit on the same line as the `%`. Ruff does not. This is intentional as these are rare and complicated things significantly +( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + + x +) + +( + b + c + d + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + + x +) + +( + b < c > d < + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + > x +) + + +self.assertEqual( + response.status_code, + status_code, + msg_prefix + "Couldn't retrieve content: Response code was %d" + " (expected %d)" % (response.status_code, status_code), +) + +def test(): + return ( + "((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -" + " (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))" + ) % {"lhs": lhs_sql, "rhs": rhs_sql}, tuple(lhs_params) * 2 + tuple(rhs_params) * 2 + +def test2(): + return "RETURNING %s INTO %s" % ( + ", ".join(field_names), + ", ".join(["%s"] * len(params)), + ), tuple(params) + +def test3(): + return ( + "(CASE WHEN JSON_TYPE(%s, %%s) IN (%s) " + "THEN JSON_TYPE(%s, %%s) ELSE JSON_EXTRACT(%s, %%s) END)" + ) % (lhs, datatype_values, lhs, lhs), (tuple(params) + (json_path,)) * 3 diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index f21d155b13..472bd33881 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,19 +1,27 @@ -use crate::comments::{trailing_comments, trailing_node_comments}; -use crate::expression::parentheses::{ - in_parentheses_only_group, in_parentheses_only_soft_line_break, - in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized, NeedsParentheses, - OptionalParentheses, -}; -use crate::expression::Parentheses; -use crate::prelude::*; -use crate::FormatNodeRule; -use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; -use ruff_python_ast::node::{AnyNodeRef, AstNode}; +use std::iter; + use rustpython_parser::ast::{ Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; use smallvec::SmallVec; -use std::iter; + +use ruff_formatter::{ + format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, +}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; +use ruff_python_ast::str::is_implicit_concatenation; + +use crate::comments::{trailing_comments, trailing_node_comments}; +use crate::expression::expr_constant::ExprConstantLayout; +use crate::expression::parentheses::{ + in_parentheses_only_group, in_parentheses_only_soft_line_break, + in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized, parenthesized, + NeedsParentheses, OptionalParentheses, +}; +use crate::expression::string::StringLayout; +use crate::expression::Parentheses; +use crate::prelude::*; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatExprBinOp { @@ -33,75 +41,124 @@ impl FormatNodeRule for FormatExprBinOp { fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); - let format_inner = format_with(|f: &mut PyFormatter| { - let source = f.context().source(); - let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(item), |parent| { - parent.left.as_bin_op_expr().and_then(|bin_expression| { - if is_expression_parenthesized(bin_expression.as_any_node_ref(), source) { - None + match Self::layout(item, f.context()) { + BinOpLayout::LeftString(expression) => { + let right_has_leading_comment = f + .context() + .comments() + .has_leading_comments(item.right.as_ref()); + + let format_right_and_op = format_with(|f| { + if right_has_leading_comment { + space().fmt(f)?; } else { - Some(bin_expression) + soft_line_break_or_space().fmt(f)?; } - }) - }) - .collect(); - // SAFETY: `binary_chain` is guaranteed not to be empty because it always contains the current expression. - let left_most = binary_chain.last().unwrap(); + item.op.format().fmt(f)?; - // Format the left most expression - in_parentheses_only_group(&left_most.left.format()).fmt(f)?; + if right_has_leading_comment { + hard_line_break().fmt(f)?; + } else { + space().fmt(f)?; + } - // Iterate upwards in the binary expression tree and, for each level, format the operator - // and the right expression. - for current in binary_chain.into_iter().rev() { - let ExprBinOp { - range: _, - left: _, - op, - right, - } = current; + group(&item.right.format()).fmt(f) + }); - let operator_comments = comments.dangling_comments(current); - let needs_space = !is_simple_power_expression(current); + let format_left = format_with(|f: &mut PyFormatter| { + let format_string = + expression.format().with_options(ExprConstantLayout::String( + StringLayout::ImplicitConcatenatedBinaryLeftSide, + )); - let before_operator_space = if needs_space { - in_parentheses_only_soft_line_break_or_space() - } else { - in_parentheses_only_soft_line_break() - }; + if is_expression_parenthesized(expression.into(), f.context().source()) { + parenthesized("(", &format_string, ")").fmt(f) + } else { + format_string.fmt(f) + } + }); - write!( - f, - [ - before_operator_space, - op.format(), - trailing_comments(operator_comments), - ] - )?; - - // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref()) || !operator_comments.is_empty() { - hard_line_break().fmt(f)?; - } else if needs_space { - space().fmt(f)?; - } - - in_parentheses_only_group(&right.format()).fmt(f)?; - - // It's necessary to format the trailing comments because the code bypasses - // `FormatNodeRule::fmt` for the nested binary expressions. - // Don't call the formatting function for the most outer binary expression because - // these comments have already been formatted. - if current != item { - trailing_node_comments(current).fmt(f)?; - } + group(&format_args![format_left, group(&format_right_and_op)]).fmt(f) } + BinOpLayout::Default => { + let format_inner = format_with(|f: &mut PyFormatter| { + let source = f.context().source(); + let binary_chain: SmallVec<[&ExprBinOp; 4]> = + iter::successors(Some(item), |parent| { + parent.left.as_bin_op_expr().and_then(|bin_expression| { + if is_expression_parenthesized( + bin_expression.as_any_node_ref(), + source, + ) { + None + } else { + Some(bin_expression) + } + }) + }) + .collect(); - Ok(()) - }); + // SAFETY: `binary_chain` is guaranteed not to be empty because it always contains the current expression. + let left_most = binary_chain.last().unwrap(); - in_parentheses_only_group(&format_inner).fmt(f) + // Format the left most expression + in_parentheses_only_group(&left_most.left.format()).fmt(f)?; + + // Iterate upwards in the binary expression tree and, for each level, format the operator + // and the right expression. + for current in binary_chain.into_iter().rev() { + let ExprBinOp { + range: _, + left: _, + op, + right, + } = current; + + let operator_comments = comments.dangling_comments(current); + let needs_space = !is_simple_power_expression(current); + + let before_operator_space = if needs_space { + in_parentheses_only_soft_line_break_or_space() + } else { + in_parentheses_only_soft_line_break() + }; + + write!( + f, + [ + before_operator_space, + op.format(), + trailing_comments(operator_comments), + ] + )?; + + // Format the operator on its own line if the right side has any leading comments. + if comments.has_leading_comments(right.as_ref()) + || !operator_comments.is_empty() + { + hard_line_break().fmt(f)?; + } else if needs_space { + space().fmt(f)?; + } + + in_parentheses_only_group(&right.format()).fmt(f)?; + + // It's necessary to format the trailing comments because the code bypasses + // `FormatNodeRule::fmt` for the nested binary expressions. + // Don't call the formatting function for the most outer binary expression because + // these comments have already been formatted. + if current != item { + trailing_node_comments(current).fmt(f)?; + } + } + + Ok(()) + }); + + in_parentheses_only_group(&format_inner).fmt(f) + } + } } fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> { @@ -110,6 +167,34 @@ impl FormatNodeRule for FormatExprBinOp { } } +impl FormatExprBinOp { + fn layout<'a>(bin_op: &'a ExprBinOp, context: &PyFormatContext) -> BinOpLayout<'a> { + if let Some( + constant @ ExprConstant { + value: Constant::Str(_), + range, + .. + }, + ) = bin_op.left.as_constant_expr() + { + let comments = context.comments(); + + if bin_op.op == Operator::Mod + && context.node_level().is_parenthesized() + && !comments.has_dangling_comments(constant) + && !comments.has_dangling_comments(bin_op) + && is_implicit_concatenation(&context.source()[*range]) + { + BinOpLayout::LeftString(constant) + } else { + BinOpLayout::Default + } + } else { + BinOpLayout::Default + } + } +} + const fn is_simple_power_expression(expr: &ExprBinOp) -> bool { expr.op.is_pow() && is_simple_power_operand(&expr.left) && is_simple_power_operand(&expr.right) } @@ -132,6 +217,24 @@ const fn is_simple_power_operand(expr: &Expr) -> bool { } } +#[derive(Copy, Clone, Debug)] +enum BinOpLayout<'a> { + Default, + + /// Specific layout for an implicit concatenated string using the "old" c-style formatting. + /// + /// ```python + /// ( + /// "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa %s" + /// "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb %s" % (a, b) + /// ) + /// ``` + /// + /// Prefers breaking the string parts over breaking in front of the `%` because it looks better if it + /// is kept on the same line. + LeftString(&'a ExprConstant), +} + #[derive(Copy, Clone)] pub struct FormatOperator; diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index 15b1a783a6..aaf7c9d6c3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -1,17 +1,37 @@ use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::{Constant, ExprConstant, Ranged}; +use ruff_formatter::FormatRuleWithOptions; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::str::is_implicit_concatenation; use crate::expression::number::{FormatComplex, FormatFloat, FormatInt}; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; -use crate::expression::string::{FormatString, StringPrefix, StringQuotes}; +use crate::expression::string::{FormatString, StringLayout, StringPrefix, StringQuotes}; use crate::prelude::*; use crate::{not_yet_implemented_custom_text, FormatNodeRule}; #[derive(Default)] -pub struct FormatExprConstant; +pub struct FormatExprConstant { + layout: ExprConstantLayout, +} + +#[derive(Copy, Clone, Debug, Default)] +pub enum ExprConstantLayout { + #[default] + Default, + + String(StringLayout), +} + +impl FormatRuleWithOptions> for FormatExprConstant { + type Options = ExprConstantLayout; + + fn with_options(mut self, options: Self::Options) -> Self { + self.layout = options; + self + } +} impl FormatNodeRule for FormatExprConstant { fn fmt_fields(&self, item: &ExprConstant, f: &mut PyFormatter) -> FormatResult<()> { @@ -31,7 +51,13 @@ impl FormatNodeRule for FormatExprConstant { Constant::Int(_) => FormatInt::new(item).fmt(f), Constant::Float(_) => FormatFloat::new(item).fmt(f), Constant::Complex { .. } => FormatComplex::new(item).fmt(f), - Constant::Str(_) => FormatString::new(item).fmt(f), + Constant::Str(_) => { + let string_layout = match self.layout { + ExprConstantLayout::Default => StringLayout::Default, + ExprConstantLayout::String(layout) => layout, + }; + FormatString::new(item).with_layout(string_layout).fmt(f) + } Constant::Bytes(_) => { not_yet_implemented_custom_text(r#"b"NOT_YET_IMPLEMENTED_BYTE_STRING""#).fmt(f) } diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 1970b72163..87e9ff824b 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -18,24 +18,48 @@ use crate::QuoteStyle; pub(super) struct FormatString<'a> { constant: &'a ExprConstant, + layout: StringLayout, +} + +#[derive(Default, Copy, Clone, Debug)] +pub enum StringLayout { + #[default] + Default, + + ImplicitConcatenatedBinaryLeftSide, } impl<'a> FormatString<'a> { pub(super) fn new(constant: &'a ExprConstant) -> Self { debug_assert!(constant.value.is_str()); - Self { constant } + Self { + constant, + layout: StringLayout::Default, + } + } + + pub(super) fn with_layout(mut self, layout: StringLayout) -> Self { + self.layout = layout; + self } } impl<'a> Format> for FormatString<'a> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let string_range = self.constant.range(); - let string_content = f.context().locator().slice(string_range); + match self.layout { + StringLayout::Default => { + let string_range = self.constant.range(); + let string_content = f.context().locator().slice(string_range); - if is_implicit_concatenation(string_content) { - in_parentheses_only_group(&FormatStringContinuation::new(self.constant)).fmt(f) - } else { - FormatStringPart::new(string_range).fmt(f) + if is_implicit_concatenation(string_content) { + in_parentheses_only_group(&FormatStringContinuation::new(self.constant)).fmt(f) + } else { + FormatStringPart::new(string_range).fmt(f) + } + } + StringLayout::ImplicitConcatenatedBinaryLeftSide => { + FormatStringContinuation::new(self.constant).fmt(f) + } } } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary_implicit_string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary_implicit_string.py.snap new file mode 100644 index 0000000000..8176cc6fdf --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary_implicit_string.py.snap @@ -0,0 +1,284 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py +--- +## Input +```py + +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." % (module, paths) +) + +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." + % + # comment + (module, paths) +) + +# Only important in parenthesized context because implicit string continuation otherwise doesn't expand +"The app module %r has multiple filesystem locations (%r); " "you must configure this app with an AppConfig subclass " "with a 'path' class attribute." % ( + module, + paths, +) + +("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "cccccccccccccccccccccccccccccccccccccccccccc" % (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d)) +("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "cccccccccccccccccccccccccccccccccccccccccccc" % aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) + +def test(): + return ( + "\n%(modified_count)s %(identifier)s %(action)s" + "%(destination)s%(unmodified)s%(post_processed)s." + ) % { + "modified_count": modified_count, + "identifier": "static file" + ("" if modified_count == 1 else "s"), + "action": "symlinked" if self.symlink else "copied", + "destination": (" to '%s'" % destination_path if destination_path else ""), + "unmodified": ( + ", %s unmodified" % unmodified_count if collected["unmodified"] else "" + ), + "post_processed": ( + collected["post_processed"] + and ", %s post-processed" % post_processed_count + or "" + ), + } + +# trailing expression comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + # Python 3.11 uses fully qualified test name in the output. + % (".test_foo" if PY311 else ""), + ) + +# dangling operator comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + % # Python 3.11 uses fully qualified test name in the output. + (".test_foo" if PY311 else ""), + ) + +# Black keeps as many operands as fit on the same line as the `%`. Ruff does not. This is intentional as these are rare and complicated things significantly +( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + + x +) + +( + b + c + d + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + + x +) + +( + b < c > d < + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + > x +) + + +self.assertEqual( + response.status_code, + status_code, + msg_prefix + "Couldn't retrieve content: Response code was %d" + " (expected %d)" % (response.status_code, status_code), +) + +def test(): + return ( + "((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -" + " (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))" + ) % {"lhs": lhs_sql, "rhs": rhs_sql}, tuple(lhs_params) * 2 + tuple(rhs_params) * 2 + +def test2(): + return "RETURNING %s INTO %s" % ( + ", ".join(field_names), + ", ".join(["%s"] * len(params)), + ), tuple(params) + +def test3(): + return ( + "(CASE WHEN JSON_TYPE(%s, %%s) IN (%s) " + "THEN JSON_TYPE(%s, %%s) ELSE JSON_EXTRACT(%s, %%s) END)" + ) % (lhs, datatype_values, lhs, lhs), (tuple(params) + (json_path,)) * 3 +``` + +## Output +```py +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." % (module, paths) +) + +raise ImproperlyConfigured( + "The app module %r has multiple filesystem locations (%r); " + "you must configure this app with an AppConfig subclass " + "with a 'path' class attr ibute." % + # comment + (module, paths) +) + +# Only important in parenthesized context because implicit string continuation otherwise doesn't expand +"The app module %r has multiple filesystem locations (%r); " "you must configure this app with an AppConfig subclass " "with a 'path' class attribute." % ( + module, + paths, +) + +( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccccccccccccccccccccc" + % ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + ) +) +( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccccccccccccccccccccc" + % aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) + + +def test(): + return ( + "\n%(modified_count)s %(identifier)s %(action)s" + "%(destination)s%(unmodified)s%(post_processed)s." + ) % { + "modified_count": modified_count, + "identifier": "static file" + ("" if modified_count == 1 else "s"), + "action": "symlinked" if self.symlink else "copied", + "destination": (" to '%s'" % destination_path if destination_path else ""), + "unmodified": ( + ", %s unmodified" % unmodified_count if collected["unmodified"] else "" + ), + "post_processed": ( + collected["post_processed"] and ", %s post-processed" % post_processed_count + or "" + ), + } + + +# trailing expression comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + # Python 3.11 uses fully qualified test name in the output. + % (".test_foo" if PY311 else ""), +) + +# dangling operator comment +self._assert_skipping( + SkipTestCase("test_foo").test_foo, + ValueError, + "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests." + "SkippingTestCase.test_skip_unless_db_feature..SkipTestCase%s) " + "as SkippingTestCase.test_skip_unless_db_feature..SkipTestCase " + "doesn't allow queries against the 'default' database." + % # Python 3.11 uses fully qualified test name in the output. + (".test_foo" if PY311 else ""), +) + +# Black keeps as many operands as fit on the same line as the `%`. Ruff does not. This is intentional as these are rare and complicated things significantly +( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" + % aaaaaaaaaaaa + + x +) + +( + b + + c + + d + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" % aaaaaaaaaaaa + + x +) + +( + b + < c + > d + < "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccccc" % aaaaaaaaaaaa + > x +) + + +self.assertEqual( + response.status_code, + status_code, + msg_prefix + + "Couldn't retrieve content: Response code was %d" + " (expected %d)" % (response.status_code, status_code), +) + + +def test(): + return ( + ( + "((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -" + " (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))" + ) % {"lhs": lhs_sql, "rhs": rhs_sql}, + tuple(lhs_params) * 2 + tuple(rhs_params) * 2, + ) + + +def test2(): + return ( + "RETURNING %s INTO %s" + % ( + ", ".join(field_names), + ", ".join(["%s"] * len(params)), + ), + tuple(params), + ) + + +def test3(): + return ( + ( + "(CASE WHEN JSON_TYPE(%s, %%s) IN (%s) " + "THEN JSON_TYPE(%s, %%s) ELSE JSON_EXTRACT(%s, %%s) END)" + ) % (lhs, datatype_values, lhs, lhs), + (tuple(params) + (json_path,)) * 3, + ) +``` + + +