Match invalid format specifier error messages (#8477)

* Match invalid format specifier error details

Assisted-by: Codex:gpt-5.6-sol

* format: convert FormatSpecError in one helper

InvalidFormatSpecifier needs the spec and the object type. Drop
IntoPyException so that path cannot produce a generic message.

Assisted-by: Grok:4.6

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
This commit is contained in:
Seonghun AnandJeong YunWon authored and GitHub committed 2026-10-05 16:39:46 +00:00
1 parent 54e119d3c1
commit 3d1c174d17
7 files changed
+105 -107

No files matched your search

-3
View File
@@ -529,7 +529,6 @@ class FormatTest(unittest.TestCase):
with self.assertRaisesRegex(ValueError, error_msg):
'{:._,f}'.format(1.1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_better_error_message_format(self):
# https://bugs.python.org/issue20524
for value in [12j, 12, 12.0, "12"]:
@@ -551,7 +550,6 @@ class FormatTest(unittest.TestCase):
with self.assertRaisesRegex(ValueError, err):
eval("f'xx{value:{bad_format_spec}}yy'")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unicode_in_error_message(self):
str_err = re.escape(
"Invalid format specifier '%ЫйЯЧ' for object of type 'str'")
@@ -615,7 +613,6 @@ class FormatTest(unittest.TestCase):
self.assertEqual(f"{-0.:x>z6.1f}", "xxx0.0")
self.assertEqual(f"{-0.:🖤>z6.1f}", "🖤🖤🖤0.0") # multi-byte fill char
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_specifier_z_error(self):
error_msg = re.compile("Invalid format specifier '.*z.*'")
with self.assertRaisesRegex(ValueError, error_msg):
+6 -5
View File
@@ -1,9 +1,8 @@
use super::{PyInt, PyStrRef, PyType, PyTypeRef, PyUtf8StrRef};
use crate::common::format::FormatSpec;
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, VirtualMachine,
class::{PyClassDef, PyClassImpl},
convert::{IntoPyException, ToPyObject, ToPyResult},
convert::{ToPyObject, ToPyResult},
function::{FuncArgs, OptionalArg},
protocol::PyNumberMethods,
types::{AsNumber, Constructor, Representable},
@@ -124,9 +123,11 @@ impl Py<PyBool> {
vm: &VirtualMachine,
) -> PyResult<String> {
let new_bool = zelf.try_to_bool(vm)?;
FormatSpec::parse(format_spec.as_str())
.and_then(|format_spec| format_spec.format_bool(new_bool))
.map_err(|err| err.into_pyexception(vm))
crate::format::parse_format_spec(zelf.as_object(), format_spec.as_str(), vm)?
.format_bool(new_bool)
.map_err(|err| {
crate::format::format_spec_error(err, zelf.as_object(), format_spec.as_str(), vm)
})
}
}
+9 -10
View File
@@ -3,8 +3,8 @@ use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::PyUtf8StrRef,
class::{PyClassDef, PyClassImpl},
common::{format::FormatSpec, wtf8::Wtf8Buf},
convert::{IntoPyException, ToPyObject, ToPyResult},
common::wtf8::Wtf8Buf,
convert::{ToPyObject, ToPyResult},
function::{FuncArgs, OptionalArg, PyComparisonValue},
protocol::PyNumberMethods,
stdlib::_warnings,
@@ -571,17 +571,16 @@ impl Py<PyComplex> {
if format_spec.is_empty() {
return Ok(zelf.as_object().str(vm)?.as_wtf8().to_owned());
}
let format_spec =
FormatSpec::parse(format_spec.as_str()).map_err(|err| err.into_pyexception(vm))?;
let result = if format_spec.has_locale_format() {
let spec = crate::format::parse_format_spec(zelf.as_object(), format_spec.as_str(), vm)?;
let result = if spec.has_locale_format() {
let locale = crate::format::get_locale_info();
format_spec.format_complex_locale(&zelf.as_complex(), &locale)
spec.format_complex_locale(&zelf.as_complex(), &locale)
} else {
format_spec.format_complex(&zelf.as_complex())
spec.format_complex(&zelf.as_complex())
};
result
.map(Wtf8Buf::from_string)
.map_err(|err| err.into_pyexception(vm))
result.map(Wtf8Buf::from_string).map_err(|err| {
crate::format::format_spec_error(err, zelf.as_object(), format_spec.as_str(), vm)
})
}
#[pyclassmethod]
+9 -10
View File
@@ -6,8 +6,8 @@ use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
TryFromBorrowedObject, TryFromObject, VirtualMachine,
class::{PyClassDef, PyClassImpl},
common::{float_ops, format::FormatSpec, hash, wtf8::Wtf8Buf},
convert::{IntoPyException, ToPyObject, ToPyResult},
common::{float_ops, hash, wtf8::Wtf8Buf},
convert::{ToPyObject, ToPyResult},
function::{ArgBytesLike, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue},
protocol::PyNumberMethods,
types::{AsNumber, Callable, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
@@ -291,17 +291,16 @@ impl Py<PyFloat> {
if format_spec.is_empty() {
return Ok(zelf.as_object().str(vm)?.as_wtf8().to_owned());
}
let format_spec =
FormatSpec::parse(format_spec.as_str()).map_err(|err| err.into_pyexception(vm))?;
let result = if format_spec.has_locale_format() {
let spec = crate::format::parse_format_spec(zelf.as_object(), format_spec.as_str(), vm)?;
let result = if spec.has_locale_format() {
let locale = crate::format::get_locale_info();
format_spec.format_float_locale(zelf.to_f64(), &locale)
spec.format_float_locale(zelf.to_f64(), &locale)
} else {
format_spec.format_float(zelf.to_f64())
spec.format_float(zelf.to_f64())
};
result
.map(Wtf8Buf::from_string)
.map_err(|err| err.into_pyexception(vm))
result.map(Wtf8Buf::from_string).map_err(|err| {
crate::format::format_spec_error(err, zelf.as_object(), format_spec.as_str(), vm)
})
}
#[pystaticmethod]
+9 -11
View File
@@ -6,12 +6,11 @@ use crate::{
byte::bytes_from_object,
class::{PyClassDef, PyClassImpl},
common::{
format::FormatSpec,
hash,
int::{bigint_to_finite_float, bytes_to_int, true_div},
wtf8::Wtf8Buf,
},
convert::{IntoPyException, ToPyObject, ToPyResult},
convert::{ToPyObject, ToPyResult},
function::{
ArgByteOrder, ArgIntoBool, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue,
PySsize,
@@ -601,20 +600,19 @@ impl Py<PyInt> {
if format_spec.is_empty() && !zelf.class().is(vm.ctx.types.int_type) {
return Ok(zelf.as_object().str(vm)?.as_wtf8().to_owned());
}
let format_spec =
FormatSpec::parse(format_spec.as_str()).map_err(|err| err.into_pyexception(vm))?;
if format_spec.is_decimal_int_format() {
let spec = crate::format::parse_format_spec(zelf.as_object(), format_spec.as_str(), vm)?;
if spec.is_decimal_int_format() {
check_int_to_str_digits(zelf.as_bigint(), vm)?;
}
let result = if format_spec.has_locale_format() {
let result = if spec.has_locale_format() {
let locale = crate::format::get_locale_info();
format_spec.format_int_locale(zelf.as_bigint(), &locale)
spec.format_int_locale(zelf.as_bigint(), &locale)
} else {
format_spec.format_int(zelf.as_bigint())
spec.format_int(zelf.as_bigint())
};
result
.map(Wtf8Buf::from_string)
.map_err(|err| err.into_pyexception(vm))
result.map(Wtf8Buf::from_string).map_err(|err| {
crate::format::format_spec_error(err, zelf.as_object(), format_spec.as_str(), vm)
})
}
#[pymethod]
+7 -7
View File
@@ -15,7 +15,7 @@ use crate::{
lock::LazyLock,
str::{PyKindStr, StrData, StrKind},
},
convert::{IntoPyException, ToPyException, ToPyObject, ToPyResult},
convert::{ToPyException, ToPyObject, ToPyResult},
format::{format, format_map},
function::{ArgIterable, FuncArgs, OptionalArg, PyComparisonValue, PySsize},
intern::PyInterned,
@@ -41,7 +41,7 @@ use num_traits::ToPrimitive;
use rustpython_common::{
ascii,
atomic::{self, PyAtomic, Radium},
format::{FormatSpec, FormatString, FromTemplate},
format::{FormatString, FromTemplate},
hash,
lock::PyMutex,
str::DeduceStrKind,
@@ -1167,11 +1167,11 @@ impl Py<PyStr> {
};
}
let zelf = zelf.try_into_utf8(vm)?;
let s = FormatSpec::parse(format_spec.as_str())
.and_then(|format_spec| {
format_spec.format_string(&CharLenStr(zelf.as_str(), zelf.char_len()))
})
.map_err(|err| err.into_pyexception(vm))?;
let s = crate::format::parse_format_spec(zelf.as_object(), format_spec.as_str(), vm)?
.format_string(&CharLenStr(zelf.as_str(), zelf.char_len()))
.map_err(|err| {
crate::format::format_spec_error(err, zelf.as_object(), format_spec.as_str(), vm)
})?;
Ok(vm.ctx.new_str(s))
}
+65 -61
View File
@@ -1,9 +1,6 @@
use crate::{
PyObject, PyResult, VirtualMachine,
builtins::PyBaseExceptionRef,
convert::{IntoPyException, ToPyException},
function::FuncArgs,
stdlib::builtins,
PyObject, PyResult, VirtualMachine, builtins::PyBaseExceptionRef, convert::ToPyException,
function::FuncArgs, stdlib::builtins,
};
use crate::common::format::*;
@@ -41,63 +38,70 @@ pub(crate) fn get_locale_info() -> LocaleInfo {
}
}
impl IntoPyException for FormatSpecError {
fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
match self {
Self::DecimalDigitsTooMany => {
vm.new_value_error("Too many decimal digits in format string")
}
Self::PrecisionTooBig => vm.new_value_error("Precision too big"),
Self::PrecisionMissing => vm.new_value_error("Format specifier missing precision"),
Self::InvalidFormatSpecifier => vm.new_value_error("Invalid format specifier"),
Self::UnspecifiedFormat(c1, c2) => {
let msg = format!("Cannot specify '{c1}' with '{c2}'.");
vm.new_value_error(msg)
}
Self::ExclusiveFormat(c1, c2) => {
let msg = format!("Cannot specify both '{c1}' and '{c2}'.");
vm.new_value_error(msg)
}
Self::UnknownFormatCode(c, s) => {
let msg = format!("Unknown format code '{c}' for object of type '{s}'");
vm.new_value_error(msg)
}
Self::PrecisionNotAllowed => {
vm.new_value_error("Precision not allowed in integer format specifier")
}
Self::NotAllowed(s) => {
let msg = format!("{s} not allowed with integer format specifier 'c'");
vm.new_value_error(msg)
}
Self::UnableToConvert => vm.new_value_error("Unable to convert int to float"),
Self::CodeNotInRange => vm.new_overflow_error("%c arg not in range(0x110000)"),
Self::IntTooLargeForCLong => {
vm.new_overflow_error("Python int too large to convert to C long")
}
Self::ZeroPadding => {
vm.new_value_error("Zero padding is not allowed in complex format specifier")
}
Self::AlignmentFlag => {
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
}
Self::NegativeZeroCoercionNotAllowed(type_name) => {
let msg = format!(
"Negative zero coercion (z) not allowed in {type_name} format specifier"
);
vm.new_value_error(msg)
}
Self::StringAlignmentFlag => {
vm.new_value_error("'=' alignment not allowed in string format specifier")
}
Self::StringSpecNotAllowed(s) => {
let msg = format!("{s} not allowed in string format specifier");
vm.new_value_error(msg)
}
Self::NotImplemented(c, s) => {
let msg = format!("Format code '{c}' for object of type '{s}' not implemented yet");
vm.new_value_error(msg)
}
pub(crate) fn parse_format_spec(
object: &PyObject,
spec: &str,
vm: &VirtualMachine,
) -> PyResult<FormatSpec> {
FormatSpec::parse(spec).map_err(|err| format_spec_error(err, object, spec, vm))
}
pub(crate) fn format_spec_error(
err: FormatSpecError,
object: &PyObject,
spec: &str,
vm: &VirtualMachine,
) -> PyBaseExceptionRef {
match err {
FormatSpecError::InvalidFormatSpecifier => vm.new_value_error(format!(
"Invalid format specifier '{spec}' for object of type '{}'",
object.class().name()
)),
FormatSpecError::DecimalDigitsTooMany => {
vm.new_value_error("Too many decimal digits in format string")
}
FormatSpecError::PrecisionTooBig => vm.new_value_error("Precision too big"),
FormatSpecError::PrecisionMissing => {
vm.new_value_error("Format specifier missing precision")
}
FormatSpecError::UnspecifiedFormat(c1, c2) => {
vm.new_value_error(format!("Cannot specify '{c1}' with '{c2}'."))
}
FormatSpecError::ExclusiveFormat(c1, c2) => {
vm.new_value_error(format!("Cannot specify both '{c1}' and '{c2}'."))
}
FormatSpecError::UnknownFormatCode(c, s) => vm.new_value_error(format!(
"Unknown format code '{c}' for object of type '{s}'"
)),
FormatSpecError::PrecisionNotAllowed => {
vm.new_value_error("Precision not allowed in integer format specifier")
}
FormatSpecError::NotAllowed(s) => {
vm.new_value_error(format!("{s} not allowed with integer format specifier 'c'"))
}
FormatSpecError::UnableToConvert => vm.new_value_error("Unable to convert int to float"),
FormatSpecError::CodeNotInRange => vm.new_overflow_error("%c arg not in range(0x110000)"),
FormatSpecError::IntTooLargeForCLong => {
vm.new_overflow_error("Python int too large to convert to C long")
}
FormatSpecError::ZeroPadding => {
vm.new_value_error("Zero padding is not allowed in complex format specifier")
}
FormatSpecError::AlignmentFlag => {
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
}
FormatSpecError::NegativeZeroCoercionNotAllowed(type_name) => vm.new_value_error(format!(
"Negative zero coercion (z) not allowed in {type_name} format specifier"
)),
FormatSpecError::StringAlignmentFlag => {
vm.new_value_error("'=' alignment not allowed in string format specifier")
}
FormatSpecError::StringSpecNotAllowed(s) => {
vm.new_value_error(format!("{s} not allowed in string format specifier"))
}
FormatSpecError::NotImplemented(c, s) => vm.new_value_error(format!(
"Format code '{c}' for object of type '{s}' not implemented yet"
)),
}
}