Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pyupgrade: Format specifiers (#1594)
A part of #827. Posting this for visibility. Still has some work to do to be done. Things that still need done before this is ready: - [x] Does not work when the item is being assigned to a variable - [x] Does not work if being used in a function call - [x] Fix incorrectly removed calls in the function - [x] Has not been tested with pyupgrade negative test cases Tests from pyupgrade can be seen here: https://github.com/asottile/pyupgrade/blob/main/tests/features/format_literals_test.py Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
- Loading branch information
1 parent
f1a5e53
commit c016c41
Showing
14 changed files
with
472 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Invalid calls; errors expected. | ||
|
||
"{0}" "{1}" "{2}".format(1, 2, 3) | ||
|
||
"a {3} complicated {1} string with {0} {2}".format( | ||
"first", "second", "third", "fourth" | ||
) | ||
|
||
'{0}'.format(1) | ||
|
||
'{0:x}'.format(30) | ||
|
||
x = '{0}'.format(1) | ||
|
||
'''{0}\n{1}\n'''.format(1, 2) | ||
|
||
x = "foo {0}" \ | ||
"bar {1}".format(1, 2) | ||
|
||
("{0}").format(1) | ||
|
||
"\N{snowman} {0}".format(1) | ||
|
||
'{' '0}'.format(1) | ||
|
||
# These will not change because we are waiting for libcst to fix this issue: | ||
# https://github.com/Instagram/LibCST/issues/846 | ||
print( | ||
'foo{0}' | ||
'bar{1}'.format(1, 2) | ||
) | ||
|
||
print( | ||
'foo{0}' # ohai\n" | ||
'bar{1}'.format(1, 2) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Valid calls; no errors expected. | ||
|
||
'{}'.format(1) | ||
|
||
|
||
x = ('{0} {1}',) | ||
|
||
'{0} {0}'.format(1) | ||
|
||
'{0:<{1}}'.format(1, 4) | ||
|
||
f"{0}".format(a) | ||
|
||
f"{0}".format(1) | ||
|
||
print(f"{0}".format(1)) | ||
|
||
# I did not include the following tests because ruff does not seem to work with | ||
# invalid python syntax (which is a good thing) | ||
|
||
# "{0}"format(1) | ||
# '{'.format(1)", "'}'.format(1) | ||
# ("{0}" # {1}\n"{2}").format(1, 2, 3) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1606,6 +1606,8 @@ | |
"UP027", | ||
"UP028", | ||
"UP029", | ||
"UP03", | ||
"UP030", | ||
"W", | ||
"W2", | ||
"W29", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
use anyhow::{anyhow, bail, Result}; | ||
use libcst_native::{Arg, Codegen, CodegenState, Expression}; | ||
use once_cell::sync::Lazy; | ||
use regex::Regex; | ||
use rustpython_ast::Expr; | ||
|
||
use crate::ast::types::Range; | ||
use crate::autofix::Fix; | ||
use crate::checkers::ast::Checker; | ||
use crate::cst::matchers::{match_call, match_expression}; | ||
use crate::pyflakes::format::FormatSummary; | ||
use crate::registry::Diagnostic; | ||
use crate::violations; | ||
|
||
// An opening curly brace, followed by any integer, followed by any text, | ||
// followed by a closing brace. | ||
static FORMAT_SPECIFIER: Lazy<Regex> = | ||
Lazy::new(|| Regex::new(r"\{(?P<int>\d+)(?P<fmt>.*?)}").unwrap()); | ||
|
||
/// Returns a string without the format specifiers. | ||
/// Ex. "Hello {0} {1}" -> "Hello {} {}" | ||
fn remove_specifiers(raw_specifiers: &str) -> String { | ||
FORMAT_SPECIFIER | ||
.replace_all(raw_specifiers, "{$fmt}") | ||
.to_string() | ||
} | ||
|
||
/// Return the corrected argument vector. | ||
fn generate_arguments<'a>( | ||
old_args: &[Arg<'a>], | ||
correct_order: &'a [usize], | ||
) -> Result<Vec<Arg<'a>>> { | ||
let mut new_args: Vec<Arg> = Vec::with_capacity(old_args.len()); | ||
for (idx, given) in correct_order.iter().enumerate() { | ||
// We need to keep the formatting in the same order but move the values. | ||
let values = old_args | ||
.get(*given) | ||
.ok_or_else(|| anyhow!("Failed to extract argument at: {given}"))?; | ||
let formatting = old_args | ||
.get(idx) | ||
.ok_or_else(|| anyhow!("Failed to extract argument at: {idx}"))?; | ||
let new_arg = Arg { | ||
value: values.value.clone(), | ||
comma: formatting.comma.clone(), | ||
equal: None, | ||
keyword: None, | ||
star: values.star, | ||
whitespace_after_star: formatting.whitespace_after_star.clone(), | ||
whitespace_after_arg: formatting.whitespace_after_arg.clone(), | ||
}; | ||
new_args.push(new_arg); | ||
} | ||
Ok(new_args) | ||
} | ||
|
||
/// Returns the corrected function call. | ||
fn generate_call(module_text: &str, correct_order: &[usize]) -> Result<String> { | ||
let mut expression = match_expression(module_text)?; | ||
let mut call = match_call(&mut expression)?; | ||
|
||
// Fix the call arguments. | ||
call.args = generate_arguments(&call.args, correct_order)?; | ||
|
||
// Fix the string itself. | ||
let Expression::Attribute(item) = &*call.func else { | ||
panic!("Expected: Expression::Attribute") | ||
}; | ||
|
||
let mut state = CodegenState::default(); | ||
item.codegen(&mut state); | ||
let cleaned = remove_specifiers(&state.to_string()); | ||
|
||
call.func = Box::new(match_expression(&cleaned)?); | ||
|
||
let mut state = CodegenState::default(); | ||
expression.codegen(&mut state); | ||
if module_text == state.to_string() { | ||
// Ex) `'{' '0}'.format(1)` | ||
bail!("Failed to generate call expression for: {module_text}") | ||
} | ||
Ok(state.to_string()) | ||
} | ||
|
||
/// UP030 | ||
pub(crate) fn format_literals(checker: &mut Checker, summary: &FormatSummary, expr: &Expr) { | ||
// The format we expect is, e.g.: `"{0} {1}".format(...)` | ||
if summary.has_nested_parts { | ||
return; | ||
} | ||
if !summary.keywords.is_empty() { | ||
return; | ||
} | ||
if !summary.autos.is_empty() { | ||
return; | ||
} | ||
if !(0..summary.indexes.len()).all(|index| summary.indexes.contains(&index)) { | ||
return; | ||
} | ||
|
||
let mut diagnostic = Diagnostic::new(violations::FormatLiterals, Range::from_located(expr)); | ||
if checker.patch(diagnostic.kind.code()) { | ||
// Currently, the only issue we know of is in LibCST: | ||
// https://github.com/Instagram/LibCST/issues/846 | ||
if let Ok(contents) = generate_call( | ||
&checker | ||
.locator | ||
.slice_source_code_range(&Range::from_located(expr)), | ||
&summary.indexes, | ||
) { | ||
diagnostic.amend(Fix::replacement( | ||
contents, | ||
expr.location, | ||
expr.end_location.unwrap(), | ||
)); | ||
}; | ||
} | ||
checker.diagnostics.push(diagnostic); | ||
} |
Oops, something went wrong.