From 88f89580c1354f95876e8a786a06d6fa1b3afc48 Mon Sep 17 00:00:00 2001 From: shAn-kor Date: Sun, 26 Jul 2026 23:35:23 +0900 Subject: [PATCH 1/2] Fix zero padding for string format specs Assisted-by: Codex:gpt-5 --- Lib/test/test_str.py | 2 +- crates/common/src/format.rs | 49 ++++++++++++++++++++++++++++++++++++- crates/vm/src/format.rs | 3 +++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 5135564284e..2a3c36f2e57 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -1070,7 +1070,7 @@ def test_issue18183(self): '\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') diff --git a/crates/common/src/format.rs b/crates/common/src/format.rs index 4218dca7b74..fa71b27c5d9 100644 --- a/crates/common/src/format.rs +++ b/crates/common/src/format.rs @@ -227,6 +227,7 @@ pub struct FormatSpec { conversion: Option, fill: Option, align: Option, + align_specified: bool, sign: Option, alternate_form: bool, width: Option, @@ -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!( diff --git a/crates/vm/src/format.rs b/crates/vm/src/format.rs index 2f4652dccdd..80b906505bf 100644 --- a/crates/vm/src/format.rs +++ b/crates/vm/src/format.rs @@ -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) From 6ac349104c082d4d19ad63e283918ececc6e0e36 Mon Sep 17 00:00:00 2001 From: shAn-kor Date: Mon, 27 Jul 2026 18:25:42 +0900 Subject: [PATCH 2/2] Add string alignment format regression test Assisted-by: Codex:gpt-5 --- extra_tests/snippets/builtin_format.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extra_tests/snippets/builtin_format.py b/extra_tests/snippets/builtin_format.py index 250d8ad6cac..c2e2a897470 100644 --- a/extra_tests/snippets/builtin_format.py +++ b/extra_tests/snippets/builtin_format.py @@ -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"