Skip to content

Conversation

@danparizher
Copy link
Contributor

Summary

Fixes #19348

@github-actions
Copy link
Contributor

github-actions bot commented Jul 15, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@MichaReiser MichaReiser added the fixes Related to suggested fixes for violations label Jul 18, 2025
@ntBre ntBre self-requested a review July 24, 2025 13:08
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think we can just extend the current regex and also handle an additional edge case at the same time.

fn replace_single_brace(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut last = 0;
for mat in FORMAT_SPECIFIER.find_iter(text) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the risk of going too wild with regular expressions, I think we can just handle this directly in FORMAT_SPECIFIER:

static FORMAT_SPECIFIER: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?x)
            (?P<prefix>
                ^|[^{]|(?:\{{2})+  # preceded by nothing, a non-brace, or an even number of braces
            )
            \{                     # opening curly brace
                (?P<int>\d+)       # followed by any integer
                (?P<fmt>.*?)       # followed by any text
            }                      # followed by a closing brace
        ",
    )
    .unwrap()
});

While I was here, I went ahead and set verbose mode on the regex (initial (?x)) and moved the comments on FORMAT_SPECIFIER into the regex itself.

With this change, we also have to pass along the $prefix in the calls below. For example:

                        string.value = arena.alloc(
                            FORMAT_SPECIFIER
                                .replace_all(string.value, "$prefix{$fmt}")
                                .to_string(),
                        );

I almost neglected the "even number of braces" check, but cases like this are valid:

"{{{0}}}".format(123)

I think this is verging on needing us to parse the whole format string manually, but if we can't come up with any other edge cases, I think the regex is fine. I'd recommend adding this as a fixable test case too.

@ntBre ntBre changed the title [pyupgrade] Fix UP030 to avoid modifying double curly braces in format strings [pyupgrade] Fix UP030 to avoid modifying double curly braces in format strings Jul 29, 2025
@ntBre ntBre added the bug Something isn't working label Jul 29, 2025
@ntBre ntBre enabled auto-merge (squash) July 29, 2025 18:28
@ntBre ntBre merged commit d449c54 into astral-sh:main Jul 29, 2025
35 checks passed
@danparizher danparizher deleted the fix-19348 branch July 29, 2025 19:31
dcreager added a commit that referenced this pull request Aug 1, 2025
* main: (24 commits)
  Add `Checker::context` method, deduplicate Unicode checks (#19609)
  [`flake8-pyi`] Preserve inline comment in ellipsis removal (`PYI013`) (#19399)
  [ty] Add flow diagram for import resolution
  [ty] Add comments to some core resolver functions
  [ty] Add missing ticks and use consistent quoting
  [ty] Reflow some long lines
  [ty] Unexport helper function
  [ty] Remove offset from `CompletionTargetTokens::Unknown`
  [`pyupgrade`] Fix `UP030` to avoid modifying double curly braces in format strings (#19378)
  [ty] fix a typo  (#19621)
  [ty] synthesize `__replace__` for dataclasses (>=3.13) (#19545)
  [ty] Discard `Definition`s when normalizing `Signature`s (#19615)
  [ty] Fix empty spans following a line terminator and unprintable character spans in diagnostics (#19535)
  Add `LinterContext::settings` to avoid passing separate settings (#19608)
  Support `.pyi` files in ruff analyze graph (#19611)
  [ty] Sync vendored typeshed stubs (#19607)
  [ty] Bump docstring-adder pin (#19606)
  [`perflint`] Ignore rule if target is `global` or `nonlocal` (`PERF401`) (#19539)
  Add license classifier back to pyproject.toml (#19599)
  [ty] Add stub mapping support to signature help (#19570)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fixes Related to suggested fixes for violations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UP030 replaces regex repetitions

3 participants