Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

Commit c6c1b1c

Browse files
authored
Report which format string mistake was made (RustPython#8659)
* fix(vm): report which format string mistake was made `ToPyException for FormatParseError` mapped two variants and sent the rest to "Unexpected error parsing format string", so most format-string mistakes gave no hint about the mistake. Against CPython 3.14.0: | expression | before | CPython | | --- | --- | --- | | `"a}b".format()` | Unexpected error… | Single `}` encountered in format string | | `"{0.}".format(1)` | Unexpected error… | Empty attribute in format string | | `"{0[]}".format([1])` | Unexpected error… | Empty attribute in format string | | `"{0[1}".format([1,2])` | Unexpected error… | expected `}` before end of string | | `"{0[0]x}".format([1])` | Unexpected error… | Only `.` or `[` may follow `]` in format field specifier | | `"{:{{}}".format(1)` | Unexpected error… | unmatched `{` in format spec | The match is now exhaustive, so a new variant cannot silently inherit the generic message. Two cases are deliberately left generic, with the reason in a comment beside each. `UnknownConversion` is raised both for a multi-character conversion (`{0!xy}`, where CPython says "expected ':' after conversion specifier") and for an empty one (`{0!}`, "unmatched '{' in format spec"), so one message cannot serve both without the parser saying which it hit. And a trailing lone `{` reaches `UnmatchedBracket` rather than `UnescapedStartBracketInLiteral`, so `"{".format()` still differs from CPython; that needs a parser change, not a message. Expectations in the new snippet are CPython 3.14.0's output and pass under both interpreters. Reverting only format.rs fails the snippet's first assertion. Assisted-by: Claude Code:claude-opus-5 * fix(common): separate a stray `{` from an unclosed field `parse_literal` already reports `UnescapedStartBracketInLiteral` for a brace that was not doubled, but `from_str` discards it with `or_else` and retries as a field, so `"{".format()` surfaced `UnmatchedBracket` and read as "expected '}' before end of string". CPython separates the two by whether anything follows the brace: "{" "a{" Single '{' encountered in format string "{s" "a{b" expected '}' before end of string "{0" expected '}' before end of string `parse_spec` now returns the literal error when the text is the brace alone, which is also what makes `UnescapedStartBracketInLiteral` reachable — the previous commit could map it but not trigger it. `format_parse_lone_start_bracket` pins both directions at the parser level, and the snippet covers the four messages under CPython 3.14.0 as well. The existing `format_parse_fail` assertion for `"{s"` is unchanged. Assisted-by: Claude Code:claude-opus-5
1 parent 4b1d9e3 commit c6c1b1c

3 files changed

Lines changed: 94 additions & 2 deletions

File tree

‎crates/common/src/format.rs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,12 @@ impl FormatString {
16181618
let right = &text[pos..];
16191619
let format_part = Self::parse_part_in_brackets(&left)?;
16201620
Ok((format_part, &right[1..]))
1621+
} else if text.len() == 1 {
1622+
// Nothing follows the brace, so it is a stray one rather than a field
1623+
// that was left open: CPython separates "{" and "a{" from "{0" and
1624+
// "a{b" the same way. `{` is one byte, so a length of one is the brace
1625+
// on its own.
1626+
Err(FormatParseError::UnescapedStartBracketInLiteral)
16211627
} else {
16221628
Err(FormatParseError::UnmatchedBracket)
16231629
}
@@ -2338,6 +2344,26 @@ mod tests {
23382344
);
23392345
}
23402346

