Skip to content

Commit 640e19f

Browse files
committed
Fix percent-format precision and integer conversion
Assisted-by: Codex:GPT-6
1 parent 908368e commit 640e19f

3 files changed

Lines changed: 89 additions & 76 deletions

File tree

‎crates/vm/src/cformat.rs‎

Lines changed: 44 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@ use crate::{
2626
stdlib::builtins,
2727
};
2828

29+
fn format_decimal_object(
30+
vm: &VirtualMachine,
31+
spec: &CFormatSpec,
32+
obj: &PyObject,
33+
) -> PyResult<String> {
34+
let type_error = || {
35+
vm.new_type_error(format!(
36+
"%{} format: a real number is required, not {}",
37+
spec.format_type.to_char(),
38+
obj.class().slot_name()
39+
))
40+
};
41+
// Decimal conversions prefer __int__; __index__ is only a fallback when absent.
42+
let i = obj
43+
.number()
44+
.int(vm)
45+
.or_else(|| obj.try_index_opt(vm))
46+
.ok_or_else(type_error)?
47+
.map_err(|error| {
48+
if error.fast_isinstance(vm.ctx.exceptions.type_error) {
49+
type_error()
50+
} else {
51+
error
52+
}
53+
})?;
54+
check_int_to_str_digits(i.as_bigint(), vm)?;
55+
Ok(spec.format_number(i.as_bigint()))
56+
}
57+
2958
fn spec_format_bytes(
3059
vm: &VirtualMachine,
3160
spec: &CFormatSpec,
@@ -80,27 +109,7 @@ fn spec_format_bytes(
80109
Ok(spec.format_number(&bigint).into_bytes())
81110
}
82111
obj => {
83-
// CPython parity: `%d` / `%i` / `%u` accept any object
84-
// with `__index__` (preferred) or `__int__`.
85-
if let Some(int_result) = obj.try_index_opt(vm) {
86-
let i = int_result?;
87-
check_int_to_str_digits(i.as_bigint(), vm)?;
88-
return Ok(spec.format_number(i.as_bigint()).into_bytes());
89-
}
90-
91-
if let Some(method) = vm.get_method(obj.clone(), identifier!(vm, __int__)) {
92-
let result = method?.call((), vm)?;
93-
if let Some(i) = result.downcast_ref::<PyInt>() {
94-
check_int_to_str_digits(i.as_bigint(), vm)?;
95-
return Ok(spec.format_number(i.as_bigint()).into_bytes());
96-
}
97-
}
98-
99-
Err(vm.new_type_error(format!(
100-
"%{} format: a real number is required, not {}",
101-
spec.format_type.to_char(),
102-
obj.class().slot_name()
103-
)))
112+
format_decimal_object(vm, spec, obj).map(String::into_bytes)
104113
}
105114
})
106115
}
@@ -209,25 +218,7 @@ fn spec_format_string(
209218
Ok(spec.format_number(&bigint).into())
210219
}
211220
obj => {
212-
// CPython parity: `%d` / `%i` / `%u` accept any object
213-
// with `__index__` (preferred) or `__int__`.
214-
if let Some(int_result) = obj.try_index_opt(vm) {
215-
let i = int_result?;
216-
check_int_to_str_digits(i.as_bigint(), vm)?;
217-
return Ok(spec.format_number(i.as_bigint()).into());
218-
}
219-
if let Some(method) = vm.get_method(obj.clone(), identifier!(vm, __int__)) {
220-
let result = method?.call((), vm)?;
221-
if let Some(i) = result.downcast_ref::<PyInt>() {
222-
check_int_to_str_digits(i.as_bigint(), vm)?;
223-
return Ok(spec.format_number(i.as_bigint()).into());
224-
}
225-
}
226-
Err(vm.new_type_error(format!(
227-
"%{} format: a real number is required, not {}",
228-
spec.format_type.to_char(),
229-
obj.class().slot_name()
230-
)))
221+
format_decimal_object(vm, spec, obj).map(Into::into)
231222
}
232223
})
233224
}
@@ -285,37 +276,11 @@ fn spec_format_string(
285276
}
286277
}
287278

