diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 4e618d1d90..3af18efae2 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -718,9 +718,10 @@ class TestAsctime4dyear(_TestAsctimeYear, _Test4dYear, unittest.TestCase): class TestPytime(unittest.TestCase): + # TODO: RUSTPYTHON + @unittest.expectedFailure @skip_if_buggy_ucrt_strfptime - @unittest.skip("TODO: RUSTPYTHON, AttributeError: module 'time' has no attribute '_STRUCT_TM_ITEMS'") - # @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") + @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. @@ -755,16 +756,18 @@ def test_localtime_timezone(self): self.assertEqual(new_lt.tm_gmtoff, lt.tm_gmtoff) self.assertEqual(new_lt9.tm_zone, lt.tm_zone) - @unittest.skip("TODO: RUSTPYTHON, AttributeError: module 'time' has no attribute '_STRUCT_TM_ITEMS'") - # @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") + # 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) - @unittest.skip("TODO: RUSTPYTHON, AttributeError: module 'time' has no attribute '_STRUCT_TM_ITEMS'") - # @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") + # TODO: RUSTPYTHON + @unittest.expectedFailure + @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") def test_short_times(self): import pickle diff --git a/derive-impl/src/pystructseq.rs b/derive-impl/src/pystructseq.rs index 4a4f9276f0..89dee8da10 100644 --- a/derive-impl/src/pystructseq.rs +++ b/derive-impl/src/pystructseq.rs @@ -80,16 +80,19 @@ fn field_names(input: &mut DeriveInput) -> Result<(Vec, Vec)> { } pub(crate) fn impl_pystruct_sequence(mut input: DeriveInput) -> Result { - let (not_skipped_fields, _skipped_fields) = field_names(&mut input)?; + let (not_skipped_fields, skipped_fields) = field_names(&mut input)?; let ty = &input.ident; let ret = quote! { impl ::rustpython_vm::types::PyStructSequence for #ty { - const FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#not_skipped_fields)),*]; + const REQUIRED_FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#not_skipped_fields),)*]; + const OPTIONAL_FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#skipped_fields),)*]; fn into_tuple(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::builtins::PyTuple { - let items = vec![#(::rustpython_vm::convert::ToPyObject::to_pyobject( - self.#not_skipped_fields, - vm, - )),*]; + let items = vec![ + #(::rustpython_vm::convert::ToPyObject::to_pyobject( + self.#not_skipped_fields, + vm, + ),)* + ]; ::rustpython_vm::builtins::PyTuple::new_unchecked(items.into_boxed_slice()) } } @@ -110,17 +113,14 @@ pub(crate) fn impl_pystruct_sequence_try_from_object( let ret = quote! { impl ::rustpython_vm::TryFromObject for #ty { fn try_from_object(vm: &::rustpython_vm::VirtualMachine, seq: ::rustpython_vm::PyObjectRef) -> ::rustpython_vm::PyResult { - const LEN: usize = #ty::FIELD_NAMES.len(); - let seq = Self::try_elements_from::(seq, vm)?; - // TODO: this is possible to be written without iterator + let seq = Self::try_elements_from(seq, vm)?; let mut iter = seq.into_iter(); Ok(Self { - #( - #not_skipped_fields: iter.next().unwrap().clone().try_into_value(vm)?, - )* - #( - #skipped_fields: vm.ctx.none(), - )* + #(#not_skipped_fields: iter.next().unwrap().clone().try_into_value(vm)?,)* + #(#skipped_fields: match iter.next() { + Some(v) => v.clone().try_into_value(vm)?, + None => vm.ctx.none(), + },)* }) } } diff --git a/vm/src/stdlib/time.rs b/vm/src/stdlib/time.rs index f98530e845..10d51bd39a 100644 --- a/vm/src/stdlib/time.rs +++ b/vm/src/stdlib/time.rs @@ -39,7 +39,7 @@ mod decl { types::PyStructSequence, }; use chrono::{ - DateTime, Datelike, Timelike, + DateTime, Datelike, TimeZone, Timelike, naive::{NaiveDate, NaiveDateTime, NaiveTime}, }; use std::time::Duration; @@ -72,6 +72,9 @@ mod decl { .map_err(|e| vm.new_value_error(format!("Time error: {e:?}"))) } + #[pyattr] + pub const _STRUCT_TM_ITEMS: usize = 11; + // TODO: implement proper monotonic time for wasm/wasi. #[cfg(not(any(unix, windows)))] fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { @@ -451,6 +454,10 @@ mod decl { tm_wday: PyObjectRef, tm_yday: PyObjectRef, tm_isdst: PyObjectRef, + #[pystruct(skip)] + tm_gmtoff: PyObjectRef, + #[pystruct(skip)] + tm_zone: PyObjectRef, } impl std::fmt::Debug for PyStructTime { @@ -462,6 +469,11 @@ mod decl { #[pyclass(with(PyStructSequence))] impl PyStructTime { fn new(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 tz_abbr = local_time.format("%Z").to_string(); + PyStructTime { tm_year: vm.ctx.new_int(tm.year()).into(), tm_mon: vm.ctx.new_int(tm.month()).into(), @@ -472,6 +484,8 @@ 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(offset_seconds).into(), + tm_zone: vm.ctx.new_str(tz_abbr).into(), } } diff --git a/vm/src/types/structseq.rs b/vm/src/types/structseq.rs index a0b445ce7d..7b7f5fb9b5 100644 --- a/vm/src/types/structseq.rs +++ b/vm/src/types/structseq.rs @@ -1,13 +1,14 @@ use crate::{ AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyTuple, PyTupleRef, PyType}, + builtins::{PyBaseExceptionRef, PyTuple, PyTupleRef, PyType}, class::{PyClassImpl, StaticType}, vm::Context, }; #[pyclass] pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { - const FIELD_NAMES: &'static [&'static str]; + const REQUIRED_FIELD_NAMES: &'static [&'static str]; + const OPTIONAL_FIELD_NAMES: &'static [&'static str]; fn into_tuple(self, vm: &VirtualMachine) -> PyTuple; @@ -17,10 +18,16 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { .unwrap() } - fn try_elements_from( - obj: PyObjectRef, - vm: &VirtualMachine, - ) -> PyResult<[PyObjectRef; FIELD_LEN]> { + fn try_elements_from(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult> { + #[cold] + fn sequence_length_error( + name: &str, + len: usize, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + vm.new_type_error(format!("{name} takes a sequence of length {len}")) + } + let typ = Self::static_type(); // if !obj.fast_isinstance(typ) { // return Err(vm.new_type_error(format!( @@ -30,13 +37,13 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { // ))); // } let seq: Vec = obj.try_into_value(vm)?; - let seq: [PyObjectRef; FIELD_LEN] = seq.try_into().map_err(|_| { - vm.new_type_error(format!( - "{} takes a sequence of length {}", - typ.name(), - FIELD_LEN - )) - })?; + if seq.len() < Self::REQUIRED_FIELD_NAMES.len() { + return Err(sequence_length_error( + &typ.name(), + Self::REQUIRED_FIELD_NAMES.len(), + vm, + )); + } Ok(seq) } @@ -49,14 +56,14 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { let (body, suffix) = if let Some(_guard) = rustpython_vm::recursion::ReprGuard::enter(vm, zelf.as_object()) { - if Self::FIELD_NAMES.len() == 1 { + if Self::REQUIRED_FIELD_NAMES.len() == 1 { let value = zelf.first().unwrap(); - let formatted = format_field((value, Self::FIELD_NAMES[0]))?; + let formatted = format_field((value, Self::REQUIRED_FIELD_NAMES[0]))?; (formatted, ",") } else { let fields: PyResult> = zelf .iter() - .zip(Self::FIELD_NAMES.iter().copied()) + .zip(Self::REQUIRED_FIELD_NAMES.iter().copied()) .map(format_field) .collect(); (fields?.join(", "), "") @@ -74,7 +81,7 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { #[extend_class] fn extend_pyclass(ctx: &Context, class: &'static Py) { - for (i, &name) in Self::FIELD_NAMES.iter().enumerate() { + for (i, &name) in Self::REQUIRED_FIELD_NAMES.iter().enumerate() { // cast i to a u8 so there's less to store in the getter closure. // Hopefully there's not struct sequences with >=256 elements :P let i = i as u8; @@ -90,7 +97,7 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { class.set_attr( identifier!(ctx, __match_args__), ctx.new_tuple( - Self::FIELD_NAMES + Self::REQUIRED_FIELD_NAMES .iter() .map(|&name| ctx.new_str(name).into()) .collect::>(),