Complete PyTime conversion fixtures and repair existing helpers (#9317)

* Fix rounding and bounds in existing PyTime test helpers

Extract the already-exposed conversion helper prerequisite from the broader #9143 surface without adding new test C-API exports or changing time.sleep.

Assisted-by: Codex:unknown

* Complete PyTime conversion fixtures in _testinternalcapi

Stack the eight consumed conversion APIs on the existing-helper prerequisite in #9317. Keep the arithmetic local to the real fixture consumers; do not expose _testcapi or change sleep/lifecycle behavior.

Assisted-by: Codex:unknown

---------

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
youknowdotandCodex authored and GitHub committed 2026-10-24 11:29:06 +09:00
1 parent a788de29da
commit 413710c107
2 files changed
+354 -13

No files matched your search

+183 -13
View File
@@ -1,5 +1,7 @@
pub(crate) use _testinternalcapi::module_def;
mod pytime;
use core::sync::atomic::{AtomicUsize, Ordering};
static RARE_SET_CLASS: AtomicUsize = AtomicUsize::new(0);
@@ -26,6 +28,7 @@ pub(crate) fn note_func_modification() {
#[pymodule]
mod _testinternalcapi {
use super::pytime::Round;
use super::*;
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
@@ -37,7 +40,7 @@ mod _testinternalcapi {
common::{hash::PyHash, lock::LazyLock},
dict_inner,
frame::FrameObject,
function::{OptionalArg, PyComparisonValue},
function::{FuncArgs, OptionalArg, PyComparisonValue},
object::{Traverse, TraverseFn},
protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods},
types::{
@@ -45,8 +48,13 @@ mod _testinternalcapi {
SelfIter,
},
};
#[cfg(any(unix, windows))]
use rustpython_host_env::time::TimeT;
use std::collections::HashMap;
#[cfg(not(any(unix, windows)))]
type TimeT = i64;
// ADAPTIVE_WARMUP_VALUE + 1
#[pyattr]
const SPECIALIZATION_THRESHOLD: usize = 2;
@@ -69,7 +77,7 @@ mod _testinternalcapi {
const SIZEOF_PYOBJECT: usize = core::mem::size_of::<crate::PyObject>();
#[pyattr]
const SIZEOF_TIME_T: usize = 8;
const SIZEOF_TIME_T: usize = core::mem::size_of::<TimeT>();
// JUMP_BACKWARD_INITIAL_VALUE + 1
#[pyattr]
@@ -486,24 +494,186 @@ mod _testinternalcapi {
}
}
fn pytime_args<'a, const N: usize>(
args: &'a FuncArgs,
name: &str,
vm: &VirtualMachine,
) -> PyResult<&'a [PyObjectRef; N]> {
// These CPython test helpers use METH_VARARGS, not keyword parsing.
if !args.kwargs.is_empty() {
return Err(vm.new_type_error(format!("{name}() takes no keyword arguments")));
}
args.args.as_slice().try_into().map_err(|_| {
vm.new_type_error(format!(
"function takes exactly {} argument{} ({} given)",
N,
if N == 1 { "" } else { "s" },
args.args.len()
))
})
}
fn pytime_c_int(object: &PyObject, vm: &VirtualMachine) -> PyResult<i32> {
// PyArg_ParseTuple's "i" converts to C long before checking C int bounds.
let value: core::ffi::c_long = object
.try_index(vm)?
.as_bigint()
.try_into()
.map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?;
num_traits::ToPrimitive::to_i32(&value).ok_or_else(|| {
vm.new_overflow_error(if value < 0 {
"signed integer is less than minimum"
} else {
"signed integer is greater than maximum"
})
})
}
fn pytime_i64(object: &PyObject, vm: &VirtualMachine) -> PyResult<i64> {
object
.try_index(vm)?
.as_bigint()
.try_into()
.map_err(|_| vm.new_overflow_error("Python int too large to convert to C int64_t"))
}
fn pytime_pair(
value: i64,
denominator: i64,
minimum: i64,
maximum: i64,
clamp: bool,
vm: &VirtualMachine,
) -> PyResult<(i64, i64)> {
let seconds = value.div_euclid(denominator);
if !(minimum..=maximum).contains(&seconds) {
if !clamp {
return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
}
return Ok((seconds.clamp(minimum, maximum), 0));
}
Ok((seconds, value.rem_euclid(denominator)))
}
#[pyfunction(name = "_PyTime_AsTimespec")]
fn pytime_as_timespec(ns: i64) -> (i64, i64) {
let sec = ns.div_euclid(1_000_000_000);
let nsec = ns.rem_euclid(1_000_000_000);
(sec, nsec)
fn pytime_as_timespec(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
let [ns] = pytime_args::<1>(&args, "_PyTime_AsTimespec", vm)?;
pytime_pair(
pytime_i64(ns, vm)?,
1_000_000_000,
TimeT::MIN as _,
TimeT::MAX as _,
false,
vm,
)
}
#[pyfunction(name = "_PyTime_AsTimespec_clamp")]
fn pytime_as_timespec_clamp(ns: i64) -> (i64, i64) {
pytime_as_timespec(ns)
fn pytime_as_timespec_clamp(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
let [ns] = pytime_args::<1>(&args, "_PyTime_AsTimespec_clamp", vm)?;
pytime_pair(
pytime_i64(ns, vm)?,
1_000_000_000,
TimeT::MIN as _,
TimeT::MAX as _,
true,
vm,
)
}
#[pyfunction(name = "_PyTime_AsTimeval_clamp")]
fn pytime_as_timeval_clamp(ns: i64, _rnd: OptionalArg<i32>) -> (i64, i64) {
let us = ns.div_euclid(1000);
let sec = us.div_euclid(1_000_000);
let usec = us.rem_euclid(1_000_000);
(sec, usec)
fn pytime_as_timeval_clamp(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval_clamp", vm)?;
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
// Validate rounding before converting the timestamp, as the C wrapper does.
pytime_timeval(pytime_i64(ns, vm)?, round, true, vm)
}
fn pytime_timeval(
ns: i64,
round: Round,
clamp: bool,
vm: &VirtualMachine,
) -> PyResult<(i64, i64)> {
let us = round.divide(ns, 1_000);
// Windows timeval uses C long even though the host time_t is 64-bit.
#[cfg(windows)]
let (minimum, maximum): (i64, i64) =
(core::ffi::c_long::MIN as _, core::ffi::c_long::MAX as _);
#[cfg(not(windows))]
let (minimum, maximum): (i64, i64) = (TimeT::MIN as _, TimeT::MAX as _);
pytime_pair(us, 1_000_000, minimum, maximum, clamp, vm)
}
#[pyfunction(name = "_PyTime_FromSeconds")]
fn pytime_from_seconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
let [seconds] = pytime_args::<1>(&args, "_PyTime_FromSeconds", vm)?;
Ok(pytime::from_seconds(pytime_c_int(seconds, vm)?))
}
#[pyfunction(name = "_PyTime_FromSecondsObject")]
fn pytime_from_seconds_object(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
let [object, round] = pytime_args::<2>(&args, "_PyTime_FromSecondsObject", vm)?;
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
pytime::from_seconds_object(object, round, vm)
}
#[pyfunction(name = "_PyTime_AsTimeval")]
fn pytime_as_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval", vm)?;
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
pytime_timeval(pytime_i64(ns, vm)?, round, false, vm)
}
fn pytime_as_subseconds(
args: FuncArgs,
name: &str,
divisor: i64,
vm: &VirtualMachine,
) -> PyResult<i64> {
let [ns, round] = pytime_args::<2>(&args, name, vm)?;
let round = pytime_c_int(round, vm)?;
// These two C wrappers convert the timestamp before validating the mode.
let ns = pytime_i64(ns, vm)?;
Ok(Round::from_int(round, vm)?.divide(ns, divisor))
}
#[pyfunction(name = "_PyTime_AsMilliseconds")]
fn pytime_as_milliseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
pytime_as_subseconds(args, "_PyTime_AsMilliseconds", 1_000_000, vm)
}
#[pyfunction(name = "_PyTime_AsMicroseconds")]
fn pytime_as_microseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
pytime_as_subseconds(args, "_PyTime_AsMicroseconds", 1_000, vm)
}
#[pyfunction(name = "_PyTime_ObjectToTime_t")]
fn pytime_object_to_time_t(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
let [object, round] = pytime_args::<2>(&args, "_PyTime_ObjectToTime_t", vm)?;
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
pytime::object_to_time_t(object, round, vm)
}
fn pytime_object_to_denominator(
args: FuncArgs,
name: &str,
denominator: i64,
vm: &VirtualMachine,
) -> PyResult<(i64, i64)> {
let [object, round] = pytime_args::<2>(&args, name, vm)?;
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
pytime::object_to_denominator(object, denominator, round, vm)
}
#[pyfunction(name = "_PyTime_ObjectToTimeval")]
fn pytime_object_to_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
pytime_object_to_denominator(args, "_PyTime_ObjectToTimeval", 1_000_000, vm)
}
#[pyfunction(name = "_PyTime_ObjectToTimespec")]
fn pytime_object_to_timespec(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
pytime_object_to_denominator(args, "_PyTime_ObjectToTimespec", 1_000_000_000, vm)
}
#[pyfunction]
@@ -0,0 +1,171 @@
//! Numeric conversions used by the internal PyTime test helpers.
#![cfg_attr(target_env = "musl", allow(deprecated))]
use crate::{AsObject, PyObject, PyResult, VirtualMachine, builtins::PyFloat};
use num_traits::ToPrimitive;
use rustpython_host_env::time::SEC_TO_NS;
#[cfg(any(unix, windows))]
pub(super) use rustpython_host_env::time::TimeT;
#[cfg(not(any(unix, windows)))]
pub(super) type TimeT = i64;
const TIME_T_MIN: i64 = TimeT::MIN as _;
const TIME_T_MAX: i64 = TimeT::MAX as _;
const PYTIME_OVERFLOW: &str = "timestamp out of range for C PyTime_t";
const TIME_T_OVERFLOW: &str = "timestamp out of range for platform time_t";
#[derive(Clone, Copy)]
pub(super) enum Round {
Floor,
Ceiling,
HalfEven,
Up,
}
impl Round {
pub(super) fn from_int(value: i32, vm: &VirtualMachine) -> PyResult<Self> {
match value {
0 => Ok(Self::Floor),
1 => Ok(Self::Ceiling),
2 => Ok(Self::HalfEven),
3 => Ok(Self::Up),
_ => Err(vm.new_value_error("invalid rounding")),
}
}
fn round(self, value: f64) -> f64 {
match self {
Self::Floor => value.floor(),
Self::Ceiling => value.ceil(),
Self::HalfEven => value.round_ties_even(),
Self::Up if value >= 0.0 => value.ceil(),
Self::Up => value.floor(),
}
}
pub(super) fn divide(self, value: i64, divisor: i64) -> i64 {
debug_assert!(divisor > 1);
let quotient = value / divisor;
let remainder = value % divisor;
let round_away = match self {
Self::Floor => remainder < 0,
Self::Ceiling => remainder > 0,
Self::HalfEven => {
let remainder = remainder.abs();
remainder > divisor / 2
|| (divisor % 2 == 0 && remainder == divisor / 2 && quotient % 2 != 0)
}
Self::Up => remainder != 0,
};
// Adjust the quotient, not the input: adding to i64::MAX or
// subtracting from i64::MIN before dividing would overflow.
quotient + if round_away { value.signum() } else { 0 }
}
}
pub(super) fn from_seconds(seconds: i32) -> i64 {
i64::from(seconds) * SEC_TO_NS
}
fn index_seconds(object: &PyObject, message: &str, vm: &VirtualMachine) -> PyResult<i64> {
let seconds = object.try_index(vm).map_err(|err| {
if err.fast_isinstance(vm.ctx.exceptions.overflow_error) {
vm.new_overflow_error(message)
} else {
err
}
})?;
seconds
.as_bigint()
.to_i64()
.ok_or_else(|| vm.new_overflow_error(message))
}
fn float_seconds(object: &PyObject, vm: &VirtualMachine) -> PyResult<f64> {
// PyFloat_AsDouble bypasses __float__ overrides on float subclasses.
let seconds = match object.downcast_ref::<PyFloat>() {
Some(float) => float.to_f64(),
None => object.try_float(vm)?.to_f64(),
};
if seconds.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
}
Ok(seconds)
}
pub(super) fn from_seconds_object(
object: &PyObject,
round: Round,
vm: &VirtualMachine,
) -> PyResult<i64> {
// _PyTime_FromSecondsObject uses __index__ before __float__ and bounds
// the converted value to signed 64-bit nanoseconds.
if object.number().is_index() {
index_seconds(object, PYTIME_OVERFLOW, vm)?
.checked_mul(SEC_TO_NS)
.ok_or_else(|| vm.new_overflow_error(PYTIME_OVERFLOW))
} else {
let nanoseconds = round.round(float_seconds(object, vm)? * SEC_TO_NS as f64);
let minimum = i64::MIN as f64;
// The maximum rounds up to 2**63 as a double, so use an exclusive bound.
if !(minimum..-minimum).contains(&nanoseconds) {
return Err(vm.new_overflow_error(PYTIME_OVERFLOW));
}
Ok(nanoseconds as i64)
}
}
fn check_time_t(seconds: i64, vm: &VirtualMachine) -> PyResult<i64> {
if !(TIME_T_MIN..=TIME_T_MAX).contains(&seconds) {
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
}
Ok(seconds)
}
fn float_to_time_t(seconds: f64, vm: &VirtualMachine) -> PyResult<i64> {
let minimum = TIME_T_MIN as f64;
if !(minimum..-minimum).contains(&seconds) {
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
}
Ok(seconds as i64)
}
pub(super) fn object_to_time_t(
object: &PyObject,
round: Round,
vm: &VirtualMachine,
) -> PyResult<i64> {
if object.number().is_index() {
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)
} else {
float_to_time_t(round.round(float_seconds(object, vm)?), vm)
}
}
pub(super) fn object_to_denominator(
object: &PyObject,
denominator: i64,
round: Round,
vm: &VirtualMachine,
) -> PyResult<(i64, i64)> {
if object.number().is_index() {
return Ok((
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)?,
0,
));
}
let value = float_seconds(object, vm)?;
let mut seconds = value.trunc();
let denominator = denominator as f64;
let mut numerator = round.round(value.fract() * denominator);
if numerator >= denominator {
numerator -= denominator;
seconds += 1.0;
} else if numerator < 0.0 {
numerator += denominator;
seconds -= 1.0;
}
Ok((float_to_time_t(seconds, vm)?, numerator as i64))
}