|
43 | 43 |
|
44 | 44 | use crate::conversion::IntoPyObject;
|
45 | 45 | use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError};
|
46 |
| -#[cfg(Py_LIMITED_API)] |
47 | 46 | use crate::intern;
|
48 | 47 | use crate::types::any::PyAnyMethods;
|
49 | 48 | use crate::types::PyNone;
|
50 | 49 | use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
|
51 | 50 | #[cfg(not(Py_LIMITED_API))]
|
52 | 51 | use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
|
| 52 | +#[cfg(feature = "chrono-local")] |
| 53 | +use crate::{ |
| 54 | + exceptions::PyRuntimeError, |
| 55 | + sync::GILOnceCell, |
| 56 | + types::{PyString, PyStringMethods}, |
| 57 | + Py, |
| 58 | +}; |
53 | 59 | use crate::{ffi, Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python};
|
54 | 60 | use chrono::offset::{FixedOffset, Utc};
|
| 61 | +#[cfg(feature = "chrono-local")] |
| 62 | +use chrono::Local; |
55 | 63 | use chrono::{
|
56 | 64 | DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset,
|
57 | 65 | TimeZone, Timelike,
|
@@ -387,7 +395,8 @@ impl FromPyObject<'_> for FixedOffset {
|
387 | 395 | // Any other timezone would require a datetime as the parameter, and return
|
388 | 396 | // None if the datetime is not provided.
|
389 | 397 | // Trying to convert None to a PyDelta in the next line will then fail.
|
390 |
| - let py_timedelta = ob.call_method1("utcoffset", (PyNone::get(ob.py()),))?; |
| 398 | + let py_timedelta = |
| 399 | + ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?; |
391 | 400 | if py_timedelta.is_none() {
|
392 | 401 | return Err(PyTypeError::new_err(format!(
|
393 | 402 | "{ob:?} is not a fixed offset timezone"
|
@@ -433,6 +442,54 @@ impl FromPyObject<'_> for Utc {
|
433 | 442 | }
|
434 | 443 | }
|
435 | 444 |
|
| 445 | +#[cfg(feature = "chrono-local")] |
| 446 | +impl<'py> IntoPyObject<'py> for Local { |
| 447 | + type Target = PyTzInfo; |
| 448 | + type Output = Borrowed<'static, 'py, Self::Target>; |
| 449 | + type Error = PyErr; |
| 450 | + |
| 451 | + fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 452 | + static LOCAL_TZ: GILOnceCell<Py<PyTzInfo>> = GILOnceCell::new(); |
| 453 | + let tz = LOCAL_TZ |
| 454 | + .get_or_try_init(py, || { |
| 455 | + let iana_name = iana_time_zone::get_timezone().map_err(|e| { |
| 456 | + PyRuntimeError::new_err(format!("Could not get local timezone: {e}")) |
| 457 | + })?; |
| 458 | + PyTzInfo::timezone(py, iana_name).map(Bound::unbind) |
| 459 | + })? |
| 460 | + .bind_borrowed(py); |
| 461 | + Ok(tz) |
| 462 | + } |
| 463 | +} |
| 464 | + |
| 465 | +#[cfg(feature = "chrono-local")] |
| 466 | +impl<'py> IntoPyObject<'py> for &Local { |
| 467 | + type Target = PyTzInfo; |
| 468 | + type Output = Borrowed<'static, 'py, Self::Target>; |
| 469 | + type Error = PyErr; |
| 470 | + |
| 471 | + #[inline] |
| 472 | + fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 473 | + (*self).into_pyobject(py) |
| 474 | + } |
| 475 | +} |
| 476 | + |
| 477 | +#[cfg(feature = "chrono-local")] |
| 478 | +impl FromPyObject<'_> for Local { |
| 479 | + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Local> { |
| 480 | + let local_tz = Local.into_pyobject(ob.py())?; |
| 481 | + if ob.eq(local_tz)? { |
| 482 | + Ok(Local) |
| 483 | + } else { |
| 484 | + let name = local_tz.getattr("key")?.downcast_into::<PyString>()?; |
| 485 | + Err(PyValueError::new_err(format!( |
| 486 | + "expected local timezone {}", |
| 487 | + name.to_cow()? |
| 488 | + ))) |
| 489 | + } |
| 490 | + } |
| 491 | +} |
| 492 | + |
436 | 493 | struct DateArgs {
|
437 | 494 | year: i32,
|
438 | 495 | month: u8,
|
@@ -1281,6 +1338,43 @@ mod tests {
|
1281 | 1338 | }
|
1282 | 1339 | })
|
1283 | 1340 | }
|
| 1341 | + |
| 1342 | + #[test] |
| 1343 | + #[cfg(all(feature = "chrono-local", not(target_os = "windows")))] |
| 1344 | + fn test_local_datetime_roundtrip( |
| 1345 | + year in 1i32..=9999i32, |
| 1346 | + month in 1u32..=12u32, |
| 1347 | + day in 1u32..=31u32, |
| 1348 | + hour in 0u32..=23u32, |
| 1349 | + min in 0u32..=59u32, |
| 1350 | + sec in 0u32..=59u32, |
| 1351 | + micro in 0u32..=1_999_999u32, |
| 1352 | + ) { |
| 1353 | + Python::with_gil(|py| { |
| 1354 | + let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
| 1355 | + let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
| 1356 | + if let (Some(date), Some(time)) = (date_opt, time_opt) { |
| 1357 | + let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) { |
| 1358 | + LocalResult::None => return, |
| 1359 | + LocalResult::Single(dt) => [Some((dt, false)), None], |
| 1360 | + LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))], |
| 1361 | + }; |
| 1362 | + for (dt, fold) in dts.iter().filter_map(|input| *input) { |
| 1363 | + // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
| 1364 | + let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); |
| 1365 | + let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip"); |
| 1366 | + // Leap seconds are not roundtripped |
| 1367 | + let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
| 1368 | + let expected_roundtrip_dt: DateTime<Local> = if fold { |
| 1369 | + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest() |
| 1370 | + } else { |
| 1371 | + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest() |
| 1372 | + }.unwrap(); |
| 1373 | + assert_eq!(expected_roundtrip_dt, roundtripped); |
| 1374 | + } |
| 1375 | + } |
| 1376 | + }) |
| 1377 | + } |
1284 | 1378 | }
|
1285 | 1379 | }
|
1286 | 1380 | }
|
0 commit comments