From 8af4bd316375ace5e08f528ed85448c0b738f094 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:13:17 -0300 Subject: [PATCH] Apply the format spec to a bool instead of dropping it format_bool answered "True" or "False" for every spec that carries no presentation type, so width, fill, alignment and sign were all discarded: >>> f"{True:>5}" 'True' CPython has no bool.__format__ of its own. It uses int's, where an empty spec on a subclass gives str(self) and everything else formats the integer. The empty spec keeps the spelled out answer, the rest now goes to format_int, which also brings back the errors an integer spec raises. Assisted-by: Claude Code:claude-opus-5 --- crates/common/src/format.rs | 71 +++++++++++++++++++++++--- extra_tests/snippets/builtin_format.py | 34 ++++++++++++ 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/crates/common/src/format.rs b/crates/common/src/format.rs index 1c5c0a9c9de..2c545de0702 100644 --- a/crates/common/src/format.rs +++ b/crates/common/src/format.rs @@ -829,6 +829,37 @@ impl FormatSpec { Ok(self.format_sign_and_align(&AsciiStr::new(&magnitude_str), "", FormatAlign::Right)) } + /// Whether the spec carries nothing at all, which is what `format(value, "")` + /// parses to. Written as a destructure so a new field cannot be forgotten here. + fn is_empty(&self) -> bool { + let Self { + conversion, + fill, + align, + align_specified, + sign, + no_neg_0, + alternate_form, + width, + grouping_option, + precision, + frac_grouping_option, + format_type, + } = self; + conversion.is_none() + && fill.is_none() + && align.is_none() + && !align_specified + && sign.is_none() + && !no_neg_0 + && !alternate_form + && width.is_none() + && grouping_option.is_none() + && precision.is_none() + && frac_grouping_option.is_none() + && format_type.is_none() + } + pub fn format_bool(&self, input: bool) -> Result { let x = u8::from(input); match &self.format_type { @@ -844,13 +875,11 @@ impl FormatSpec { Some(FormatType::Exponent(_) | FormatType::FixedPoint(_) | FormatType::Percentage) => { self.format_float(x as f64) } - None => { - if self.no_neg_0 { - return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")); - } - let first_letter = (input.to_string().as_bytes()[0] as char).to_uppercase(); - Ok(first_letter.collect::() + &input.to_string()[1..]) - } + // Only the empty spec spells the value out. Everything else without a + // presentation type, a width or an alignment included, formats `bool` + // the way it formats the `int` it is. + None if self.is_empty() => Ok(if input { "True" } else { "False" }.to_owned()), + None => self.format_int(&BigInt::from_u8(x).unwrap()), Some(format_type) => { let ch = char::from(format_type); Err(FormatSpecError::UnknownFormatCode(ch, "bool")) @@ -1769,6 +1798,34 @@ mod tests { assert_eq!(format_bool("%", false), Ok("0.000000%".to_owned())); } + #[test] + fn format_bool_without_a_presentation_type() { + // The bare spec is the only one that spells the value out. + assert_eq!(format_bool("", true), Ok("True".to_owned())); + assert_eq!(format_bool("", false), Ok("False".to_owned())); + + // Anything else formats the integer, the way `int.__format__` would. + assert_eq!(format_bool("5", true), Ok(" 1".to_owned())); + assert_eq!(format_bool("<5", true), Ok("1 ".to_owned())); + assert_eq!(format_bool(">5", false), Ok(" 0".to_owned())); + assert_eq!(format_bool("^5", true), Ok(" 1 ".to_owned())); + assert_eq!(format_bool("05", true), Ok("00001".to_owned())); + assert_eq!(format_bool("+", true), Ok("+1".to_owned())); + assert_eq!(format_bool(" ", false), Ok(" 0".to_owned())); + assert_eq!(format_bool(",", true), Ok("1".to_owned())); + assert_eq!(format_bool("<", true), Ok("1".to_owned())); + + // And it inherits the integer rules, precision included. + assert_eq!( + format_bool(".2", true), + Err(FormatSpecError::PrecisionNotAllowed) + ); + assert_eq!( + format_bool("z", true), + Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")) + ); + } + #[test] fn format_string_zero_padding_uses_left_alignment() { let spec = FormatSpec::parse("08s").unwrap(); diff --git a/extra_tests/snippets/builtin_format.py b/extra_tests/snippets/builtin_format.py index 8ec8f3d5c2c..d92ddefe8ba 100644 --- a/extra_tests/snippets/builtin_format.py +++ b/extra_tests/snippets/builtin_format.py @@ -269,3 +269,37 @@ def test_zero_padding(): assert "{:.3g}".format(1234.5) == "1.23e+03" assert f"{float('nan'):.10f}" == "nan" assert f"{float('inf'):.10f}" == "inf" + +# bool has no __format__ of its own, so the int rules apply. Only the bare +# spec spells the value out. +assert format(True, "") == "True" +assert format(False, "") == "False" +assert f"{True}" == "True" +assert f"{True:}" == "True" + +assert format(True, "5") == " 1" +assert format(True, "<5") == "1 " +assert format(False, ">5") == " 0" +assert format(True, "^5") == " 1 " +assert format(True, "=5") == " 1" +assert format(True, "05") == "00001" +assert format(False, "05") == "00000" +assert format(True, "+") == "+1" +assert format(False, " ") == " 0" +assert format(True, ",") == "1" +assert format(True, "<") == "1" +assert "{:>6}|{:^6}".format(True, False) == " 1| 0 " + +# The presentation types were already right, and stay right. +assert format(True, "d") == "1" +assert format(True, "#b") == "0b1" +assert format(False, "x") == "0" +assert format(True, "c") == "\x01" +assert format(True, "e") == "1.000000e+00" +assert format(True, "%") == "100.000000%" + +# Precision belongs to no integer spec, bool included. +assert_raises(ValueError, format, True, ".2") +assert_raises(ValueError, format, True, "5.2") +assert_raises(ValueError, format, True, "z") +assert_raises(ValueError, format, True, "s")