diff --git a/crates/stdlib/src/math.rs b/crates/stdlib/src/math.rs index cb406e515..4b23b7b7b 100644 --- a/crates/stdlib/src/math.rs +++ b/crates/stdlib/src/math.rs @@ -368,6 +368,11 @@ mod math { #[pyfunction] fn ceil(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // `math_ceil`: an exact float skips the `__ceil__` lookup. + if let Some(f) = x.downcast_ref_if_exact::(vm) { + let v = try_f64_to_bigint(f.to_f64().ceil(), vm)?; + return Ok(vm.ctx.new_int(v).into()); + } // Only call __ceil__ if the class defines it - if it exists but is not callable, // the error should be propagated (not fall back to float conversion) if x.class().has_attr(identifier!(vm, __ceil__)) { @@ -378,14 +383,16 @@ mod math { let v = try_f64_to_bigint(v?.to_f64().ceil(), vm)?; return Ok(vm.ctx.new_int(v).into()); } - Err(vm.new_type_error(format!( - "type '{}' doesn't define '__ceil__' method", - x.class().name(), - ))) + Err(vm.new_type_error(format!("must be real number, not {}", x.class().name()))) } #[pyfunction] fn floor(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // `math_floor`: an exact float skips the `__floor__` lookup. + if let Some(f) = x.downcast_ref_if_exact::(vm) { + let v = try_f64_to_bigint(f.to_f64().floor(), vm)?; + return Ok(vm.ctx.new_int(v).into()); + } // Only call __floor__ if the class defines it - if it exists but is not callable, // the error should be propagated (not fall back to float conversion) if x.class().has_attr(identifier!(vm, __floor__)) { @@ -396,10 +403,7 @@ mod math { let v = try_f64_to_bigint(v?.to_f64().floor(), vm)?; return Ok(vm.ctx.new_int(v).into()); } - Err(vm.new_type_error(format!( - "type '{}' doesn't define '__floor__' method", - x.class().name(), - ))) + Err(vm.new_type_error(format!("must be real number, not {}", x.class().name()))) } #[pyfunction] diff --git a/extra_tests/snippets/stdlib_math.py b/extra_tests/snippets/stdlib_math.py index 08182f2fc..00d6c5f3a 100644 --- a/extra_tests/snippets/stdlib_math.py +++ b/extra_tests/snippets/stdlib_math.py @@ -102,6 +102,30 @@ with assert_raises(TypeError): math.ceil(object()) with assert_raises(TypeError): math.floor(object()) +with assert_raises(TypeError) as cm: + math.floor("1.5") +assert str(cm.exception) == "must be real number, not str" +with assert_raises(TypeError) as cm: + math.ceil(None) +assert str(cm.exception) == "must be real number, not NoneType" + +assert math.floor(-0.5) == -1 and math.ceil(-0.5) == 0 +with assert_raises(OverflowError): + math.floor(INF) +with assert_raises(ValueError): + math.ceil(NAN) + + +class FloatFloor(float): + def __floor__(self): + return "sub floor" + + def __ceil__(self): + return "sub ceil" + + +assert math.floor(FloatFloor(1.5)) == "sub floor" +assert math.ceil(FloatFloor(1.5)) == "sub ceil" isclose = math.isclose