288-
fn try_update_quantity_from_element(
289-
vm: &VirtualMachine,
290-
element: Option<&PyObject>,
291-
) -> PyResult<CFormatQuantity> {
279+
fn get_star_arg(vm: &VirtualMachine, element: Option<&PyObject>) -> PyResult<i32> {
292280
match element {
293-
Some(width_obj) => {
294-
if let Some(i) = width_obj.downcast_ref::<PyInt>() {
295-
let i = i.try_to_primitive::<i32>(vm)?.unsigned_abs();
296-
Ok(CFormatQuantity::Amount(i as usize))
297-
} else {
298-
Err(vm.new_type_error("* wants int"))
299-
}
300-
}
301-
None => Err(vm.new_type_error("not enough arguments for format string")),
302-
}
303-
}
304-
305-
fn try_conversion_flag_from_tuple(
306-
vm: &VirtualMachine,
307-
element: Option<&PyObject>,
308-
) -> PyResult<CConversionFlags> {
309-
match element {
310-
Some(width_obj) => {
311-
if let Some(i) = width_obj.downcast_ref::<PyInt>() {
312-
let i = i.try_to_primitive::<i32>(vm)?;
313-
let flags = if i < 0 {
314-
CConversionFlags::LEFT_ADJUST
315-
} else {
316-
CConversionFlags::from_bits(0).unwrap()
317-
};
318-
Ok(flags)
281+
Some(obj) => {
282+
if let Some(i) = obj.downcast_ref::<PyInt>() {
283+
i.try_to_primitive::<i32>(vm)
319284
} else {
320285
Err(vm.new_type_error("* wants int"))
321286
}
@@ -334,10 +299,11 @@ fn try_update_quantity_from_tuple<'a, I: Iterator<Item = &'a PyObject>>(
334299
return Ok(());
335300
};
336301

337-
let element = elements.next();
338-
f.insert(try_conversion_flag_from_tuple(vm, element)?);
339-
let quantity = try_update_quantity_from_element(vm, element)?;
340-
*q = Some(quantity);
302+
let width = get_star_arg(vm, elements.next())?;
303+
if width < 0 {
304+
f.insert(CConversionFlags::LEFT_ADJUST);
305+
}
306+
*q = Some(CFormatQuantity::Amount(width.unsigned_abs() as usize));
341307
Ok(())
342308
}
343309

@@ -350,8 +316,10 @@ fn try_update_precision_from_tuple<'a, I: Iterator<Item = &'a PyObject>>(
350316
return Ok(());
351317
};
352318

353-
let quantity = try_update_quantity_from_element(vm, elements.next())?;
354-
*p = Some(CFormatPrecision::Quantity(quantity));
319+
let precision = get_star_arg(vm, elements.next())?.max(0) as usize;
320+
*p = Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(
321+
precision,
322+
)));
355323
Ok(())
356324
}
357325

‎extra_tests/snippets/builtin_bytes.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,14 @@
644644
# mod
645645
assert b"rust%bpython%b" % (b" ", b"!") == b"rust python!"
646646
assert b"x=%i y=%f" % (1, 2.5) == b"x=1 y=2.500000"
647+
for precision in (-1, -3, -(2**31)):
648+
assert b"%.*b" % (precision, b"hello") == b""
649+
assert b"%.*s" % (-3, bytearray(b"hello")) == b""
650+
assert b"%.*a" % (-3, "hello") == b""
651+
assert b"%*.*d" % (-6, -3, 12) == b"12 "
652+
assert b"%.*f" % (-3, 1.25) == b"1"
653+
assert_raises(OverflowError, b"%.*b".__mod__, (-(2**31) - 1, b"abc"))
654+
assert_raises(TypeError, b"%.*b".__mod__, (1.0, b"abc"))
647655

648656

649657
# __bytes__

‎extra_tests/snippets/builtin_str.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,43 @@ def __repr__(self):
338338
# Printf-style String formatting
339339
assert "%d %d" % (1, 2) == "1 2"
340340
assert "%*c " % (3, "❤") == " ❤ "
341+
for precision in (-1, -3, -(2**31)):
342+
assert "%.*s" % (precision, "🐍hello") == ""
343+
assert "%*.*d" % (-6, -3, 12) == "12 "
344+
assert "%.*f" % (-3, 1.25) == "1"
345+
assert "%.*g" % (-3, 12.5) == "1e+01"
346+
assert_raises(OverflowError, "%.*s".__mod__, (-(2**31) - 1, "abc"))
347+
assert_raises(TypeError, "%.*s".__mod__, (1.0, "abc"))
348+
349+
350+
class PercentIndex:
351+
def __index__(self):
352+
return 7
353+
354+
355+
class PercentInt(PercentIndex):
356+
def __init__(self, value):
357+
self.value = value
358+
359+
def __int__(self):
360+
if isinstance(self.value, Exception):
361+
raise self.value
362+
return self.value
363+
364+
365+
for template in ("%d", "%i", "%u", b"%d", b"%i", b"%u"):
366+
assert template % PercentInt(3) == template % 3
367+
assert template % PercentIndex() == template % 7
368+
for value in (None, TypeError("conversion failed")):
369+
with assert_raises(TypeError) as cm:
370+
template % PercentInt(value)
371+
assert str(cm.exception).endswith("a real number is required, not PercentInt")
372+
assert_raises(
373+
RuntimeError, template.__mod__, PercentInt(RuntimeError("conversion failed"))
374+
)
375+
assert "%x" % PercentInt(3) == "7"
376+
assert b"%o" % PercentInt(3) == b"7"
377+
341378
assert (
342379
"%(first)s %(second)s" % {"second": "World!", "first": "Hello,"} == "Hello, World!"
343380
)

0 commit comments

Comments
 (0)