From d1e88dc98467084a18b1ea612ac06cf203b4ab9c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 16 Nov 2023 13:59:54 -0600 Subject: [PATCH] Update UP032 to unescape curly braces in literal parts of converted strings (#8697) Closes #8694 --- .../test/fixtures/pyupgrade/UP032_0.py | 17 +++ .../src/rules/pyupgrade/helpers.rs | 13 +++ .../src/rules/pyupgrade/rules/f_strings.rs | 10 +- ...__rules__pyupgrade__tests__UP032_0.py.snap | 109 ++++++++++++++++-- 4 files changed, 136 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py index 19868afac3284..c5f21e0b7d93d 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py @@ -226,3 +226,20 @@ async def c(): "".format(new_dict, d) ) + +# The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +( + "{}" + "{{}}" +).format(a) + +("{}" "{{}}").format(a) + + +# Both strings will be converted to an f-string and the curly braces in the second should left escaped +( + "{}" + "{{{}}}" +).format(a, b) + +("{}" "{{{}}}").format(a, b) diff --git a/crates/ruff_linter/src/rules/pyupgrade/helpers.rs b/crates/ruff_linter/src/rules/pyupgrade/helpers.rs index 03f48d877aa0d..07196a9127332 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/helpers.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/helpers.rs @@ -19,3 +19,16 @@ pub(super) fn curly_escape(text: &str) -> Cow<'_, str> { } }) } + +static DOUBLE_CURLY_BRACES: Lazy = Lazy::new(|| Regex::new(r"((\{\{)|(\}\}))").unwrap()); + +pub(super) fn curly_unescape(text: &str) -> Cow<'_, str> { + // Match all double curly braces and replace with a single + DOUBLE_CURLY_BRACES.replace_all(text, |caps: &Captures| { + if &caps[1] == "{{" { + "{".to_string() + } else { + "}".to_string() + } + }) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs index 0ce52785c4139..e7cefb982f898 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -18,7 +18,7 @@ use crate::checkers::ast::Checker; use crate::fix::edits::fits_or_shrinks; use crate::rules::pyflakes::format::FormatSummary; -use crate::rules::pyupgrade::helpers::curly_escape; +use crate::rules::pyupgrade::helpers::{curly_escape, curly_unescape}; /// ## What it does /// Checks for `str.format` calls that can be replaced with f-strings. @@ -357,9 +357,11 @@ pub(crate) fn f_strings( Some((Tok::String { .. }, range)) => { match try_convert_to_f_string(range, &mut summary, checker.locator()) { Ok(Some(fstring)) => patches.push((range, fstring)), - // Skip any strings that don't require conversion (e.g., literal segments of an - // implicit concatenation). - Ok(None) => continue, + // Convert escaped curly brackets e.g. `{{` to `{` in literal string parts + Ok(None) => patches.push(( + range, + curly_unescape(checker.locator().slice(range)).to_string(), + )), // If any of the segments fail to convert, then we can't convert the entire // expression. Err(_) => return, diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index 4538fe9b917de..83e87da819fd2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -995,10 +995,11 @@ UP032_0.py:212:18: UP032 [*] Use f-string instead of `format` call 211 211 | # When fixing, trim the trailing empty string. 212 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}" 213 |- "".format(new_dict, d)) - 212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}") -214 213 | -215 214 | # When fixing, trim the trailing empty string. -216 215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}" + 212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}" + 213 |+ "") +214 214 | +215 215 | # When fixing, trim the trailing empty string. +216 216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}" UP032_0.py:216:18: UP032 [*] Use f-string instead of `format` call | @@ -1041,9 +1042,10 @@ UP032_0.py:220:5: UP032 [*] Use f-string instead of `format` call 220 |- "Conflicting configuration dicts: {!r} {!r}" 221 |- "".format(new_dict, d) 220 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}" -222 221 | ) -223 222 | -224 223 | raise ValueError( + 221 |+ "" +222 222 | ) +223 223 | +224 224 | raise ValueError( UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call | @@ -1064,7 +1066,96 @@ UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call 225 |- "Conflicting configuration dicts: {!r} {!r}" 226 |- "".format(new_dict, d) 225 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}" -227 226 | -228 227 | ) + 226 |+ "" +227 227 | +228 228 | ) +229 229 | + +UP032_0.py:231:1: UP032 [*] Use f-string instead of `format` call + | +230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +231 | / ( +232 | | "{}" +233 | | "{{}}" +234 | | ).format(a) + | |___________^ UP032 +235 | +236 | ("{}" "{{}}").format(a) + | + = help: Convert to f-string + +ℹ Safe fix +229 229 | +230 230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +231 231 | ( + 232 |+ f"{a}" +232 233 | "{}" +233 |- "{{}}" +234 |-).format(a) + 234 |+) +235 235 | +236 236 | ("{}" "{{}}").format(a) +237 237 | + +UP032_0.py:236:1: UP032 [*] Use f-string instead of `format` call + | +234 | ).format(a) +235 | +236 | ("{}" "{{}}").format(a) + | ^^^^^^^^^^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Safe fix +233 233 | "{{}}" +234 234 | ).format(a) +235 235 | +236 |-("{}" "{{}}").format(a) + 236 |+(f"{a}" "{}") +237 237 | +238 238 | +239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped + +UP032_0.py:240:1: UP032 [*] Use f-string instead of `format` call + | +239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped +240 | / ( +241 | | "{}" +242 | | "{{{}}}" +243 | | ).format(a, b) + | |______________^ UP032 +244 | +245 | ("{}" "{{{}}}").format(a, b) + | + = help: Convert to f-string + +ℹ Safe fix +238 238 | +239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped +240 240 | ( +241 |- "{}" +242 |- "{{{}}}" +243 |-).format(a, b) + 241 |+ f"{a}" + 242 |+ f"{{{b}}}" + 243 |+) +244 244 | +245 245 | ("{}" "{{{}}}").format(a, b) + +UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call + | +243 | ).format(a, b) +244 | +245 | ("{}" "{{{}}}").format(a, b) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Safe fix +242 242 | "{{{}}}" +243 243 | ).format(a, b) +244 244 | +245 |-("{}" "{{{}}}").format(a, b) + 245 |+(f"{a}" f"{{{b}}}")