mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Fix zero padding for string format specs (#8407)
* Fix zero padding for string format specs Assisted-by: Codex:gpt-5 * Add string alignment format regression test Assisted-by: Codex:gpt-5
This commit is contained in:
1 parent
1613741f86
commit
bf4a2b110c
4 files changed
+60
-2
No files matched your search
Vendored
+1
-1
@@ -1070,7 +1070,7 @@ class StrTest(string_tests.StringLikeTest,
|
||||
'\U00100000'.ljust(3, '\U00010000')
|
||||
'\U00100000'.rjust(3, '\U00010000')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; '{0:08s}'.format('result') misalign — '0' fill treated as numeric zero-pad for str type
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; '{0.}'.format() raises ValueError instead of IndexError
|
||||
def test_format(self):
|
||||
self.assertEqual(''.format(), '')
|
||||
self.assertEqual('a'.format(), 'a')
|
||||
|
||||
@@ -227,6 +227,7 @@ pub struct FormatSpec {
|
||||
conversion: Option<FormatConversion>,
|
||||
fill: Option<CodePoint>,
|
||||
align: Option<FormatAlign>,
|
||||
align_specified: bool,
|
||||
sign: Option<FormatSign>,
|
||||
alternate_form: bool,
|
||||
width: Option<usize>,
|
||||
@@ -346,6 +347,7 @@ impl FormatSpec {
|
||||
// get_integer in CPython
|
||||
let (conversion, text) = FormatConversion::parse(text);
|
||||
let (mut fill, mut align, text) = parse_fill_and_align(text);
|
||||
let align_specified = align.is_some();
|
||||
let (sign, text) = FormatSign::parse(text);
|
||||
let (alternate_form, text) = parse_alternate_form(text);
|
||||
let (zero, text) = parse_zero(text);
|
||||
@@ -374,6 +376,7 @@ impl FormatSpec {
|
||||
conversion,
|
||||
fill,
|
||||
align,
|
||||
align_specified,
|
||||
sign,
|
||||
alternate_form,
|
||||
width,
|
||||
@@ -1020,13 +1023,24 @@ impl FormatSpec {
|
||||
self.validate_format(FormatType::String)?;
|
||||
match self.format_type {
|
||||
Some(FormatType::String) | None => {
|
||||
if self.align == Some(FormatAlign::AfterSign) && self.align_specified {
|
||||
return Err(FormatSpecError::StringAlignmentFlag);
|
||||
}
|
||||
// CPython parity: precision truncates BEFORE width pads.
|
||||
// `'{:3.2s}'.format('abc')` -> 'ab ' (truncate to 'ab', pad to 3).
|
||||
let truncated: String = match self.precision {
|
||||
Some(p) => s.deref().chars().take(p).collect(),
|
||||
None => s.deref().to_owned(),
|
||||
};
|
||||
Ok(self.format_sign_and_align(&truncated, "", FormatAlign::Left))
|
||||
let spec = Self {
|
||||
align: if self.align == Some(FormatAlign::AfterSign) {
|
||||
Some(FormatAlign::Left)
|
||||
} else {
|
||||
self.align
|
||||
},
|
||||
..*self
|
||||
};
|
||||
Ok(spec.format_sign_and_align(&truncated, "", FormatAlign::Left))
|
||||
}
|
||||
_ => {
|
||||
let ch = char::from(self.format_type.as_ref().unwrap());
|
||||
@@ -1260,6 +1274,7 @@ pub enum FormatSpecError {
|
||||
CodeNotInRange,
|
||||
ZeroPadding,
|
||||
AlignmentFlag,
|
||||
StringAlignmentFlag,
|
||||
NotImplemented(char, &'static str),
|
||||
}
|
||||
|
||||
@@ -1609,6 +1624,7 @@ mod tests {
|
||||
conversion: None,
|
||||
fill: None,
|
||||
align: None,
|
||||
align_specified: false,
|
||||
sign: None,
|
||||
alternate_form: false,
|
||||
width: Some(33),
|
||||
@@ -1626,6 +1642,7 @@ mod tests {
|
||||
conversion: None,
|
||||
fill: Some('<'.into()),
|
||||
align: Some(FormatAlign::Right),
|
||||
align_specified: true,
|
||||
sign: None,
|
||||
alternate_form: false,
|
||||
width: Some(33),
|
||||
@@ -1643,6 +1660,7 @@ mod tests {
|
||||
conversion: None,
|
||||
fill: Some('<'.into()),
|
||||
align: Some(FormatAlign::Right),
|
||||
align_specified: true,
|
||||
sign: Some(FormatSign::Minus),
|
||||
alternate_form: true,
|
||||
width: Some(23),
|
||||
@@ -1690,6 +1708,35 @@ mod tests {
|
||||
assert_eq!(format_bool("%", false), Ok("0.000000%".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_string_zero_padding_uses_left_alignment() {
|
||||
let spec = FormatSpec::parse("08s").unwrap();
|
||||
let value = "result".to_owned();
|
||||
|
||||
assert_eq!(spec.format_string(&value), Ok("result00".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_string_explicit_after_sign_alignment_is_invalid() {
|
||||
let spec = FormatSpec::parse("=8s").unwrap();
|
||||
let value = "result".to_owned();
|
||||
|
||||
assert_eq!(
|
||||
spec.format_string(&value),
|
||||
Err(FormatSpecError::StringAlignmentFlag)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_int_zero_padding_stays_after_sign() {
|
||||
let spec = FormatSpec::parse("08").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
spec.format_int(&BigInt::from(-42)),
|
||||
Ok("-0000042".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_int() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -77,6 +77,9 @@ impl IntoPyException for FormatSpecError {
|
||||
Self::AlignmentFlag => {
|
||||
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
|
||||
}
|
||||
Self::StringAlignmentFlag => {
|
||||
vm.new_value_error("'=' alignment not allowed in string format specifier")
|
||||
}
|
||||
Self::NotImplemented(c, s) => {
|
||||
let msg = format!("Format code '{c}' for object of type '{s}' not implemented yet");
|
||||
vm.new_value_error(msg)
|
||||
|
||||
@@ -24,6 +24,14 @@ def test_zero_padding():
|
||||
|
||||
test_zero_padding()
|
||||
|
||||
try:
|
||||
format("result", "=8s")
|
||||
except ValueError as error:
|
||||
if str(error) != "'=' alignment not allowed in string format specifier":
|
||||
raise AssertionError(f"unexpected error message: {error}") from error
|
||||
else:
|
||||
raise AssertionError("expected ValueError for '=8s' string format specifier")
|
||||
|
||||
assert "{:,}".format(100) == "100"
|
||||
assert "{:,}".format(1024) == "1,024"
|
||||
assert "{:_}".format(65536) == "65_536"
|
||||
|
||||
Reference in new issue
Block a user