mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Apply the format spec to a bool instead of dropping it (#8566)
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
This commit is contained in:
1 parent
f8de240feb
commit
4c448c4704
2 files changed
+98
-7
No files matched your search
@@ -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<String, FormatSpecError> {
|
||||
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::<String>() + &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();
|
||||
|
||||
@@ -269,3 +269,37 @@ assert "{:.4e}".format(1234.5) == "1.2345e+03"
|
||||
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")
|
||||
Reference in new issue
Block a user