2347+
#[test]
2348+
fn format_parse_lone_start_bracket() {
2349+
// A brace with nothing after it is a stray brace; one holding a field
2350+
// that was never closed is not. CPython words the two differently.
2351+
for lone in ["{", "a{"] {
2352+
assert_eq!(
2353+
FormatString::from_str(lone.as_ref()),
2354+
Err(FormatParseError::UnescapedStartBracketInLiteral),
2355+
"{lone:?}"
2356+
);
2357+
}
2358+
for unclosed in ["{s", "a{b", "{0"] {
2359+
assert_eq!(
2360+
FormatString::from_str(unclosed.as_ref()),
2361+
Err(FormatParseError::UnmatchedBracket),
2362+
"{unclosed:?}"
2363+
);
2364+
}
2365+
}
2366+
23412367
#[test]
23422368
fn square_brackets_inside_format() {
23432369
assert_eq!(

‎crates/vm/src/format.rs‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,36 @@ impl IntoPyException for FormatSpecError {
103103

104104
impl ToPyException for FormatParseError {
105105
fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
106+
// Matched exhaustively on purpose: a catch-all sent most of these to
107+
// "Unexpected error parsing format string", which tells the reader nothing
108+
// about the format string they mistyped.
106109
match self {
107-
Self::UnmatchedBracket => vm.new_value_error("expected '}' before end of string"),
110+
Self::UnmatchedBracket | Self::MissingRightBracket => {
111+
vm.new_value_error("expected '}' before end of string")
112+
}
113+
Self::MissingStartBracket => {
114+
vm.new_value_error("Single '}' encountered in format string")
115+
}
116+
// A brace in literal text that was not doubled, i.e. a stray `{` with
117+
// nothing after it. `parse_spec` reports this rather than
118+
// UnmatchedBracket so that "{" and "a{" are separated from "{0" and
119+
// "a{b", which are fields left open.
120+
Self::UnescapedStartBracketInLiteral => {
121+
vm.new_value_error("Single '{' encountered in format string")
122+
}
123+
Self::InvalidFormatSpecifier => vm.new_value_error("unmatched '{' in format spec"),
124+
// Reached both for a conversion that is not one character (`{0!xy}`,
125+
// where CPython says "expected ':' after conversion specifier") and for
126+
// an empty one (`{0!}`, "unmatched '{' in format spec"). Telling those
127+
// apart needs the parser to say which, so the generic message stays.
128+
Self::UnknownConversion => vm.new_value_error("Unexpected error parsing format string"),
129+
Self::EmptyAttribute => vm.new_value_error("Empty attribute in format string"),
130+
Self::InvalidCharacterAfterRightBracket => {
131+
vm.new_value_error("Only '.' or '[' may follow ']' in format field specifier")
132+
}
108133
Self::TooManyDecimalDigits => {
109134
vm.new_value_error("Too many decimal digits in format string")
110135
}
111-
_ => vm.new_value_error("Unexpected error parsing format string"),
112136
}
113137
}
114138
}

‎extra_tests/snippets/builtin_format.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,45 @@ def test_zero_padding():
336336
assert_raises(ValueError, format, True, "5.2")
337337
assert_raises(ValueError, format, True, "z")
338338
assert_raises(ValueError, format, True, "s")
339+
340+
341+
def test_format_parse_error_messages():
342+
# Most FormatParseError variants used to fall into a catch-all that said
343+
# "Unexpected error parsing format string", which does not say what is wrong
344+
# with the format string. Messages are CPython 3.14.0's.
345+
def message(fmt, *args):
346+
try:
347+
fmt.format(*args)
348+
except ValueError as err:
349+
return str(err)
350+
raise AssertionError("ValueError was not raised for " + repr(fmt))
351+
352+
assert message("a}b") == "Single '}' encountered in format string"
353+
assert message("{{a}") == "Single '}' encountered in format string"
354+
355+
assert message("{0.}", 1) == "Empty attribute in format string"
356+
assert message("{0[]}", [1]) == "Empty attribute in format string"
357+
358+
assert message("{0[1}", [1, 2]) == "expected '}' before end of string"
359+
assert message("{0", 1) == "expected '}' before end of string"
360+
361+
assert (
362+
message("{0[0]x}", [1])
363+
== "Only '.' or '[' may follow ']' in format field specifier"
364+
)
365+
assert (
366+
message("{0[0]]}", [1])
367+
== "Only '.' or '[' may follow ']' in format field specifier"
368+
)
369+
370+
assert message("{:{{}}", 1) == "unmatched '{' in format spec"
371+
372+
# A brace with nothing after it is a stray brace; one holding an unclosed
373+
# field is not.
374+
assert message("{") == "Single '{' encountered in format string"
375+
assert message("a{") == "Single '{' encountered in format string"
376+
assert message("{s") == "expected '}' before end of string"
377+
assert message("a{b") == "expected '}' before end of string"
378+
379+
380+
test_format_parse_error_messages()

0 commit comments

Comments
 (0)