mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 10:22:24 +00:00
Prefer breaking the implicit string concatenation over breaking before %
(#5947)
This commit is contained in:
parent
42d969f19f
commit
fdb3c8852f
5 changed files with 637 additions and 80 deletions
120
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py
vendored
Normal file
120
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_implicit_string.py
vendored
Normal file
|
@ -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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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
|
|
@ -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,11 +41,56 @@ impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
|
|||
fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let comments = f.context().comments().clone();
|
||||
|
||||
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 {
|
||||
soft_line_break_or_space().fmt(f)?;
|
||||
}
|
||||
|
||||
item.op.format().fmt(f)?;
|
||||
|
||||
if right_has_leading_comment {
|
||||
hard_line_break().fmt(f)?;
|
||||
} else {
|
||||
space().fmt(f)?;
|
||||
}
|
||||
|
||||
group(&item.right.format()).fmt(f)
|
||||
});
|
||||
|
||||
let format_left = format_with(|f: &mut PyFormatter| {
|
||||
let format_string =
|
||||
expression.format().with_options(ExprConstantLayout::String(
|
||||
StringLayout::ImplicitConcatenatedBinaryLeftSide,
|
||||
));
|
||||
|
||||
if is_expression_parenthesized(expression.into(), f.context().source()) {
|
||||
parenthesized("(", &format_string, ")").fmt(f)
|
||||
} else {
|
||||
format_string.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| {
|
||||
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) {
|
||||
if is_expression_parenthesized(
|
||||
bin_expression.as_any_node_ref(),
|
||||
source,
|
||||
) {
|
||||
None
|
||||
} else {
|
||||
Some(bin_expression)
|
||||
|
@ -81,7 +134,9 @@ impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
|
|||
)?;
|
||||
|
||||
// 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() {
|
||||
if comments.has_leading_comments(right.as_ref())
|
||||
|| !operator_comments.is_empty()
|
||||
{
|
||||
hard_line_break().fmt(f)?;
|
||||
} else if needs_space {
|
||||
space().fmt(f)?;
|
||||
|
@ -103,6 +158,8 @@ impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
|
|||
|
||||
in_parentheses_only_group(&format_inner).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> {
|
||||
// Handled inside of `fmt_fields`
|
||||
|
@ -110,6 +167,34 @@ impl FormatNodeRule<ExprBinOp> 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;
|
||||
|
||||
|
|
|
@ -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<ExprConstant, PyFormatContext<'_>> for FormatExprConstant {
|
||||
type Options = ExprConstantLayout;
|
||||
|
||||
fn with_options(mut self, options: Self::Options) -> Self {
|
||||
self.layout = options;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatNodeRule<ExprConstant> for FormatExprConstant {
|
||||
fn fmt_fields(&self, item: &ExprConstant, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
|
@ -31,7 +51,13 @@ impl FormatNodeRule<ExprConstant> 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)
|
||||
}
|
||||
|
|
|
@ -18,17 +18,36 @@ 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<PyFormatContext<'_>> for FormatString<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
match self.layout {
|
||||
StringLayout::Default => {
|
||||
let string_range = self.constant.range();
|
||||
let string_content = f.context().locator().slice(string_range);
|
||||
|
||||
|
@ -38,6 +57,11 @@ impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
|||
FormatStringPart::new(string_range).fmt(f)
|
||||
}
|
||||
}
|
||||
StringLayout::ImplicitConcatenatedBinaryLeftSide => {
|
||||
FormatStringContinuation::new(self.constant).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FormatStringContinuation<'a> {
|
||||
|
|
|
@ -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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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.<locals>.SkipTestCase%s) "
|
||||
"as SkippingTestCase.test_skip_unless_db_feature.<locals>.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,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue