Pass round() ndigits to __round__ unchanged (#8997)

round(x, ndigits) ran ndigits through __index__ before calling
__round__, so a user-defined __round__ never saw a non-integer ndigits
and an __index__ object reached it already converted to int. CPython's
builtin_round_impl() forwards ndigits as-is and leaves the conversion
to float.__round__ and int.__round__.

Forward the object, and take ndigits as ArgIndex in both slots so they
apply __index__ themselves. Calling the slots directly with an
__index__ object now works as in CPython instead of raising TypeError.

Assisted-by: Claude Code:claude-opus-5
Assisted-by: Claude Code:claude-opus-5-5
This commit is contained in:
Jeongseop Lim authored and GitHub committed 2026-10-07 23:18:55 +09:00
1 parent 0e67b82f52
commit a39d6c9676
4 files changed
+41 -11

No files matched your search

+5 -3
View File
@@ -8,7 +8,9 @@ use crate::{
class::{PyClassDef, PyClassImpl},
common::{float_ops, hash, wtf8::Wtf8Buf},
convert::{ToPyObject, ToPyResult},
function::{ArgBytesLike, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue},
function::{
ArgBytesLike, ArgIndex, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue,
},
protocol::PyNumberMethods,
types::{AsNumber, Callable, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
};
@@ -273,7 +275,7 @@ pub fn float_from_string(val: &PyObject, vm: &VirtualMachine) -> PyResult<f64> {
#[derive(FromArgs)]
struct RoundArgs {
#[pyarg(positional, optional)]
ndigits: Option<PyIntRef>,
ndigits: Option<ArgIndex>,
}
#[pyclass(
@@ -340,7 +342,7 @@ impl Py<PyFloat> {
fn __round__(&self, args: RoundArgs, vm: &VirtualMachine) -> PyResult {
let ndigits = args.ndigits;
let value = if let Some(ndigits) = ndigits {
let ndigits = ndigits.as_bigint();
let ndigits = ndigits.as_ref().as_bigint();
let ndigits = match ndigits.to_i32() {
Some(n) => n,
None if ndigits.is_positive() => i32::MAX,
+4 -4
View File
@@ -12,8 +12,8 @@ use crate::{
},
convert::{ToPyObject, ToPyResult},
function::{
ArgByteOrder, ArgIntoBool, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue,
PySsize,
ArgByteOrder, ArgIndex, ArgIntoBool, FuncArgs, OptionalArg, PyArithmeticValue,
PyComparisonValue, PySsize,
},
protocol::{PyNumberMethods, handle_bytes_to_int_err, numeric_literal_from_str},
types::{AsNumber, Comparable, Constructor, Hashable, PyComparisonOp, Representable},
@@ -526,7 +526,7 @@ impl Py<PyInt> {
#[derive(FromArgs)]
struct RoundArgs {
#[pyarg(positional, optional)]
ndigits: Option<PyIntRef>,
ndigits: Option<ArgIndex>,
}
#[pyclass(
@@ -538,7 +538,7 @@ impl Py<PyInt> {
#[pymethod]
fn __round__(zelf: PyRef<PyInt>, args: RoundArgs, vm: &VirtualMachine) -> PyRef<PyInt> {
if let Some(ndigits) = args.ndigits {
let ndigits = ndigits.as_bigint();
let ndigits = ndigits.as_ref().as_bigint();
// round(12345, -2) == 12300
// If precision >= 0, then any integer is already rounded correctly
if let Some(ndigits) = ndigits.neg().to_u32()
+3 -4
View File
@@ -1163,10 +1163,9 @@ mod builtins {
))
})?;
match ndigits {
Some(obj) => {
let ndigits = obj.try_index(vm)?;
meth.invoke((ndigits,), vm)
}
// Like CPython, pass ndigits through as-is; the __round__ implementation
// decides what it accepts (float and int apply __index__ themselves).
Some(obj) => meth.invoke((obj,), vm),
None => {
// without a parameter, the result type is coerced to int
meth.invoke((), vm)
+29
View File
@@ -111,3 +111,32 @@ assert type(round(MyInt(5))) is int
assert type(round(MyInt(5), 2)) is int
# A negative ndigits already produced a fresh exact int.
assert type(round(MyInt(15), -1)) is int
# round() passes ndigits to __round__ unchanged, like CPython's builtin_round_impl().
class Echo:
def __round__(self, *args):
return args
assert round(Echo(), []) == ([],)
assert round(Echo(), 1.5) == (1.5,)
assert type(round(Echo(), True)[0]) is bool
class Index:
def __index__(self):
return -1
assert type(round(Echo(), Index())[0]) is Index
# int.__round__ and float.__round__ apply __index__ to ndigits themselves.
assert round(15, Index()) == 20
assert round(1.25, Index()) == 0.0
assert (15).__round__(Index()) == 20
assert (1.25).__round__(Index()) == 0.0
with assert_raises(TypeError):
(15).__round__(1.0)
with assert_raises(TypeError):
(1.25).__round__([])