From d55a2261fa76b03ea988dd39691203f2f51e7109 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 11 Feb 2026 01:11:39 +0900 Subject: [PATCH 1/2] Use libc time functions on unix for time module - Replace chrono-based gmtime/localtime/mktime/strftime/asctime/ctime with direct libc calls (gmtime_r, localtime_r, mktime, strftime) on unix, keeping chrono as fallback for non-unix platforms - Add checked_tm_from_struct_time for struct_time field validation - Accept None in gmtime/localtime/ctime (OptionalArg>) - Fix StructTimeData field order: tm_zone before tm_gmtoff - Fix PyStructTime slot_new to accept optional dict argument --- crates/vm/src/stdlib/time.rs | 567 +++++++++++++++++++++++++++++++---- 1 file changed, 502 insertions(+), 65 deletions(-) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index d41e0370464..eb6265a37cc 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -27,10 +27,15 @@ unsafe extern "C" { mod decl { use crate::{ AsObject, Py, PyObjectRef, PyResult, VirtualMachine, - builtins::{PyStrRef, PyTypeRef, PyUtf8StrRef}, + builtins::{PyStrRef, PyTypeRef}, function::{Either, FuncArgs, OptionalArg}, types::{PyStructSequence, struct_sequence_new}, }; + #[cfg(unix)] + use crate::{common::wtf8::Wtf8Buf, convert::ToPyObject}; + #[cfg(unix)] + use alloc::ffi::CString; + #[cfg(not(unix))] use chrono::{ DateTime, Datelike, TimeZone, Timelike, naive::{NaiveDate, NaiveDateTime, NaiveTime}, @@ -82,6 +87,7 @@ mod decl { #[pyfunction] fn sleep(seconds: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let seconds_type_name = seconds.clone().class().name().to_owned(); let dur = seconds.try_into_value::(vm).map_err(|e| { if e.class().is(vm.ctx.exceptions.value_error) && let Some(s) = e.args().first().and_then(|arg| arg.str(vm).ok()) @@ -89,6 +95,11 @@ mod decl { { return vm.new_value_error("sleep length must be non-negative"); } + if e.class().is(vm.ctx.exceptions.type_error) { + return vm.new_type_error(format!( + "'{seconds_type_name}' object cannot be interpreted as an integer or float" + )); + } e })?; @@ -269,40 +280,188 @@ mod decl { tz_name.into_pytuple(vm) } + #[cfg(not(unix))] fn pyobj_to_date_time( value: Either, vm: &VirtualMachine, ) -> PyResult> { - let timestamp = match value { + let secs = match value { Either::A(float) => { - let secs = float.trunc() as i64; - let nano_secs = (float.fract() * 1e9) as u32; - DateTime::::from_timestamp(secs, nano_secs) + if !float.is_finite() { + return Err(vm.new_value_error("Invalid value for timestamp")); + } + float.floor() as i64 } - Either::B(int) => DateTime::::from_timestamp(int, 0), + Either::B(int) => int, }; - timestamp.ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t")) + DateTime::::from_timestamp(secs, 0) + .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t")) } - impl OptionalArg> { + #[cfg(not(unix))] + impl OptionalArg>> { /// Construct a localtime from the optional seconds, or get the current local time. fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { - Self::Present(secs) => pyobj_to_date_time(secs, vm)? + Self::Present(Some(secs)) => pyobj_to_date_time(secs, vm)? .with_timezone(&chrono::Local) .naive_local(), - Self::Missing => chrono::offset::Local::now().naive_local(), + Self::Present(None) | Self::Missing => chrono::offset::Local::now().naive_local(), }) } + } - fn naive_or_utc(self, vm: &VirtualMachine) -> PyResult { - Ok(match self { - Self::Present(secs) => pyobj_to_date_time(secs, vm)?.naive_utc(), - Self::Missing => chrono::offset::Utc::now().naive_utc(), - }) + #[cfg(unix)] + struct CheckedTm { + tm: libc::tm, + zone: Option, + } + + #[cfg(unix)] + fn checked_tm_from_struct_time( + t: &StructTimeData, + vm: &VirtualMachine, + func_name: &'static str, + ) -> PyResult { + let invalid_tuple = + || vm.new_type_error(format!("{func_name}(): illegal time tuple argument")); + + let year: i64 = t.tm_year.clone().try_into_value(vm).map_err(|e| { + if e.class().is(vm.ctx.exceptions.overflow_error) { + vm.new_overflow_error("year out of range") + } else { + invalid_tuple() + } + })?; + if year < i64::from(i32::MIN) + 1900 || year > i64::from(i32::MAX) { + return Err(vm.new_overflow_error("year out of range")); + } + let year = year as i32; + let mut tm = libc::tm { + tm_year: year - 1900, + tm_mon: t + .tm_mon + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1, + tm_mday: t + .tm_mday + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_hour: t + .tm_hour + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_min: t + .tm_min + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_sec: t + .tm_sec + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_wday: (t + .tm_wday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + + 1) + % 7, + tm_yday: t + .tm_yday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1, + tm_isdst: t + .tm_isdst + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_gmtoff: 0, + tm_zone: core::ptr::null_mut(), + }; + + if tm.tm_mon == -1 { + tm.tm_mon = 0; + } else if tm.tm_mon < 0 || tm.tm_mon > 11 { + return Err(vm.new_value_error("month out of range")); + } + if tm.tm_mday == 0 { + tm.tm_mday = 1; + } else if tm.tm_mday < 0 || tm.tm_mday > 31 { + return Err(vm.new_value_error("day of month out of range")); + } + if tm.tm_hour < 0 || tm.tm_hour > 23 { + return Err(vm.new_value_error("hour out of range")); } + if tm.tm_min < 0 || tm.tm_min > 59 { + return Err(vm.new_value_error("minute out of range")); + } + if tm.tm_sec < 0 || tm.tm_sec > 61 { + return Err(vm.new_value_error("seconds out of range")); + } + if tm.tm_wday < 0 { + return Err(vm.new_value_error("day of week out of range")); + } + if tm.tm_yday == -1 { + tm.tm_yday = 0; + } else if tm.tm_yday < 0 || tm.tm_yday > 365 { + return Err(vm.new_value_error("day of year out of range")); + } + + let zone = if t.tm_zone.is(&vm.ctx.none) { + None + } else { + let zone: PyStrRef = t + .tm_zone + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + Some( + CString::new(zone.as_str()) + .map_err(|_| vm.new_value_error("embedded null character"))?, + ) + }; + if let Some(zone) = &zone { + tm.tm_zone = zone.as_ptr().cast_mut(); + } + if !t.tm_gmtoff.is(&vm.ctx.none) { + let gmtoff: i64 = t + .tm_gmtoff + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_gmtoff = gmtoff as _; + } + + Ok(CheckedTm { tm, zone }) + } + + #[cfg(unix)] + fn asctime_from_tm(tm: &libc::tm) -> String { + const WDAY_NAME: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const MON_NAME: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + format!( + "{} {}{:>3} {:02}:{:02}:{:02} {}", + WDAY_NAME[tm.tm_wday as usize], + MON_NAME[tm.tm_mon as usize], + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + tm.tm_year + 1900 + ) } + #[cfg(not(unix))] impl OptionalArg { fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { @@ -315,78 +474,218 @@ mod decl { /// https://docs.python.org/3/library/time.html?highlight=gmtime#time.gmtime #[pyfunction] fn gmtime( - secs: OptionalArg>, + secs: OptionalArg>>, vm: &VirtualMachine, ) -> PyResult { - let instant = secs.naive_or_utc(vm)?; - Ok(StructTimeData::new_utc(vm, instant)) + #[cfg(unix)] + { + let ts = match secs { + OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, + OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(), + }; + gmtime_from_timestamp(ts, vm) + } + + #[cfg(not(unix))] + { + let instant = match secs { + OptionalArg::Present(Some(secs)) => pyobj_to_date_time(secs, vm)?.naive_utc(), + OptionalArg::Present(None) | OptionalArg::Missing => { + chrono::offset::Utc::now().naive_utc() + } + }; + Ok(StructTimeData::new_utc(vm, instant)) + } } #[pyfunction] fn localtime( - secs: OptionalArg>, + secs: OptionalArg>>, vm: &VirtualMachine, ) -> PyResult { + #[cfg(unix)] + { + let ts = match secs { + OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, + OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(), + }; + localtime_from_timestamp(ts, vm) + } + + #[cfg(not(unix))] let instant = secs.naive_or_local(vm)?; - // TODO: isdst flag must be valid value here - // https://docs.python.org/3/library/time.html#time.localtime - Ok(StructTimeData::new_local(vm, instant, -1)) + #[cfg(not(unix))] + { + Ok(StructTimeData::new_local(vm, instant, 0)) + } } #[pyfunction] fn mktime(t: StructTimeData, vm: &VirtualMachine) -> PyResult { - let datetime = t.to_date_time(vm)?; - // mktime interprets struct_time as local time - let local_dt = chrono::Local - .from_local_datetime(&datetime) - .single() - .ok_or_else(|| vm.new_overflow_error("mktime argument out of range"))?; - let seconds_since_epoch = local_dt.timestamp() as f64; - Ok(seconds_since_epoch) + #[cfg(unix)] + { + unix_mktime(&t, vm) + } + + #[cfg(not(unix))] + { + let datetime = t.to_date_time(vm)?; + // mktime interprets struct_time as local time + let local_dt = chrono::Local + .from_local_datetime(&datetime) + .single() + .ok_or_else(|| vm.new_overflow_error("mktime argument out of range"))?; + let seconds_since_epoch = local_dt.timestamp() as f64; + Ok(seconds_since_epoch) + } } + #[cfg(not(unix))] const CFMT: &str = "%a %b %e %H:%M:%S %Y"; #[pyfunction] fn asctime(t: OptionalArg, vm: &VirtualMachine) -> PyResult { - let instant = t.naive_or_local(vm)?; - let formatted_time = instant.format(CFMT).to_string(); - Ok(vm.ctx.new_str(formatted_time).into()) + #[cfg(unix)] + { + let tm = match t { + OptionalArg::Present(value) => { + checked_tm_from_struct_time(&value, vm, "asctime")?.tm + } + OptionalArg::Missing => { + let now = current_time_t(); + let local = localtime_from_timestamp(now, vm)?; + checked_tm_from_struct_time(&local, vm, "asctime")?.tm + } + }; + Ok(vm.ctx.new_str(asctime_from_tm(&tm)).into()) + } + + #[cfg(not(unix))] + { + let instant = t.naive_or_local(vm)?; + let formatted_time = instant.format(CFMT).to_string(); + Ok(vm.ctx.new_str(formatted_time).into()) + } } #[pyfunction] - fn ctime(secs: OptionalArg>, vm: &VirtualMachine) -> PyResult { - let instant = secs.naive_or_local(vm)?; - Ok(instant.format(CFMT).to_string()) + fn ctime(secs: OptionalArg>>, vm: &VirtualMachine) -> PyResult { + #[cfg(unix)] + { + let ts = match secs { + OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, + OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(), + }; + let local = localtime_from_timestamp(ts, vm)?; + let tm = checked_tm_from_struct_time(&local, vm, "asctime")?.tm; + Ok(asctime_from_tm(&tm)) + } + + #[cfg(not(unix))] + { + let instant = secs.naive_or_local(vm)?; + Ok(instant.format(CFMT).to_string()) + } } #[pyfunction] - fn strftime( - format: PyUtf8StrRef, - t: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { - use core::fmt::Write; - - let instant = t.naive_or_local(vm)?; + fn strftime(format: PyStrRef, t: OptionalArg, vm: &VirtualMachine) -> PyResult { + #[cfg(unix)] + { + let checked_tm = match t { + OptionalArg::Present(value) => checked_tm_from_struct_time(&value, vm, "strftime")?, + OptionalArg::Missing => { + let now = current_time_t(); + let local = localtime_from_timestamp(now, vm)?; + checked_tm_from_struct_time(&local, vm, "strftime")? + } + }; + let _keep_zone_alive = &checked_tm.zone; + let mut tm = checked_tm.tm; + tm.tm_isdst = tm.tm_isdst.clamp(-1, 1); + + fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult { + let fmt_c = + CString::new(fmt).map_err(|_| vm.new_value_error("embedded null character"))?; + let mut size = 1024usize; + let max_scale = 256usize.saturating_mul(fmt.len().max(1)); + loop { + let mut out = vec![0u8; size]; + let written = unsafe { + libc::strftime( + out.as_mut_ptr().cast(), + out.len(), + fmt_c.as_ptr(), + tm as *const libc::tm, + ) + }; + if written > 0 || size >= max_scale { + return Ok(String::from_utf8_lossy(&out[..written]).into_owned()); + } + size = size.saturating_mul(2); + } + } - // On Windows/AIX/Solaris, %y format with year < 1900 is not supported - #[cfg(any(windows, target_os = "aix", target_os = "solaris"))] - if instant.year() < 1900 && format.as_str().contains("%y") { - let msg = "format %y requires year >= 1900 on Windows"; - return Err(vm.new_value_error(msg.to_owned())); + let mut out = Wtf8Buf::new(); + let mut ascii = String::new(); + + for codepoint in format.as_wtf8().code_points() { + if codepoint.to_u32() == 0 { + if !ascii.is_empty() { + let part = strftime_ascii(&ascii, &tm, vm)?; + out.extend(part.chars()); + ascii.clear(); + } + out.push(codepoint); + continue; + } + if let Some(ch) = codepoint.to_char() + && ch.is_ascii() + { + ascii.push(ch); + continue; + } + + if !ascii.is_empty() { + let part = strftime_ascii(&ascii, &tm, vm)?; + out.extend(part.chars()); + ascii.clear(); + } + out.push(codepoint); + } + if !ascii.is_empty() { + let part = strftime_ascii(&ascii, &tm, vm)?; + out.extend(part.chars()); + } + Ok(out.to_pyobject(vm)) } - let mut formatted_time = String::new(); + #[cfg(not(unix))] + { + use core::fmt::Write; + + let fmt_lossy = format.to_string_lossy(); + + // If the struct_time can't be represented as NaiveDateTime + // (e.g. month=0), return the format string as-is, matching + // the fallback behavior for unsupported chrono formats. + let instant = match t.naive_or_local(vm) { + Ok(dt) => dt, + Err(_) => return Ok(vm.ctx.new_str(fmt_lossy.into_owned()).into()), + }; + + // On Windows/AIX/Solaris, %y format with year < 1900 is not supported + #[cfg(any(windows, target_os = "aix", target_os = "solaris"))] + if instant.year() < 1900 && fmt_lossy.contains("%y") { + let msg = "format %y requires year >= 1900 on Windows"; + return Err(vm.new_value_error(msg.to_owned())); + } - /* - * chrono doesn't support all formats and it - * raises an error if unsupported format is supplied. - * If error happens, we set result as input arg. - */ - write!(&mut formatted_time, "{}", instant.format(format.as_str())) - .unwrap_or_else(|_| formatted_time = format.to_string()); - Ok(vm.ctx.new_str(formatted_time).into()) + let mut formatted_time = String::new(); + write!(&mut formatted_time, "{}", instant.format(&fmt_lossy)) + .unwrap_or_else(|_| formatted_time = format.to_string()); + Ok(vm.ctx.new_str(formatted_time).into()) + } } #[pyfunction] @@ -491,9 +790,9 @@ mod decl { pub tm_yday: PyObjectRef, pub tm_isdst: PyObjectRef, #[pystruct_sequence(skip)] - pub tm_gmtoff: PyObjectRef, - #[pystruct_sequence(skip)] pub tm_zone: PyObjectRef, + #[pystruct_sequence(skip)] + pub tm_gmtoff: PyObjectRef, } impl core::fmt::Debug for StructTimeData { @@ -503,6 +802,7 @@ mod decl { } impl StructTimeData { + #[cfg(not(unix))] fn new_inner( vm: &VirtualMachine, tm: NaiveDateTime, @@ -520,25 +820,27 @@ mod decl { tm_wday: vm.ctx.new_int(tm.weekday().num_days_from_monday()).into(), tm_yday: vm.ctx.new_int(tm.ordinal()).into(), tm_isdst: vm.ctx.new_int(isdst).into(), - tm_gmtoff: vm.ctx.new_int(gmtoff).into(), tm_zone: vm.ctx.new_str(zone).into(), + tm_gmtoff: vm.ctx.new_int(gmtoff).into(), } } /// Create struct_time for UTC (gmtime) + #[cfg(not(unix))] fn new_utc(vm: &VirtualMachine, tm: NaiveDateTime) -> Self { Self::new_inner(vm, tm, 0, 0, "UTC") } /// Create struct_time for local timezone (localtime) + #[cfg(not(unix))] fn new_local(vm: &VirtualMachine, tm: NaiveDateTime, isdst: i32) -> Self { let local_time = chrono::Local.from_local_datetime(&tm).unwrap(); - let offset_seconds = - local_time.offset().local_minus_utc() + if isdst == 1 { 3600 } else { 0 }; + let offset_seconds = local_time.offset().local_minus_utc(); let tz_abbr = local_time.format("%Z").to_string(); Self::new_inner(vm, tm, isdst, offset_seconds, &tz_abbr) } + #[cfg(not(unix))] fn to_date_time(&self, vm: &VirtualMachine) -> PyResult { let invalid_overflow = || vm.new_overflow_error("mktime argument out of range"); let invalid_value = || vm.new_value_error("invalid struct_time parameter"); @@ -566,7 +868,7 @@ mod decl { impl PyStructTime { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let seq: PyObjectRef = args.bind(vm)?; + let (seq, _dict): (PyObjectRef, OptionalArg) = args.bind(vm)?; struct_sequence_new(cls, seq, vm) } } @@ -594,14 +896,16 @@ mod decl { #[pymodule(sub)] mod platform { #[allow(unused_imports)] - use super::decl::{SEC_TO_NS, US_TO_NS}; + use super::decl::{SEC_TO_NS, StructTimeData, US_TO_NS}; #[cfg_attr(target_os = "macos", allow(unused_imports))] use crate::{ PyObject, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine, builtins::{PyNamespace, PyStrRef}, convert::IntoPyException, + function::Either, }; use core::time::Duration; + use libc::time_t; use nix::{sys::time::TimeSpec, time::ClockId}; #[cfg(target_os = "solaris")] @@ -640,6 +944,139 @@ mod platform { } } + pub(super) fn pyobj_to_time_t( + value: Either, + vm: &VirtualMachine, + ) -> PyResult { + let secs = match value { + Either::A(float) => { + if !float.is_finite() { + return Err(vm.new_value_error("Invalid value for timestamp")); + } + float.floor() + } + Either::B(int) => int as f64, + }; + if secs < time_t::MIN as f64 || secs > time_t::MAX as f64 { + return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); + } + Ok(secs as time_t) + } + + fn struct_time_from_tm(vm: &VirtualMachine, tm: libc::tm) -> StructTimeData { + let zone = unsafe { + if tm.tm_zone.is_null() { + String::new() + } else { + core::ffi::CStr::from_ptr(tm.tm_zone) + .to_string_lossy() + .into_owned() + } + }; + StructTimeData { + tm_year: vm.ctx.new_int(tm.tm_year + 1900).into(), + tm_mon: vm.ctx.new_int(tm.tm_mon + 1).into(), + tm_mday: vm.ctx.new_int(tm.tm_mday).into(), + tm_hour: vm.ctx.new_int(tm.tm_hour).into(), + tm_min: vm.ctx.new_int(tm.tm_min).into(), + tm_sec: vm.ctx.new_int(tm.tm_sec).into(), + tm_wday: vm.ctx.new_int((tm.tm_wday + 6) % 7).into(), + tm_yday: vm.ctx.new_int(tm.tm_yday + 1).into(), + tm_isdst: vm.ctx.new_int(tm.tm_isdst).into(), + tm_zone: vm.ctx.new_str(zone).into(), + tm_gmtoff: vm.ctx.new_int(tm.tm_gmtoff).into(), + } + } + + pub(super) fn current_time_t() -> time_t { + unsafe { libc::time(core::ptr::null_mut()) } + } + + pub(super) fn gmtime_from_timestamp( + when: time_t, + vm: &VirtualMachine, + ) -> PyResult { + let mut out = core::mem::MaybeUninit::::uninit(); + let ret = unsafe { libc::gmtime_r(&when, out.as_mut_ptr()) }; + if ret.is_null() { + return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); + } + Ok(struct_time_from_tm(vm, unsafe { out.assume_init() })) + } + + pub(super) fn localtime_from_timestamp( + when: time_t, + vm: &VirtualMachine, + ) -> PyResult { + let mut out = core::mem::MaybeUninit::::uninit(); + let ret = unsafe { libc::localtime_r(&when, out.as_mut_ptr()) }; + if ret.is_null() { + return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); + } + Ok(struct_time_from_tm(vm, unsafe { out.assume_init() })) + } + + pub(super) fn unix_mktime(t: &StructTimeData, vm: &VirtualMachine) -> PyResult { + let invalid_tuple = || vm.new_type_error("mktime(): illegal time tuple argument"); + let year: i32 = t + .tm_year + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + if year < i32::MIN + 1900 { + return Err(vm.new_overflow_error("year out of range")); + } + + let mut tm = libc::tm { + tm_sec: t + .tm_sec + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_min: t + .tm_min + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_hour: t + .tm_hour + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_mday: t + .tm_mday + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_mon: t + .tm_mon + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1, + tm_year: year - 1900, + tm_wday: -1, + tm_yday: t + .tm_yday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1, + tm_isdst: t + .tm_isdst + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?, + tm_gmtoff: 0, + tm_zone: core::ptr::null_mut(), + }; + let timestamp = unsafe { libc::mktime(&mut tm) }; + if timestamp == -1 && tm.tm_wday == -1 { + return Err(vm.new_overflow_error("mktime argument out of range")); + } + Ok(timestamp as f64) + } + fn get_clock_time(clk_id: ClockId, vm: &VirtualMachine) -> PyResult { let ts = nix::time::clock_gettime(clk_id).map_err(|e| e.into_pyexception(vm))?; Ok(ts.into()) From c4f23295bfeac8fadff39ce6dfc6ca1730806bfe Mon Sep 17 00:00:00 2001 From: CPython Developers <> Date: Wed, 11 Feb 2026 01:13:00 +0900 Subject: [PATCH 2/2] Update test_time from v3.14.3 --- Lib/test/datetimetester.py | 2 +- Lib/test/test_support.py | 1 - Lib/test/test_time.py | 344 ++++++++++++++++++++++++------------- 3 files changed, 230 insertions(+), 117 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 8352b69f7b2..af11a611887 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1519,7 +1519,7 @@ def test_strftime(self): # bpo-41260: The parameter was named "fmt" in the pure python impl. t.strftime(format="%f") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON; chrono fallback on Windows") def test_strftime_trailing_percent(self): # bpo-35066: Make sure trailing '%' doesn't cause datetime's strftime to # complain. Different libcs have different handling of trailing diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index caf36cf61a9..37b5543badf 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -614,7 +614,6 @@ def test_print_warning(self): self.check_print_warning("a\nb", 'Warning -- a\nWarning -- b\n') - @unittest.expectedFailureIf(sys.platform != "win32", "TODO: RUSTPYTHON; no has_strftime_extensions yet") def test_has_strftime_extensions(self): if sys.platform == "win32": self.assertFalse(support.has_strftime_extensions) diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 35055e03b8e..1464aaffa86 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -2,7 +2,6 @@ from test.support import warnings_helper import decimal import enum -import locale import math import platform import sys @@ -14,8 +13,12 @@ import _testcapi except ImportError: _testcapi = None +try: + import _testinternalcapi +except ImportError: + _testinternalcapi = None -from test.support import skip_if_buggy_ucrt_strfptime +from test.support import skip_if_buggy_ucrt_strfptime, SuppressCrashReport # Max year is only limited by the size of C int. SIZEOF_INT = sysconfig.get_config_var('SIZEOF_INT') or 4 @@ -38,6 +41,10 @@ class _PyTime(enum.IntEnum): # Round away from zero ROUND_UP = 3 +# _PyTime_t is int64_t +PyTime_MIN = -2 ** 63 +PyTime_MAX = 2 ** 63 - 1 + # Rounding modes supported by PyTime ROUNDING_MODES = ( # (PyTime rounding method, decimal rounding method) @@ -109,6 +116,7 @@ def test_clock_monotonic(self): 'need time.pthread_getcpuclockid()') @unittest.skipUnless(hasattr(time, 'clock_gettime'), 'need time.clock_gettime()') + @unittest.skipIf(support.is_emscripten, "Fails to find clock") def test_pthread_getcpuclockid(self): clk_id = time.pthread_getcpuclockid(threading.get_ident()) self.assertTrue(type(clk_id) is int) @@ -150,13 +158,32 @@ def test_conversions(self): self.assertEqual(int(time.mktime(time.localtime(self.t))), int(self.t)) - def test_sleep(self): + def test_sleep_exceptions(self): + self.assertRaises(TypeError, time.sleep, []) + self.assertRaises(TypeError, time.sleep, "a") + self.assertRaises(TypeError, time.sleep, complex(0, 0)) + self.assertRaises(ValueError, time.sleep, -2) self.assertRaises(ValueError, time.sleep, -1) - time.sleep(1.2) + self.assertRaises(ValueError, time.sleep, -0.1) + + # Improved exception #81267 + with self.assertRaises(TypeError) as errmsg: + time.sleep([]) + self.assertIn("integer or float", str(errmsg.exception)) + + def test_sleep(self): + for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2]: + with self.subTest(value=value): + time.sleep(value) + + def test_epoch(self): + # bpo-43869: Make sure that Python use the same Epoch on all platforms: + # January 1, 1970, 00:00:00 (UTC). + epoch = time.gmtime(0) + # Only test the date and time, ignore other gmtime() members + self.assertEqual(tuple(epoch)[:6], (1970, 1, 1, 0, 0, 0), epoch) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_strftime(self): tt = time.gmtime(self.t) for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'H', 'I', @@ -169,8 +196,46 @@ def test_strftime(self): self.fail('conversion specifier: %r failed.' % format) self.assertRaises(TypeError, time.strftime, b'%S', tt) - # embedded null character - self.assertRaises(ValueError, time.strftime, '%S\0', tt) + + def test_strftime_invalid_format(self): + tt = time.gmtime(self.t) + with SuppressCrashReport(): + for i in range(1, 128): + format = ' %' + chr(i) + with self.subTest(format=format): + try: + time.strftime(format, tt) + except ValueError as exc: + self.assertEqual(str(exc), 'Invalid format string') + + # TODO: RUSTPYTHON; chrono fallback on Windows does not preserve surrogates + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_strftime_special(self): + tt = time.gmtime(self.t) + s1 = time.strftime('%c', tt) + s2 = time.strftime('%B', tt) + # gh-52551, gh-78662: Unicode strings should pass through strftime, + # independently from locale. + self.assertEqual(time.strftime('\U0001f40d', tt), '\U0001f40d') + self.assertEqual(time.strftime('\U0001f4bb%c\U0001f40d%B', tt), f'\U0001f4bb{s1}\U0001f40d{s2}') + self.assertEqual(time.strftime('%c\U0001f4bb%B\U0001f40d', tt), f'{s1}\U0001f4bb{s2}\U0001f40d') + # Lone surrogates should pass through. + self.assertEqual(time.strftime('\ud83d', tt), '\ud83d') + self.assertEqual(time.strftime('\udc0d', tt), '\udc0d') + self.assertEqual(time.strftime('\ud83d%c\udc0d%B', tt), f'\ud83d{s1}\udc0d{s2}') + self.assertEqual(time.strftime('%c\ud83d%B\udc0d', tt), f'{s1}\ud83d{s2}\udc0d') + self.assertEqual(time.strftime('%c\udc0d%B\ud83d', tt), f'{s1}\udc0d{s2}\ud83d') + # Surrogate pairs should not recombine. + self.assertEqual(time.strftime('\ud83d\udc0d', tt), '\ud83d\udc0d') + self.assertEqual(time.strftime('%c\ud83d\udc0d%B', tt), f'{s1}\ud83d\udc0d{s2}') + # Surrogate-escaped bytes should not recombine. + self.assertEqual(time.strftime('\udcf0\udc9f\udc90\udc8d', tt), '\udcf0\udc9f\udc90\udc8d') + self.assertEqual(time.strftime('%c\udcf0\udc9f\udc90\udc8d%B', tt), f'{s1}\udcf0\udc9f\udc90\udc8d{s2}') + # gh-124531: The null character should not terminate the format string. + self.assertEqual(time.strftime('\0', tt), '\0') + self.assertEqual(time.strftime('\0'*1000, tt), '\0'*1000) + self.assertEqual(time.strftime('\0%c\0%B', tt), f'\0{s1}\0{s2}') + self.assertEqual(time.strftime('%c\0%B\0', tt), f'{s1}\0{s2}\0') def _bounds_checking(self, func): # Make sure that strftime() checks the bounds of the various parts @@ -229,8 +294,8 @@ def _bounds_checking(self, func): self.assertRaises(ValueError, func, (1900, 1, 1, 0, 0, 0, 0, 367, -1)) - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono on Windows rejects month=0/day=0 and raises wrong error type + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_strftime_bounding_check(self): self._bounds_checking(lambda tup: time.strftime('', tup)) @@ -247,8 +312,8 @@ def test_strftime_format_check(self): except ValueError: pass - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono on Windows does not handle month=0/day=0 in struct_time + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_default_values_for_zero(self): # Make sure that using all zeros uses the proper default # values. No test for daylight savings since strftime() does @@ -259,8 +324,8 @@ def test_default_values_for_zero(self): result = time.strftime("%Y %m %d %H %M %S %w %j", (2000,)+(0,)*8) self.assertEqual(expected, result) - # TODO: RUSTPYTHON - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono %Z on Windows not compatible with strptime + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") @skip_if_buggy_ucrt_strfptime def test_strptime(self): # Should be able to go round-trip from strftime to strptime without @@ -270,6 +335,8 @@ def test_strptime(self): 'j', 'm', 'M', 'p', 'S', 'U', 'w', 'W', 'x', 'X', 'y', 'Y', 'Z', '%'): format = '%' + directive + if directive == 'd': + format += ',%Y' # Avoid GH-70647. strf_output = time.strftime(format, tt) try: time.strptime(strf_output, format) @@ -286,14 +353,20 @@ def test_strptime_exception_context(self): # check that this doesn't chain exceptions needlessly (see #17572) with self.assertRaises(ValueError) as e: time.strptime('', '%D') - self.assertIs(e.exception.__suppress_context__, True) - # additional check for IndexError branch (issue #19545) + self.assertTrue(e.exception.__suppress_context__) + # additional check for stray % branch with self.assertRaises(ValueError) as e: - time.strptime('19', '%Y %') - self.assertIs(e.exception.__suppress_context__, True) + time.strptime('%', '%') + self.assertTrue(e.exception.__suppress_context__) + + def test_strptime_leap_year(self): + # GH-70647: warns if parsing a format with a day and no year. + with self.assertWarnsRegex(DeprecationWarning, + r'.*day of month without a year.*'): + time.strptime('02-07 18:28', '%m-%d %H:%M') - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono on Windows cannot handle month=0/big years + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_asctime(self): time.asctime(time.gmtime(self.t)) @@ -309,13 +382,13 @@ def test_asctime(self): self.assertRaises(TypeError, time.asctime, ()) self.assertRaises(TypeError, time.asctime, (0,) * 10) - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono on Windows rejects month=0/day=0 + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_asctime_bounding_check(self): self._bounds_checking(time.asctime) - # TODO: RUSTPYTHON - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono on Windows formats negative years differently + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_ctime(self): t = time.mktime((1973, 9, 16, 1, 3, 52, 0, 0, -1)) self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973') @@ -415,8 +488,6 @@ def test_insane_timestamps(self): for unreasonable in -1e200, 1e200: self.assertRaises(OverflowError, func, unreasonable) - # TODO: RUSTPYTHON, TypeError: unexpected type NoneType - @unittest.expectedFailure def test_ctime_without_arg(self): # Not sure how to check the values, since the clock could tick # at any time. Make sure these are at least accepted and @@ -424,8 +495,6 @@ def test_ctime_without_arg(self): time.ctime() time.ctime(None) - # TODO: RUSTPYTHON, TypeError: unexpected type NoneType - @unittest.expectedFailure def test_gmtime_without_arg(self): gt0 = time.gmtime() gt1 = time.gmtime(None) @@ -433,8 +502,6 @@ def test_gmtime_without_arg(self): t1 = time.mktime(gt1) self.assertAlmostEqual(t1, t0, delta=0.2) - # TODO: RUSTPYTHON, TypeError: unexpected type NoneType - @unittest.expectedFailure def test_localtime_without_arg(self): lt0 = time.localtime() lt1 = time.localtime(None) @@ -469,8 +536,6 @@ def test_mktime_error(self): pass self.assertEqual(time.strftime('%Z', tt), tzname) - # TODO: RUSTPYTHON - @unittest.skipIf(sys.platform == "win32", "Implement get_clock_info for Windows.") def test_monotonic(self): # monotonic() should not go backward times = [time.monotonic() for n in range(100)] @@ -497,6 +562,12 @@ def test_monotonic(self): def test_perf_counter(self): time.perf_counter() + @unittest.skipIf( + support.is_wasi, "process_time not available on WASI" + ) + @unittest.skipIf( + support.is_emscripten, "process_time present but doesn't exclude sleep" + ) def test_process_time(self): # process_time() should not include time spend during a sleep start = time.process_time() @@ -512,7 +583,7 @@ def test_process_time(self): def test_thread_time(self): if not hasattr(time, 'thread_time'): - if sys.platform.startswith(('linux', 'win')): + if sys.platform.startswith(('linux', 'android', 'win')): self.fail("time.thread_time() should be available on %r" % (sys.platform,)) else: @@ -520,11 +591,10 @@ def test_thread_time(self): # thread_time() should not include time spend during a sleep start = time.thread_time() - time.sleep(0.100) + time.sleep(0.200) stop = time.thread_time() - # use 20 ms because thread_time() has usually a resolution of 15 ms - # on Windows - self.assertLess(stop - start, 0.020) + # gh-143528: use 100 ms to support slow CI + self.assertLess(stop - start, 0.100) info = time.get_clock_info('thread_time') self.assertTrue(info.monotonic) @@ -572,8 +642,9 @@ def test_get_clock_info(self): 'perf_counter', 'process_time', 'time', - 'thread_time', ] + if hasattr(time, 'thread_time'): + clocks.append('thread_time') for name in clocks: with self.subTest(name=name): @@ -592,17 +663,8 @@ def test_get_clock_info(self): class TestLocale(unittest.TestCase): - def setUp(self): - self.oldloc = locale.setlocale(locale.LC_ALL) - - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.oldloc) - + @support.run_with_locale('LC_ALL', 'fr_FR', '') def test_bug_3061(self): - try: - tmp = locale.setlocale(locale.LC_ALL, "fr_FR") - except locale.Error: - self.skipTest('could not set locale.LC_ALL to fr_FR') # This should not cause an exception time.strftime("%B", (2009,2,1,0,0,0,0,0,0)) @@ -613,8 +675,6 @@ class _TestAsctimeYear: def yearstr(self, y): return time.asctime((y,) + (0,) * 8).split()[-1] - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure def test_large_year(self): # Check that it doesn't crash for year > 9999 self.assertEqual(self.yearstr(12345), '12345') @@ -627,15 +687,17 @@ class _TestStrftimeYear: # assumes year >= 1900, so it does not specify the number # of digits. - # TODO: RUSTPYTHON - # if time.strftime('%Y', (1,) + (0,) * 8) == '0001': - # _format = '%04d' - # else: - # _format = '%d' + if time.strftime('%Y', (1,) + (0,) * 8) == '0001': + _format = '%04d' + else: + _format = '%d' def yearstr(self, y): return time.strftime('%Y', (y,) + (0,) * 8) + @unittest.skipUnless( + support.has_strftime_extensions, "requires strftime extension" + ) def test_4dyear(self): # Check that we can return the zero padded value. if self._format == '%04d': @@ -646,8 +708,7 @@ def year4d(y): self.test_year('%04d', func=year4d) def skip_if_not_supported(y): - msg = "strftime() is limited to [1; 9999] with Visual Studio" - # Check that it doesn't crash for year > 9999 + msg = f"strftime() does not support year {y} on this platform" try: time.strftime('%Y', (y,) + (0,) * 8) except ValueError: @@ -670,8 +731,6 @@ def test_negative(self): class _Test4dYear: _format = '%d' - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure def test_year(self, fmt=None, func=None): fmt = fmt or self._format func = func or self.yearstr @@ -688,8 +747,6 @@ def test_large_year(self): self.assertEqual(self.yearstr(TIME_MAXYEAR).lstrip('+'), str(TIME_MAXYEAR)) self.assertRaises(OverflowError, self.yearstr, TIME_MAXYEAR + 1) - # TODO: RUSTPYTHON, ValueError: invalid struct_time parameter - @unittest.expectedFailure def test_negative(self): self.assertEqual(self.yearstr(-1), self._format % -1) self.assertEqual(self.yearstr(-1234), '-1234') @@ -704,32 +761,54 @@ def test_negative(self): class TestAsctime4dyear(_TestAsctimeYear, _Test4dYear, unittest.TestCase): - pass + # TODO: RUSTPYTHON; chrono on Windows cannot handle month=0/day=0 in struct_time + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_year(self, *args, **kwargs): + return super().test_year(*args, **kwargs) -# class TestStrftime4dyear(_TestStrftimeYear, _Test4dYear, unittest.TestCase): -# pass + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_large_year(self): + return super().test_large_year() + + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_negative(self): + return super().test_negative() + +class TestStrftime4dyear(_TestStrftimeYear, _Test4dYear, unittest.TestCase): + # TODO: RUSTPYTHON; chrono on Windows cannot handle month=0/day=0 in struct_time + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_year(self, *args, **kwargs): + return super().test_year(*args, **kwargs) + + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_large_year(self): + return super().test_large_year() + + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") + def test_negative(self): + return super().test_negative() class TestPytime(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure + # TODO: RUSTPYTHON; chrono %Z on Windows gives offset instead of timezone name + @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") @skip_if_buggy_ucrt_strfptime @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") def test_localtime_timezone(self): # Get the localtime and examine it for the offset and zone. lt = time.localtime() - self.assertTrue(hasattr(lt, "tm_gmtoff")) - self.assertTrue(hasattr(lt, "tm_zone")) + self.assertHasAttr(lt, "tm_gmtoff") + self.assertHasAttr(lt, "tm_zone") # See if the offset and zone are similar to the module # attributes. if lt.tm_gmtoff is None: - self.assertTrue(not hasattr(time, "timezone")) + self.assertNotHasAttr(time, "timezone") else: self.assertEqual(lt.tm_gmtoff, -[time.timezone, time.altzone][lt.tm_isdst]) if lt.tm_zone is None: - self.assertTrue(not hasattr(time, "tzname")) + self.assertNotHasAttr(time, "tzname") else: self.assertEqual(lt.tm_zone, time.tzname[lt.tm_isdst]) @@ -748,18 +827,14 @@ def test_localtime_timezone(self): self.assertEqual(new_lt9, lt) self.assertEqual(new_lt.tm_gmtoff, lt.tm_gmtoff) self.assertEqual(new_lt9.tm_zone, lt.tm_zone) - - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") def test_strptime_timezone(self): t = time.strptime("UTC", "%Z") self.assertEqual(t.tm_zone, 'UTC') t = time.strptime("+0500", "%z") self.assertEqual(t.tm_gmtoff, 5 * 3600) - - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") def test_short_times(self): @@ -772,7 +847,8 @@ def test_short_times(self): self.assertIs(lt.tm_zone, None) -@unittest.skipIf(_testcapi is None, 'need the _testcapi module') +@unittest.skipIf(_testcapi is None, 'need the _testinternalcapi module') +@unittest.skipIf(_testinternalcapi is None, 'need the _testinternalcapi module') class CPyTimeTestCase: """ Base class to test the C _PyTime_t API. @@ -780,7 +856,7 @@ class CPyTimeTestCase: OVERFLOW_SECONDS = None def setUp(self): - from _testcapi import SIZEOF_TIME_T + from _testinternalcapi import SIZEOF_TIME_T bits = SIZEOF_TIME_T * 8 - 1 self.time_t_min = -2 ** bits self.time_t_max = 2 ** bits - 1 @@ -859,7 +935,7 @@ def convert_values(ns_timestamps): # test rounding ns_timestamps = self._rounding_values(use_float) valid_values = convert_values(ns_timestamps) - for time_rnd, decimal_rnd in ROUNDING_MODES : + for time_rnd, decimal_rnd in ROUNDING_MODES: with decimal.localcontext() as context: context.rounding = decimal_rnd @@ -908,36 +984,36 @@ class TestCPyTime(CPyTimeTestCase, unittest.TestCase): OVERFLOW_SECONDS = math.ceil((2**63 + 1) / SEC_TO_NS) def test_FromSeconds(self): - from _testcapi import PyTime_FromSeconds + from _testinternalcapi import _PyTime_FromSeconds - # PyTime_FromSeconds() expects a C int, reject values out of range + # _PyTime_FromSeconds() expects a C int, reject values out of range def c_int_filter(secs): return (_testcapi.INT_MIN <= secs <= _testcapi.INT_MAX) - self.check_int_rounding(lambda secs, rnd: PyTime_FromSeconds(secs), + self.check_int_rounding(lambda secs, rnd: _PyTime_FromSeconds(secs), lambda secs: secs * SEC_TO_NS, value_filter=c_int_filter) # test nan for time_rnd, _ in ROUNDING_MODES: with self.assertRaises(TypeError): - PyTime_FromSeconds(float('nan')) + _PyTime_FromSeconds(float('nan')) def test_FromSecondsObject(self): - from _testcapi import PyTime_FromSecondsObject + from _testinternalcapi import _PyTime_FromSecondsObject self.check_int_rounding( - PyTime_FromSecondsObject, + _PyTime_FromSecondsObject, lambda secs: secs * SEC_TO_NS) self.check_float_rounding( - PyTime_FromSecondsObject, + _PyTime_FromSecondsObject, lambda ns: self.decimal_round(ns * SEC_TO_NS)) # test nan for time_rnd, _ in ROUNDING_MODES: with self.assertRaises(ValueError): - PyTime_FromSecondsObject(float('nan'), time_rnd) + _PyTime_FromSecondsObject(float('nan'), time_rnd) def test_AsSecondsDouble(self): from _testcapi import PyTime_AsSecondsDouble @@ -952,11 +1028,6 @@ def float_converter(ns): float_converter, NS_TO_SEC) - # test nan - for time_rnd, _ in ROUNDING_MODES: - with self.assertRaises(TypeError): - PyTime_AsSecondsDouble(float('nan')) - def create_decimal_converter(self, denominator): denom = decimal.Decimal(denominator) @@ -967,7 +1038,7 @@ def converter(value): return converter def test_AsTimeval(self): - from _testcapi import PyTime_AsTimeval + from _testinternalcapi import _PyTime_AsTimeval us_converter = self.create_decimal_converter(US_TO_NS) @@ -984,35 +1055,78 @@ def seconds_filter(secs): else: seconds_filter = self.time_t_filter - self.check_int_rounding(PyTime_AsTimeval, + self.check_int_rounding(_PyTime_AsTimeval, timeval_converter, NS_TO_SEC, value_filter=seconds_filter) - @unittest.skipUnless(hasattr(_testcapi, 'PyTime_AsTimespec'), - 'need _testcapi.PyTime_AsTimespec') + @unittest.skipUnless(hasattr(_testinternalcapi, '_PyTime_AsTimespec'), + 'need _testinternalcapi._PyTime_AsTimespec') def test_AsTimespec(self): - from _testcapi import PyTime_AsTimespec + from _testinternalcapi import _PyTime_AsTimespec def timespec_converter(ns): return divmod(ns, SEC_TO_NS) - self.check_int_rounding(lambda ns, rnd: PyTime_AsTimespec(ns), + self.check_int_rounding(lambda ns, rnd: _PyTime_AsTimespec(ns), timespec_converter, NS_TO_SEC, value_filter=self.time_t_filter) + @unittest.skipUnless(hasattr(_testinternalcapi, '_PyTime_AsTimeval_clamp'), + 'need _testinternalcapi._PyTime_AsTimeval_clamp') + def test_AsTimeval_clamp(self): + from _testinternalcapi import _PyTime_AsTimeval_clamp + + if sys.platform == 'win32': + from _testcapi import LONG_MIN, LONG_MAX + tv_sec_max = LONG_MAX + tv_sec_min = LONG_MIN + else: + tv_sec_max = self.time_t_max + tv_sec_min = self.time_t_min + + for t in (PyTime_MIN, PyTime_MAX): + ts = _PyTime_AsTimeval_clamp(t, _PyTime.ROUND_CEILING) + with decimal.localcontext() as context: + context.rounding = decimal.ROUND_CEILING + us = self.decimal_round(decimal.Decimal(t) / US_TO_NS) + tv_sec, tv_usec = divmod(us, SEC_TO_US) + if tv_sec_max < tv_sec: + tv_sec = tv_sec_max + tv_usec = 0 + elif tv_sec < tv_sec_min: + tv_sec = tv_sec_min + tv_usec = 0 + self.assertEqual(ts, (tv_sec, tv_usec)) + + @unittest.skipUnless(hasattr(_testinternalcapi, '_PyTime_AsTimespec_clamp'), + 'need _testinternalcapi._PyTime_AsTimespec_clamp') + def test_AsTimespec_clamp(self): + from _testinternalcapi import _PyTime_AsTimespec_clamp + + for t in (PyTime_MIN, PyTime_MAX): + ts = _PyTime_AsTimespec_clamp(t) + tv_sec, tv_nsec = divmod(t, NS_TO_SEC) + if self.time_t_max < tv_sec: + tv_sec = self.time_t_max + tv_nsec = 0 + elif tv_sec < self.time_t_min: + tv_sec = self.time_t_min + tv_nsec = 0 + self.assertEqual(ts, (tv_sec, tv_nsec)) + def test_AsMilliseconds(self): - from _testcapi import PyTime_AsMilliseconds + from _testinternalcapi import _PyTime_AsMilliseconds - self.check_int_rounding(PyTime_AsMilliseconds, + self.check_int_rounding(_PyTime_AsMilliseconds, self.create_decimal_converter(MS_TO_NS), NS_TO_SEC) def test_AsMicroseconds(self): - from _testcapi import PyTime_AsMicroseconds + from _testinternalcapi import _PyTime_AsMicroseconds - self.check_int_rounding(PyTime_AsMicroseconds, + self.check_int_rounding(_PyTime_AsMicroseconds, self.create_decimal_converter(US_TO_NS), NS_TO_SEC) @@ -1026,13 +1140,13 @@ class TestOldPyTime(CPyTimeTestCase, unittest.TestCase): OVERFLOW_SECONDS = 2 ** 64 def test_object_to_time_t(self): - from _testcapi import pytime_object_to_time_t + from _testinternalcapi import _PyTime_ObjectToTime_t - self.check_int_rounding(pytime_object_to_time_t, + self.check_int_rounding(_PyTime_ObjectToTime_t, lambda secs: secs, value_filter=self.time_t_filter) - self.check_float_rounding(pytime_object_to_time_t, + self.check_float_rounding(_PyTime_ObjectToTime_t, self.decimal_round, value_filter=self.time_t_filter) @@ -1052,36 +1166,36 @@ def converter(secs): return converter def test_object_to_timeval(self): - from _testcapi import pytime_object_to_timeval + from _testinternalcapi import _PyTime_ObjectToTimeval - self.check_int_rounding(pytime_object_to_timeval, + self.check_int_rounding(_PyTime_ObjectToTimeval, lambda secs: (secs, 0), value_filter=self.time_t_filter) - self.check_float_rounding(pytime_object_to_timeval, + self.check_float_rounding(_PyTime_ObjectToTimeval, self.create_converter(SEC_TO_US), value_filter=self.time_t_filter) # test nan for time_rnd, _ in ROUNDING_MODES: with self.assertRaises(ValueError): - pytime_object_to_timeval(float('nan'), time_rnd) + _PyTime_ObjectToTimeval(float('nan'), time_rnd) def test_object_to_timespec(self): - from _testcapi import pytime_object_to_timespec + from _testinternalcapi import _PyTime_ObjectToTimespec - self.check_int_rounding(pytime_object_to_timespec, + self.check_int_rounding(_PyTime_ObjectToTimespec, lambda secs: (secs, 0), value_filter=self.time_t_filter) - self.check_float_rounding(pytime_object_to_timespec, + self.check_float_rounding(_PyTime_ObjectToTimespec, self.create_converter(SEC_TO_NS), value_filter=self.time_t_filter) # test nan for time_rnd, _ in ROUNDING_MODES: with self.assertRaises(ValueError): - pytime_object_to_timespec(float('nan'), time_rnd) + _PyTime_ObjectToTimespec(float('nan'), time_rnd) @unittest.skipUnless(sys.platform == "darwin", "test weak linking on macOS") class TestTimeWeaklinking(unittest.TestCase): @@ -1107,11 +1221,11 @@ def test_clock_functions(self): if mac_ver >= (10, 12): for name in clock_names: - self.assertTrue(hasattr(time, name), f"time.{name} is not available") + self.assertHasAttr(time, name) else: for name in clock_names: - self.assertFalse(hasattr(time, name), f"time.{name} is available") + self.assertNotHasAttr(time, name) if __name__ == "__main__":