| 1 | #![cfg (feature = "chrono" )] |
| 2 | |
| 3 | //! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`, |
| 4 | //! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`. |
| 5 | //! |
| 6 | //! # Setup |
| 7 | //! |
| 8 | //! To use this feature, add this to your **`Cargo.toml`**: |
| 9 | //! |
| 10 | //! ```toml |
| 11 | //! [dependencies] |
| 12 | //! chrono = "0.4" |
| 13 | #![doc = concat!("pyo3 = { version = \"" , env!("CARGO_PKG_VERSION" ), " \", features = [ \"chrono \"] }" )] |
| 14 | //! ``` |
| 15 | //! |
| 16 | //! Note that you must use compatible versions of chrono and PyO3. |
| 17 | //! The required chrono version may vary based on the version of PyO3. |
| 18 | //! |
| 19 | //! # Example: Convert a `datetime.datetime` to chrono's `DateTime<Utc>` |
| 20 | //! |
| 21 | //! ```rust |
| 22 | //! use chrono::{DateTime, Duration, TimeZone, Utc}; |
| 23 | //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; |
| 24 | //! |
| 25 | //! fn main() -> PyResult<()> { |
| 26 | //! pyo3::prepare_freethreaded_python(); |
| 27 | //! Python::with_gil(|py| { |
| 28 | //! // Build some chrono values |
| 29 | //! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap(); |
| 30 | //! let chrono_duration = Duration::seconds(1); |
| 31 | //! // Convert them to Python |
| 32 | //! let py_datetime = chrono_datetime.into_pyobject(py)?; |
| 33 | //! let py_timedelta = chrono_duration.into_pyobject(py)?; |
| 34 | //! // Do an operation in Python |
| 35 | //! let py_sum = py_datetime.call_method1("__add__" , (py_timedelta,))?; |
| 36 | //! // Convert back to Rust |
| 37 | //! let chrono_sum: DateTime<Utc> = py_sum.extract()?; |
| 38 | //! println!("DateTime<Utc>: {}" , chrono_datetime); |
| 39 | //! Ok(()) |
| 40 | //! }) |
| 41 | //! } |
| 42 | //! ``` |
| 43 | |
| 44 | use crate::conversion::IntoPyObject; |
| 45 | use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; |
| 46 | #[cfg (Py_LIMITED_API)] |
| 47 | use crate::intern; |
| 48 | use crate::types::any::PyAnyMethods; |
| 49 | #[cfg (not(Py_LIMITED_API))] |
| 50 | use crate::types::datetime::timezone_from_offset; |
| 51 | #[cfg (Py_LIMITED_API)] |
| 52 | use crate::types::datetime_abi3::{check_type, timezone_utc, DatetimeTypes}; |
| 53 | #[cfg (Py_LIMITED_API)] |
| 54 | use crate::types::IntoPyDict; |
| 55 | use crate::types::PyNone; |
| 56 | #[cfg (not(Py_LIMITED_API))] |
| 57 | use crate::types::{ |
| 58 | timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, |
| 59 | PyTzInfo, PyTzInfoAccess, |
| 60 | }; |
| 61 | use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; |
| 62 | #[allow (deprecated)] |
| 63 | use crate::{IntoPy, ToPyObject}; |
| 64 | use chrono::offset::{FixedOffset, Utc}; |
| 65 | use chrono::{ |
| 66 | DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, |
| 67 | TimeZone, Timelike, |
| 68 | }; |
| 69 | |
| 70 | #[allow (deprecated)] |
| 71 | impl ToPyObject for Duration { |
| 72 | #[inline ] |
| 73 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 74 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | #[allow (deprecated)] |
| 79 | impl IntoPy<PyObject> for Duration { |
| 80 | #[inline ] |
| 81 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 82 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | impl<'py> IntoPyObject<'py> for Duration { |
| 87 | #[cfg (Py_LIMITED_API)] |
| 88 | type Target = PyAny; |
| 89 | #[cfg (not(Py_LIMITED_API))] |
| 90 | type Target = PyDelta; |
| 91 | type Output = Bound<'py, Self::Target>; |
| 92 | type Error = PyErr; |
| 93 | |
| 94 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 95 | // Total number of days |
| 96 | let days = self.num_days(); |
| 97 | // Remainder of seconds |
| 98 | let secs_dur = self - Duration::days(days); |
| 99 | let secs = secs_dur.num_seconds(); |
| 100 | // Fractional part of the microseconds |
| 101 | let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds())) |
| 102 | .num_microseconds() |
| 103 | // This should never panic since we are just getting the fractional |
| 104 | // part of the total microseconds, which should never overflow. |
| 105 | .unwrap(); |
| 106 | |
| 107 | #[cfg (not(Py_LIMITED_API))] |
| 108 | { |
| 109 | // We do not need to check the days i64 to i32 cast from rust because |
| 110 | // python will panic with OverflowError. |
| 111 | // We pass true as the `normalize` parameter since we'd need to do several checks here to |
| 112 | // avoid that, and it shouldn't have a big performance impact. |
| 113 | // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day |
| 114 | PyDelta::new( |
| 115 | py, |
| 116 | days.try_into().unwrap_or(i32::MAX), |
| 117 | secs.try_into()?, |
| 118 | micros.try_into()?, |
| 119 | true, |
| 120 | ) |
| 121 | } |
| 122 | |
| 123 | #[cfg (Py_LIMITED_API)] |
| 124 | { |
| 125 | DatetimeTypes::try_get(py) |
| 126 | .and_then(|dt| dt.timedelta.bind(py).call1((days, secs, micros))) |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | impl<'py> IntoPyObject<'py> for &Duration { |
| 132 | #[cfg (Py_LIMITED_API)] |
| 133 | type Target = PyAny; |
| 134 | #[cfg (not(Py_LIMITED_API))] |
| 135 | type Target = PyDelta; |
| 136 | type Output = Bound<'py, Self::Target>; |
| 137 | type Error = PyErr; |
| 138 | |
| 139 | #[inline ] |
| 140 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 141 | (*self).into_pyobject(py) |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | impl FromPyObject<'_> for Duration { |
| 146 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Duration> { |
| 147 | // Python size are much lower than rust size so we do not need bound checks. |
| 148 | // 0 <= microseconds < 1000000 |
| 149 | // 0 <= seconds < 3600*24 |
| 150 | // -999999999 <= days <= 999999999 |
| 151 | #[cfg (not(Py_LIMITED_API))] |
| 152 | let (days, seconds, microseconds) = { |
| 153 | let delta = ob.downcast::<PyDelta>()?; |
| 154 | ( |
| 155 | delta.get_days().into(), |
| 156 | delta.get_seconds().into(), |
| 157 | delta.get_microseconds().into(), |
| 158 | ) |
| 159 | }; |
| 160 | #[cfg (Py_LIMITED_API)] |
| 161 | let (days, seconds, microseconds) = { |
| 162 | check_type(ob, &DatetimeTypes::get(ob.py()).timedelta, "PyDelta" )?; |
| 163 | ( |
| 164 | ob.getattr(intern!(ob.py(), "days" ))?.extract()?, |
| 165 | ob.getattr(intern!(ob.py(), "seconds" ))?.extract()?, |
| 166 | ob.getattr(intern!(ob.py(), "microseconds" ))?.extract()?, |
| 167 | ) |
| 168 | }; |
| 169 | Ok( |
| 170 | Duration::days(days) |
| 171 | + Duration::seconds(seconds) |
| 172 | + Duration::microseconds(microseconds), |
| 173 | ) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | #[allow (deprecated)] |
| 178 | impl ToPyObject for NaiveDate { |
| 179 | #[inline ] |
| 180 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 181 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | #[allow (deprecated)] |
| 186 | impl IntoPy<PyObject> for NaiveDate { |
| 187 | #[inline ] |
| 188 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 189 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | impl<'py> IntoPyObject<'py> for NaiveDate { |
| 194 | #[cfg (Py_LIMITED_API)] |
| 195 | type Target = PyAny; |
| 196 | #[cfg (not(Py_LIMITED_API))] |
| 197 | type Target = PyDate; |
| 198 | type Output = Bound<'py, Self::Target>; |
| 199 | type Error = PyErr; |
| 200 | |
| 201 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 202 | let DateArgs { year: i32, month: u8, day: u8 } = (&self).into(); |
| 203 | #[cfg (not(Py_LIMITED_API))] |
| 204 | { |
| 205 | PyDate::new(py, year, month, day) |
| 206 | } |
| 207 | |
| 208 | #[cfg (Py_LIMITED_API)] |
| 209 | { |
| 210 | DatetimeTypes::try_get(py).and_then(|dt: &DatetimeTypes| dt.date.bind(py).call1((year, month, day))) |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | impl<'py> IntoPyObject<'py> for &NaiveDate { |
| 216 | #[cfg (Py_LIMITED_API)] |
| 217 | type Target = PyAny; |
| 218 | #[cfg (not(Py_LIMITED_API))] |
| 219 | type Target = PyDate; |
| 220 | type Output = Bound<'py, Self::Target>; |
| 221 | type Error = PyErr; |
| 222 | |
| 223 | #[inline ] |
| 224 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 225 | (*self).into_pyobject(py) |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | impl FromPyObject<'_> for NaiveDate { |
| 230 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveDate> { |
| 231 | #[cfg (not(Py_LIMITED_API))] |
| 232 | { |
| 233 | let date = ob.downcast::<PyDate>()?; |
| 234 | py_date_to_naive_date(date) |
| 235 | } |
| 236 | #[cfg (Py_LIMITED_API)] |
| 237 | { |
| 238 | check_type(value:ob, &DatetimeTypes::get(ob.py()).date, type_name:"PyDate" )?; |
| 239 | py_date_to_naive_date(py_date:ob) |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | #[allow (deprecated)] |
| 245 | impl ToPyObject for NaiveTime { |
| 246 | #[inline ] |
| 247 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 248 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | #[allow (deprecated)] |
| 253 | impl IntoPy<PyObject> for NaiveTime { |
| 254 | #[inline ] |
| 255 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 256 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | impl<'py> IntoPyObject<'py> for NaiveTime { |
| 261 | #[cfg (Py_LIMITED_API)] |
| 262 | type Target = PyAny; |
| 263 | #[cfg (not(Py_LIMITED_API))] |
| 264 | type Target = PyTime; |
| 265 | type Output = Bound<'py, Self::Target>; |
| 266 | type Error = PyErr; |
| 267 | |
| 268 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 269 | let TimeArgs { |
| 270 | hour, |
| 271 | min, |
| 272 | sec, |
| 273 | micro, |
| 274 | truncated_leap_second, |
| 275 | } = (&self).into(); |
| 276 | |
| 277 | #[cfg (not(Py_LIMITED_API))] |
| 278 | let time = PyTime::new(py, hour, min, sec, micro, None)?; |
| 279 | |
| 280 | #[cfg (Py_LIMITED_API)] |
| 281 | let time = DatetimeTypes::try_get(py) |
| 282 | .and_then(|dt| dt.time.bind(py).call1((hour, min, sec, micro)))?; |
| 283 | |
| 284 | if truncated_leap_second { |
| 285 | warn_truncated_leap_second(&time); |
| 286 | } |
| 287 | |
| 288 | Ok(time) |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | impl<'py> IntoPyObject<'py> for &NaiveTime { |
| 293 | #[cfg (Py_LIMITED_API)] |
| 294 | type Target = PyAny; |
| 295 | #[cfg (not(Py_LIMITED_API))] |
| 296 | type Target = PyTime; |
| 297 | type Output = Bound<'py, Self::Target>; |
| 298 | type Error = PyErr; |
| 299 | |
| 300 | #[inline ] |
| 301 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 302 | (*self).into_pyobject(py) |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | impl FromPyObject<'_> for NaiveTime { |
| 307 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveTime> { |
| 308 | #[cfg (not(Py_LIMITED_API))] |
| 309 | { |
| 310 | let time = ob.downcast::<PyTime>()?; |
| 311 | py_time_to_naive_time(time) |
| 312 | } |
| 313 | #[cfg (Py_LIMITED_API)] |
| 314 | { |
| 315 | check_type(value:ob, &DatetimeTypes::get(ob.py()).time, type_name:"PyTime" )?; |
| 316 | py_time_to_naive_time(py_time:ob) |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | #[allow (deprecated)] |
| 322 | impl ToPyObject for NaiveDateTime { |
| 323 | #[inline ] |
| 324 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 325 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | #[allow (deprecated)] |
| 330 | impl IntoPy<PyObject> for NaiveDateTime { |
| 331 | #[inline ] |
| 332 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 333 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | impl<'py> IntoPyObject<'py> for NaiveDateTime { |
| 338 | #[cfg (Py_LIMITED_API)] |
| 339 | type Target = PyAny; |
| 340 | #[cfg (not(Py_LIMITED_API))] |
| 341 | type Target = PyDateTime; |
| 342 | type Output = Bound<'py, Self::Target>; |
| 343 | type Error = PyErr; |
| 344 | |
| 345 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 346 | let DateArgs { year, month, day } = (&self.date()).into(); |
| 347 | let TimeArgs { |
| 348 | hour, |
| 349 | min, |
| 350 | sec, |
| 351 | micro, |
| 352 | truncated_leap_second, |
| 353 | } = (&self.time()).into(); |
| 354 | |
| 355 | #[cfg (not(Py_LIMITED_API))] |
| 356 | let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?; |
| 357 | |
| 358 | #[cfg (Py_LIMITED_API)] |
| 359 | let datetime = DatetimeTypes::try_get(py).and_then(|dt| { |
| 360 | dt.datetime |
| 361 | .bind(py) |
| 362 | .call1((year, month, day, hour, min, sec, micro)) |
| 363 | })?; |
| 364 | |
| 365 | if truncated_leap_second { |
| 366 | warn_truncated_leap_second(&datetime); |
| 367 | } |
| 368 | |
| 369 | Ok(datetime) |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | impl<'py> IntoPyObject<'py> for &NaiveDateTime { |
| 374 | #[cfg (Py_LIMITED_API)] |
| 375 | type Target = PyAny; |
| 376 | #[cfg (not(Py_LIMITED_API))] |
| 377 | type Target = PyDateTime; |
| 378 | type Output = Bound<'py, Self::Target>; |
| 379 | type Error = PyErr; |
| 380 | |
| 381 | #[inline ] |
| 382 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 383 | (*self).into_pyobject(py) |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | impl FromPyObject<'_> for NaiveDateTime { |
| 388 | fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<NaiveDateTime> { |
| 389 | #[cfg (not(Py_LIMITED_API))] |
| 390 | let dt = dt.downcast::<PyDateTime>()?; |
| 391 | #[cfg (Py_LIMITED_API)] |
| 392 | check_type(value:dt, &DatetimeTypes::get(dt.py()).datetime, type_name:"PyDateTime" )?; |
| 393 | |
| 394 | // If the user tries to convert a timezone aware datetime into a naive one, |
| 395 | // we return a hard error. We could silently remove tzinfo, or assume local timezone |
| 396 | // and do a conversion, but better leave this decision to the user of the library. |
| 397 | #[cfg (not(Py_LIMITED_API))] |
| 398 | let has_tzinfo = dt.get_tzinfo().is_some(); |
| 399 | #[cfg (Py_LIMITED_API)] |
| 400 | let has_tzinfo: bool = !dt.getattr(attr_name:intern!(dt.py(), "tzinfo" ))?.is_none(); |
| 401 | if has_tzinfo { |
| 402 | return Err(PyTypeError::new_err(args:"expected a datetime without tzinfo" )); |
| 403 | } |
| 404 | |
| 405 | let dt: NaiveDateTime = NaiveDateTime::new(py_date_to_naive_date(py_date:dt)?, py_time_to_naive_time(py_time:dt)?); |
| 406 | Ok(dt) |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | #[allow (deprecated)] |
| 411 | impl<Tz: TimeZone> ToPyObject for DateTime<Tz> { |
| 412 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 413 | // FIXME: convert to better timezone representation here than just convert to fixed offset |
| 414 | // See https://github.com/PyO3/pyo3/issues/3266 |
| 415 | let tz: Py = self.offset().fix().to_object(py); |
| 416 | let tz: &Bound<'_, PyAny> = tz.bind(py).downcast().unwrap(); |
| 417 | naive_datetime_to_py_datetime(py, &self.naive_local(), tzinfo:Some(tz)) |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | #[allow (deprecated)] |
| 422 | impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> { |
| 423 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 424 | self.to_object(py) |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime<Tz> |
| 429 | where |
| 430 | Tz: IntoPyObject<'py>, |
| 431 | { |
| 432 | #[cfg (Py_LIMITED_API)] |
| 433 | type Target = PyAny; |
| 434 | #[cfg (not(Py_LIMITED_API))] |
| 435 | type Target = PyDateTime; |
| 436 | type Output = Bound<'py, Self::Target>; |
| 437 | type Error = PyErr; |
| 438 | |
| 439 | #[inline ] |
| 440 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 441 | (&self).into_pyobject(py) |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime<Tz> |
| 446 | where |
| 447 | Tz: IntoPyObject<'py>, |
| 448 | { |
| 449 | #[cfg (Py_LIMITED_API)] |
| 450 | type Target = PyAny; |
| 451 | #[cfg (not(Py_LIMITED_API))] |
| 452 | type Target = PyDateTime; |
| 453 | type Output = Bound<'py, Self::Target>; |
| 454 | type Error = PyErr; |
| 455 | |
| 456 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 457 | let tz = self.timezone().into_bound_py_any(py)?; |
| 458 | |
| 459 | #[cfg (not(Py_LIMITED_API))] |
| 460 | let tz = tz.downcast()?; |
| 461 | |
| 462 | let DateArgs { year, month, day } = (&self.naive_local().date()).into(); |
| 463 | let TimeArgs { |
| 464 | hour, |
| 465 | min, |
| 466 | sec, |
| 467 | micro, |
| 468 | truncated_leap_second, |
| 469 | } = (&self.naive_local().time()).into(); |
| 470 | |
| 471 | let fold = matches!( |
| 472 | self.timezone().offset_from_local_datetime(&self.naive_local()), |
| 473 | LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix() |
| 474 | ); |
| 475 | |
| 476 | #[cfg (not(Py_LIMITED_API))] |
| 477 | let datetime = |
| 478 | PyDateTime::new_with_fold(py, year, month, day, hour, min, sec, micro, Some(tz), fold)?; |
| 479 | |
| 480 | #[cfg (Py_LIMITED_API)] |
| 481 | let datetime = DatetimeTypes::try_get(py).and_then(|dt| { |
| 482 | dt.datetime.bind(py).call( |
| 483 | (year, month, day, hour, min, sec, micro, tz), |
| 484 | Some(&[("fold" , fold as u8)].into_py_dict(py)?), |
| 485 | ) |
| 486 | })?; |
| 487 | |
| 488 | if truncated_leap_second { |
| 489 | warn_truncated_leap_second(&datetime); |
| 490 | } |
| 491 | |
| 492 | Ok(datetime) |
| 493 | } |
| 494 | } |
| 495 | |
| 496 | impl<Tz: TimeZone + for<'py> dynFromPyObject<'py>> FromPyObject<'_> for DateTime<Tz> { |
| 497 | fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<DateTime<Tz>> { |
| 498 | #[cfg (not(Py_LIMITED_API))] |
| 499 | let dt = dt.downcast::<PyDateTime>()?; |
| 500 | #[cfg (Py_LIMITED_API)] |
| 501 | check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime" )?; |
| 502 | |
| 503 | #[cfg (not(Py_LIMITED_API))] |
| 504 | let tzinfo = dt.get_tzinfo(); |
| 505 | #[cfg (Py_LIMITED_API)] |
| 506 | let tzinfo: Option<Bound<'_, PyAny>> = dt.getattr(intern!(dt.py(), "tzinfo" ))?.extract()?; |
| 507 | |
| 508 | let tz = if let Some(tzinfo) = tzinfo { |
| 509 | tzinfo.extract()? |
| 510 | } else { |
| 511 | return Err(PyTypeError::new_err( |
| 512 | "expected a datetime with non-None tzinfo" , |
| 513 | )); |
| 514 | }; |
| 515 | let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?); |
| 516 | match naive_dt.and_local_timezone(tz) { |
| 517 | LocalResult::Single(value) => Ok(value), |
| 518 | LocalResult::Ambiguous(earliest, latest) => { |
| 519 | #[cfg (not(Py_LIMITED_API))] |
| 520 | let fold = dt.get_fold(); |
| 521 | |
| 522 | #[cfg (Py_LIMITED_API)] |
| 523 | let fold = dt.getattr(intern!(dt.py(), "fold" ))?.extract::<usize>()? > 0; |
| 524 | |
| 525 | if fold { |
| 526 | Ok(latest) |
| 527 | } else { |
| 528 | Ok(earliest) |
| 529 | } |
| 530 | } |
| 531 | LocalResult::None => Err(PyValueError::new_err(format!( |
| 532 | "The datetime {:?} contains an incompatible timezone" , |
| 533 | dt |
| 534 | ))), |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | #[allow (deprecated)] |
| 540 | impl ToPyObject for FixedOffset { |
| 541 | #[inline ] |
| 542 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 543 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 544 | } |
| 545 | } |
| 546 | |
| 547 | #[allow (deprecated)] |
| 548 | impl IntoPy<PyObject> for FixedOffset { |
| 549 | #[inline ] |
| 550 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 551 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | impl<'py> IntoPyObject<'py> for FixedOffset { |
| 556 | #[cfg (Py_LIMITED_API)] |
| 557 | type Target = PyAny; |
| 558 | #[cfg (not(Py_LIMITED_API))] |
| 559 | type Target = PyTzInfo; |
| 560 | type Output = Bound<'py, Self::Target>; |
| 561 | type Error = PyErr; |
| 562 | |
| 563 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 564 | let seconds_offset: i32 = self.local_minus_utc(); |
| 565 | #[cfg (not(Py_LIMITED_API))] |
| 566 | { |
| 567 | let td = PyDelta::new(py, 0, seconds_offset, 0, true)?; |
| 568 | timezone_from_offset(&td) |
| 569 | } |
| 570 | |
| 571 | #[cfg (Py_LIMITED_API)] |
| 572 | { |
| 573 | let td: Bound<'_, PyAny> = Duration::seconds(seconds_offset.into()).into_pyobject(py)?; |
| 574 | DatetimeTypes::try_get(py).and_then(|dt: &DatetimeTypes| dt.timezone.bind(py).call1((td,))) |
| 575 | } |
| 576 | } |
| 577 | } |
| 578 | |
| 579 | impl<'py> IntoPyObject<'py> for &FixedOffset { |
| 580 | #[cfg (Py_LIMITED_API)] |
| 581 | type Target = PyAny; |
| 582 | #[cfg (not(Py_LIMITED_API))] |
| 583 | type Target = PyTzInfo; |
| 584 | type Output = Bound<'py, Self::Target>; |
| 585 | type Error = PyErr; |
| 586 | |
| 587 | #[inline ] |
| 588 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 589 | (*self).into_pyobject(py) |
| 590 | } |
| 591 | } |
| 592 | |
| 593 | impl FromPyObject<'_> for FixedOffset { |
| 594 | /// Convert python tzinfo to rust [`FixedOffset`]. |
| 595 | /// |
| 596 | /// Note that the conversion will result in precision lost in microseconds as chrono offset |
| 597 | /// does not supports microseconds. |
| 598 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<FixedOffset> { |
| 599 | #[cfg (not(Py_LIMITED_API))] |
| 600 | let ob = ob.downcast::<PyTzInfo>()?; |
| 601 | #[cfg (Py_LIMITED_API)] |
| 602 | check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo" )?; |
| 603 | |
| 604 | // Passing Python's None to the `utcoffset` function will only |
| 605 | // work for timezones defined as fixed offsets in Python. |
| 606 | // Any other timezone would require a datetime as the parameter, and return |
| 607 | // None if the datetime is not provided. |
| 608 | // Trying to convert None to a PyDelta in the next line will then fail. |
| 609 | let py_timedelta = ob.call_method1("utcoffset" , (PyNone::get(ob.py()),))?; |
| 610 | if py_timedelta.is_none() { |
| 611 | return Err(PyTypeError::new_err(format!( |
| 612 | " {:?} is not a fixed offset timezone" , |
| 613 | ob |
| 614 | ))); |
| 615 | } |
| 616 | let total_seconds: Duration = py_timedelta.extract()?; |
| 617 | // This cast is safe since the timedelta is limited to -24 hours and 24 hours. |
| 618 | let total_seconds = total_seconds.num_seconds() as i32; |
| 619 | FixedOffset::east_opt(total_seconds) |
| 620 | .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds" )) |
| 621 | } |
| 622 | } |
| 623 | |
| 624 | #[allow (deprecated)] |
| 625 | impl ToPyObject for Utc { |
| 626 | #[inline ] |
| 627 | fn to_object(&self, py: Python<'_>) -> PyObject { |
| 628 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | #[allow (deprecated)] |
| 633 | impl IntoPy<PyObject> for Utc { |
| 634 | #[inline ] |
| 635 | fn into_py(self, py: Python<'_>) -> PyObject { |
| 636 | self.into_pyobject(py).unwrap().into_any().unbind() |
| 637 | } |
| 638 | } |
| 639 | |
| 640 | impl<'py> IntoPyObject<'py> for Utc { |
| 641 | #[cfg (Py_LIMITED_API)] |
| 642 | type Target = PyAny; |
| 643 | #[cfg (not(Py_LIMITED_API))] |
| 644 | type Target = PyTzInfo; |
| 645 | type Output = Bound<'py, Self::Target>; |
| 646 | type Error = PyErr; |
| 647 | |
| 648 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 649 | #[cfg (Py_LIMITED_API)] |
| 650 | { |
| 651 | Ok(timezone_utc(py).into_any()) |
| 652 | } |
| 653 | #[cfg (not(Py_LIMITED_API))] |
| 654 | { |
| 655 | Ok(timezone_utc(py)) |
| 656 | } |
| 657 | } |
| 658 | } |
| 659 | |
| 660 | impl<'py> IntoPyObject<'py> for &Utc { |
| 661 | #[cfg (Py_LIMITED_API)] |
| 662 | type Target = PyAny; |
| 663 | #[cfg (not(Py_LIMITED_API))] |
| 664 | type Target = PyTzInfo; |
| 665 | type Output = Bound<'py, Self::Target>; |
| 666 | type Error = PyErr; |
| 667 | |
| 668 | #[inline ] |
| 669 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
| 670 | (*self).into_pyobject(py) |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | impl FromPyObject<'_> for Utc { |
| 675 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Utc> { |
| 676 | let py_utc: Bound<'_, PyAny> = timezone_utc(ob.py()); |
| 677 | if ob.eq(py_utc)? { |
| 678 | Ok(Utc) |
| 679 | } else { |
| 680 | Err(PyValueError::new_err(args:"expected datetime.timezone.utc" )) |
| 681 | } |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | struct DateArgs { |
| 686 | year: i32, |
| 687 | month: u8, |
| 688 | day: u8, |
| 689 | } |
| 690 | |
| 691 | impl From<&NaiveDate> for DateArgs { |
| 692 | fn from(value: &NaiveDate) -> Self { |
| 693 | Self { |
| 694 | year: value.year(), |
| 695 | month: value.month() as u8, |
| 696 | day: value.day() as u8, |
| 697 | } |
| 698 | } |
| 699 | } |
| 700 | |
| 701 | struct TimeArgs { |
| 702 | hour: u8, |
| 703 | min: u8, |
| 704 | sec: u8, |
| 705 | micro: u32, |
| 706 | truncated_leap_second: bool, |
| 707 | } |
| 708 | |
| 709 | impl From<&NaiveTime> for TimeArgs { |
| 710 | fn from(value: &NaiveTime) -> Self { |
| 711 | let ns: u32 = value.nanosecond(); |
| 712 | let checked_sub: Option = ns.checked_sub(1_000_000_000); |
| 713 | let truncated_leap_second: bool = checked_sub.is_some(); |
| 714 | let micro: u32 = checked_sub.unwrap_or(default:ns) / 1000; |
| 715 | Self { |
| 716 | hour: value.hour() as u8, |
| 717 | min: value.minute() as u8, |
| 718 | sec: value.second() as u8, |
| 719 | micro, |
| 720 | truncated_leap_second, |
| 721 | } |
| 722 | } |
| 723 | } |
| 724 | |
| 725 | fn naive_datetime_to_py_datetime( |
| 726 | py: Python<'_>, |
| 727 | naive_datetime: &NaiveDateTime, |
| 728 | #[cfg (not(Py_LIMITED_API))] tzinfo: Option<&Bound<'_, PyTzInfo>>, |
| 729 | #[cfg (Py_LIMITED_API)] tzinfo: Option<&Bound<'_, PyAny>>, |
| 730 | ) -> PyObject { |
| 731 | let DateArgs { year: i32, month: u8, day: u8 } = (&naive_datetime.date()).into(); |
| 732 | let TimeArgs { |
| 733 | hour: u8, |
| 734 | min: u8, |
| 735 | sec: u8, |
| 736 | micro: u32, |
| 737 | truncated_leap_second: bool, |
| 738 | } = (&naive_datetime.time()).into(); |
| 739 | #[cfg (not(Py_LIMITED_API))] |
| 740 | let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, tzinfo) |
| 741 | .expect("failed to construct datetime" ); |
| 742 | #[cfg (Py_LIMITED_API)] |
| 743 | let datetime: Bound<'_, PyAny> = DatetimeTypes::get(py) |
| 744 | .datetime |
| 745 | .bind(py) |
| 746 | .call1((year, month, day, hour, min, sec, micro, tzinfo)) |
| 747 | .expect(msg:"failed to construct datetime.datetime" ); |
| 748 | if truncated_leap_second { |
| 749 | warn_truncated_leap_second(&datetime); |
| 750 | } |
| 751 | datetime.into() |
| 752 | } |
| 753 | |
| 754 | fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { |
| 755 | let py: Python<'_> = obj.py(); |
| 756 | if let Err(e: PyErr) = PyErr::warn( |
| 757 | py, |
| 758 | &py.get_type::<PyUserWarning>(), |
| 759 | message:ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds" ), |
| 760 | stacklevel:0, |
| 761 | ) { |
| 762 | e.write_unraisable(py, obj:Some(obj)) |
| 763 | }; |
| 764 | } |
| 765 | |
| 766 | #[cfg (not(Py_LIMITED_API))] |
| 767 | fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult<NaiveDate> { |
| 768 | NaiveDate::from_ymd_opt( |
| 769 | py_date.get_year(), |
| 770 | py_date.get_month().into(), |
| 771 | py_date.get_day().into(), |
| 772 | ) |
| 773 | .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date" )) |
| 774 | } |
| 775 | |
| 776 | #[cfg (Py_LIMITED_API)] |
| 777 | fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> { |
| 778 | NaiveDate::from_ymd_opt( |
| 779 | py_date.getattr(intern!(py_date.py(), "year" ))?.extract()?, |
| 780 | py_date.getattr(intern!(py_date.py(), "month" ))?.extract()?, |
| 781 | py_date.getattr(intern!(py_date.py(), "day" ))?.extract()?, |
| 782 | ) |
| 783 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range date" )) |
| 784 | } |
| 785 | |
| 786 | #[cfg (not(Py_LIMITED_API))] |
| 787 | fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult<NaiveTime> { |
| 788 | NaiveTime::from_hms_micro_opt( |
| 789 | py_time.get_hour().into(), |
| 790 | py_time.get_minute().into(), |
| 791 | py_time.get_second().into(), |
| 792 | py_time.get_microsecond(), |
| 793 | ) |
| 794 | .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time" )) |
| 795 | } |
| 796 | |
| 797 | #[cfg (Py_LIMITED_API)] |
| 798 | fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> { |
| 799 | NaiveTime::from_hms_micro_opt( |
| 800 | py_time.getattr(intern!(py_time.py(), "hour" ))?.extract()?, |
| 801 | py_time |
| 802 | .getattr(intern!(py_time.py(), "minute" ))? |
| 803 | .extract()?, |
| 804 | py_time |
| 805 | .getattr(intern!(py_time.py(), "second" ))? |
| 806 | .extract()?, |
| 807 | py_time |
| 808 | .getattr(intern!(py_time.py(), "microsecond" ))? |
| 809 | .extract()?, |
| 810 | ) |
| 811 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range time" )) |
| 812 | } |
| 813 | |
| 814 | #[cfg (test)] |
| 815 | mod tests { |
| 816 | use super::*; |
| 817 | use crate::{types::PyTuple, BoundObject}; |
| 818 | use std::{cmp::Ordering, panic}; |
| 819 | |
| 820 | #[test ] |
| 821 | // Only Python>=3.9 has the zoneinfo package |
| 822 | // We skip the test on windows too since we'd need to install |
| 823 | // tzdata there to make this work. |
| 824 | #[cfg (all(Py_3_9, not(target_os = "windows" )))] |
| 825 | fn test_zoneinfo_is_not_fixed_offset() { |
| 826 | use crate::ffi; |
| 827 | use crate::types::any::PyAnyMethods; |
| 828 | use crate::types::dict::PyDictMethods; |
| 829 | |
| 830 | Python::with_gil(|py| { |
| 831 | let locals = crate::types::PyDict::new(py); |
| 832 | py.run( |
| 833 | ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')" ), |
| 834 | None, |
| 835 | Some(&locals), |
| 836 | ) |
| 837 | .unwrap(); |
| 838 | let result: PyResult<FixedOffset> = locals.get_item("zi" ).unwrap().unwrap().extract(); |
| 839 | assert!(result.is_err()); |
| 840 | let res = result.err().unwrap(); |
| 841 | // Also check the error message is what we expect |
| 842 | let msg = res.value(py).repr().unwrap().to_string(); |
| 843 | assert_eq!(msg, "TypeError( \"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone \")" ); |
| 844 | }); |
| 845 | } |
| 846 | |
| 847 | #[test ] |
| 848 | fn test_timezone_aware_to_naive_fails() { |
| 849 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
| 850 | // one, the conversion fails. |
| 851 | Python::with_gil(|py| { |
| 852 | let py_datetime = |
| 853 | new_py_datetime_ob(py, "datetime" , (2022, 1, 1, 1, 0, 0, 0, python_utc(py))); |
| 854 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
| 855 | let res: PyResult<NaiveDateTime> = py_datetime.extract(); |
| 856 | assert_eq!( |
| 857 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
| 858 | "TypeError('expected a datetime without tzinfo')" |
| 859 | ); |
| 860 | }); |
| 861 | } |
| 862 | |
| 863 | #[test ] |
| 864 | fn test_naive_to_timezone_aware_fails() { |
| 865 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
| 866 | // one, the conversion fails. |
| 867 | Python::with_gil(|py| { |
| 868 | let py_datetime = new_py_datetime_ob(py, "datetime" , (2022, 1, 1, 1, 0, 0, 0)); |
| 869 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
| 870 | let res: PyResult<DateTime<Utc>> = py_datetime.extract(); |
| 871 | assert_eq!( |
| 872 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
| 873 | "TypeError('expected a datetime with non-None tzinfo')" |
| 874 | ); |
| 875 | |
| 876 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
| 877 | let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract(); |
| 878 | assert_eq!( |
| 879 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
| 880 | "TypeError('expected a datetime with non-None tzinfo')" |
| 881 | ); |
| 882 | }); |
| 883 | } |
| 884 | |
| 885 | #[test ] |
| 886 | fn test_invalid_types_fail() { |
| 887 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
| 888 | // one, the conversion fails. |
| 889 | Python::with_gil(|py| { |
| 890 | let none = py.None().into_bound(py); |
| 891 | assert_eq!( |
| 892 | none.extract::<Duration>().unwrap_err().to_string(), |
| 893 | "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" |
| 894 | ); |
| 895 | assert_eq!( |
| 896 | none.extract::<FixedOffset>().unwrap_err().to_string(), |
| 897 | "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" |
| 898 | ); |
| 899 | assert_eq!( |
| 900 | none.extract::<Utc>().unwrap_err().to_string(), |
| 901 | "ValueError: expected datetime.timezone.utc" |
| 902 | ); |
| 903 | assert_eq!( |
| 904 | none.extract::<NaiveTime>().unwrap_err().to_string(), |
| 905 | "TypeError: 'NoneType' object cannot be converted to 'PyTime'" |
| 906 | ); |
| 907 | assert_eq!( |
| 908 | none.extract::<NaiveDate>().unwrap_err().to_string(), |
| 909 | "TypeError: 'NoneType' object cannot be converted to 'PyDate'" |
| 910 | ); |
| 911 | assert_eq!( |
| 912 | none.extract::<NaiveDateTime>().unwrap_err().to_string(), |
| 913 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
| 914 | ); |
| 915 | assert_eq!( |
| 916 | none.extract::<DateTime<Utc>>().unwrap_err().to_string(), |
| 917 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
| 918 | ); |
| 919 | assert_eq!( |
| 920 | none.extract::<DateTime<FixedOffset>>() |
| 921 | .unwrap_err() |
| 922 | .to_string(), |
| 923 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
| 924 | ); |
| 925 | }); |
| 926 | } |
| 927 | |
| 928 | #[test ] |
| 929 | fn test_pyo3_timedelta_into_pyobject() { |
| 930 | // Utility function used to check different durations. |
| 931 | // The `name` parameter is used to identify the check in case of a failure. |
| 932 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
| 933 | Python::with_gil(|py| { |
| 934 | let delta = delta.into_pyobject(py).unwrap(); |
| 935 | let py_delta = new_py_datetime_ob(py, "timedelta" , (py_days, py_seconds, py_ms)); |
| 936 | assert!( |
| 937 | delta.eq(&py_delta).unwrap(), |
| 938 | "{}: {} != {}" , |
| 939 | name, |
| 940 | delta, |
| 941 | py_delta |
| 942 | ); |
| 943 | }); |
| 944 | }; |
| 945 | |
| 946 | let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10); |
| 947 | check("delta normalization" , delta, -1, 1, -10); |
| 948 | |
| 949 | // Check the minimum value allowed by PyDelta, which is different |
| 950 | // from the minimum value allowed in Duration. This should pass. |
| 951 | let delta = Duration::seconds(-86399999913600); // min |
| 952 | check("delta min value" , delta, -999999999, 0, 0); |
| 953 | |
| 954 | // Same, for max value |
| 955 | let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max |
| 956 | check("delta max value" , delta, 999999999, 86399, 999999); |
| 957 | |
| 958 | // Also check that trying to convert an out of bound value errors. |
| 959 | Python::with_gil(|py| { |
| 960 | // min_value and max_value were deprecated in chrono 0.4.39 |
| 961 | #[allow (deprecated)] |
| 962 | { |
| 963 | assert!(Duration::min_value().into_pyobject(py).is_err()); |
| 964 | assert!(Duration::max_value().into_pyobject(py).is_err()); |
| 965 | } |
| 966 | }); |
| 967 | } |
| 968 | |
| 969 | #[test ] |
| 970 | fn test_pyo3_timedelta_frompyobject() { |
| 971 | // Utility function used to check different durations. |
| 972 | // The `name` parameter is used to identify the check in case of a failure. |
| 973 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
| 974 | Python::with_gil(|py| { |
| 975 | let py_delta = new_py_datetime_ob(py, "timedelta" , (py_days, py_seconds, py_ms)); |
| 976 | let py_delta: Duration = py_delta.extract().unwrap(); |
| 977 | assert_eq!(py_delta, delta, "{}: {} != {}" , name, py_delta, delta); |
| 978 | }) |
| 979 | }; |
| 980 | |
| 981 | // Check the minimum value allowed by PyDelta, which is different |
| 982 | // from the minimum value allowed in Duration. This should pass. |
| 983 | check( |
| 984 | "min py_delta value" , |
| 985 | Duration::seconds(-86399999913600), |
| 986 | -999999999, |
| 987 | 0, |
| 988 | 0, |
| 989 | ); |
| 990 | // Same, for max value |
| 991 | check( |
| 992 | "max py_delta value" , |
| 993 | Duration::seconds(86399999999999) + Duration::microseconds(999999), |
| 994 | 999999999, |
| 995 | 86399, |
| 996 | 999999, |
| 997 | ); |
| 998 | |
| 999 | // This check is to assert that we can't construct every possible Duration from a PyDelta |
| 1000 | // since they have different bounds. |
| 1001 | Python::with_gil(|py| { |
| 1002 | let low_days: i32 = -1000000000; |
| 1003 | // This is possible |
| 1004 | assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok()); |
| 1005 | // This panics on PyDelta::new |
| 1006 | assert!(panic::catch_unwind(|| { |
| 1007 | let py_delta = new_py_datetime_ob(py, "timedelta" , (low_days, 0, 0)); |
| 1008 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
| 1009 | // So we should never get here |
| 1010 | } |
| 1011 | }) |
| 1012 | .is_err()); |
| 1013 | |
| 1014 | let high_days: i32 = 1000000000; |
| 1015 | // This is possible |
| 1016 | assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok()); |
| 1017 | // This panics on PyDelta::new |
| 1018 | assert!(panic::catch_unwind(|| { |
| 1019 | let py_delta = new_py_datetime_ob(py, "timedelta" , (high_days, 0, 0)); |
| 1020 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
| 1021 | // So we should never get here |
| 1022 | } |
| 1023 | }) |
| 1024 | .is_err()); |
| 1025 | }); |
| 1026 | } |
| 1027 | |
| 1028 | #[test ] |
| 1029 | fn test_pyo3_date_into_pyobject() { |
| 1030 | let eq_ymd = |name: &'static str, year, month, day| { |
| 1031 | Python::with_gil(|py| { |
| 1032 | let date = NaiveDate::from_ymd_opt(year, month, day) |
| 1033 | .unwrap() |
| 1034 | .into_pyobject(py) |
| 1035 | .unwrap(); |
| 1036 | let py_date = new_py_datetime_ob(py, "date" , (year, month, day)); |
| 1037 | assert_eq!( |
| 1038 | date.compare(&py_date).unwrap(), |
| 1039 | Ordering::Equal, |
| 1040 | "{}: {} != {}" , |
| 1041 | name, |
| 1042 | date, |
| 1043 | py_date |
| 1044 | ); |
| 1045 | }) |
| 1046 | }; |
| 1047 | |
| 1048 | eq_ymd("past date" , 2012, 2, 29); |
| 1049 | eq_ymd("min date" , 1, 1, 1); |
| 1050 | eq_ymd("future date" , 3000, 6, 5); |
| 1051 | eq_ymd("max date" , 9999, 12, 31); |
| 1052 | } |
| 1053 | |
| 1054 | #[test ] |
| 1055 | fn test_pyo3_date_frompyobject() { |
| 1056 | let eq_ymd = |name: &'static str, year, month, day| { |
| 1057 | Python::with_gil(|py| { |
| 1058 | let py_date = new_py_datetime_ob(py, "date" , (year, month, day)); |
| 1059 | let py_date: NaiveDate = py_date.extract().unwrap(); |
| 1060 | let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); |
| 1061 | assert_eq!(py_date, date, "{}: {} != {}" , name, date, py_date); |
| 1062 | }) |
| 1063 | }; |
| 1064 | |
| 1065 | eq_ymd("past date" , 2012, 2, 29); |
| 1066 | eq_ymd("min date" , 1, 1, 1); |
| 1067 | eq_ymd("future date" , 3000, 6, 5); |
| 1068 | eq_ymd("max date" , 9999, 12, 31); |
| 1069 | } |
| 1070 | |
| 1071 | #[test ] |
| 1072 | fn test_pyo3_datetime_into_pyobject_utc() { |
| 1073 | Python::with_gil(|py| { |
| 1074 | let check_utc = |
| 1075 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
| 1076 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
| 1077 | .unwrap() |
| 1078 | .and_hms_micro_opt(hour, minute, second, ms) |
| 1079 | .unwrap() |
| 1080 | .and_utc(); |
| 1081 | let datetime = datetime.into_pyobject(py).unwrap(); |
| 1082 | let py_datetime = new_py_datetime_ob( |
| 1083 | py, |
| 1084 | "datetime" , |
| 1085 | ( |
| 1086 | year, |
| 1087 | month, |
| 1088 | day, |
| 1089 | hour, |
| 1090 | minute, |
| 1091 | second, |
| 1092 | py_ms, |
| 1093 | python_utc(py), |
| 1094 | ), |
| 1095 | ); |
| 1096 | assert_eq!( |
| 1097 | datetime.compare(&py_datetime).unwrap(), |
| 1098 | Ordering::Equal, |
| 1099 | "{}: {} != {}" , |
| 1100 | name, |
| 1101 | datetime, |
| 1102 | py_datetime |
| 1103 | ); |
| 1104 | }; |
| 1105 | |
| 1106 | check_utc("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
| 1107 | |
| 1108 | #[cfg (not(Py_GIL_DISABLED))] |
| 1109 | assert_warnings!( |
| 1110 | py, |
| 1111 | check_utc("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
| 1112 | [( |
| 1113 | PyUserWarning, |
| 1114 | "ignored leap-second, `datetime` does not support leap-seconds" |
| 1115 | )] |
| 1116 | ); |
| 1117 | }) |
| 1118 | } |
| 1119 | |
| 1120 | #[test ] |
| 1121 | fn test_pyo3_datetime_into_pyobject_fixed_offset() { |
| 1122 | Python::with_gil(|py| { |
| 1123 | let check_fixed_offset = |
| 1124 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
| 1125 | let offset = FixedOffset::east_opt(3600).unwrap(); |
| 1126 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
| 1127 | .unwrap() |
| 1128 | .and_hms_micro_opt(hour, minute, second, ms) |
| 1129 | .unwrap() |
| 1130 | .and_local_timezone(offset) |
| 1131 | .unwrap(); |
| 1132 | let datetime = datetime.into_pyobject(py).unwrap(); |
| 1133 | let py_tz = offset.into_pyobject(py).unwrap(); |
| 1134 | let py_datetime = new_py_datetime_ob( |
| 1135 | py, |
| 1136 | "datetime" , |
| 1137 | (year, month, day, hour, minute, second, py_ms, py_tz), |
| 1138 | ); |
| 1139 | assert_eq!( |
| 1140 | datetime.compare(&py_datetime).unwrap(), |
| 1141 | Ordering::Equal, |
| 1142 | "{}: {} != {}" , |
| 1143 | name, |
| 1144 | datetime, |
| 1145 | py_datetime |
| 1146 | ); |
| 1147 | }; |
| 1148 | |
| 1149 | check_fixed_offset("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
| 1150 | |
| 1151 | #[cfg (not(Py_GIL_DISABLED))] |
| 1152 | assert_warnings!( |
| 1153 | py, |
| 1154 | check_fixed_offset("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
| 1155 | [( |
| 1156 | PyUserWarning, |
| 1157 | "ignored leap-second, `datetime` does not support leap-seconds" |
| 1158 | )] |
| 1159 | ); |
| 1160 | }) |
| 1161 | } |
| 1162 | |
| 1163 | #[test ] |
| 1164 | #[cfg (all(Py_3_9, feature = "chrono-tz" , not(windows)))] |
| 1165 | fn test_pyo3_datetime_into_pyobject_tz() { |
| 1166 | Python::with_gil(|py| { |
| 1167 | let datetime = NaiveDate::from_ymd_opt(2024, 12, 11) |
| 1168 | .unwrap() |
| 1169 | .and_hms_opt(23, 3, 13) |
| 1170 | .unwrap() |
| 1171 | .and_local_timezone(chrono_tz::Tz::Europe__London) |
| 1172 | .unwrap(); |
| 1173 | let datetime = datetime.into_pyobject(py).unwrap(); |
| 1174 | let py_datetime = new_py_datetime_ob( |
| 1175 | py, |
| 1176 | "datetime" , |
| 1177 | ( |
| 1178 | 2024, |
| 1179 | 12, |
| 1180 | 11, |
| 1181 | 23, |
| 1182 | 3, |
| 1183 | 13, |
| 1184 | 0, |
| 1185 | python_zoneinfo(py, "Europe/London" ), |
| 1186 | ), |
| 1187 | ); |
| 1188 | assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal); |
| 1189 | }) |
| 1190 | } |
| 1191 | |
| 1192 | #[test ] |
| 1193 | fn test_pyo3_datetime_frompyobject_utc() { |
| 1194 | Python::with_gil(|py| { |
| 1195 | let year = 2014; |
| 1196 | let month = 5; |
| 1197 | let day = 6; |
| 1198 | let hour = 7; |
| 1199 | let minute = 8; |
| 1200 | let second = 9; |
| 1201 | let micro = 999_999; |
| 1202 | let tz_utc = timezone_utc(py); |
| 1203 | let py_datetime = new_py_datetime_ob( |
| 1204 | py, |
| 1205 | "datetime" , |
| 1206 | (year, month, day, hour, minute, second, micro, tz_utc), |
| 1207 | ); |
| 1208 | let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap(); |
| 1209 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
| 1210 | .unwrap() |
| 1211 | .and_hms_micro_opt(hour, minute, second, micro) |
| 1212 | .unwrap() |
| 1213 | .and_utc(); |
| 1214 | assert_eq!(py_datetime, datetime,); |
| 1215 | }) |
| 1216 | } |
| 1217 | |
| 1218 | #[test ] |
| 1219 | fn test_pyo3_datetime_frompyobject_fixed_offset() { |
| 1220 | Python::with_gil(|py| { |
| 1221 | let year = 2014; |
| 1222 | let month = 5; |
| 1223 | let day = 6; |
| 1224 | let hour = 7; |
| 1225 | let minute = 8; |
| 1226 | let second = 9; |
| 1227 | let micro = 999_999; |
| 1228 | let offset = FixedOffset::east_opt(3600).unwrap(); |
| 1229 | let py_tz = offset.into_pyobject(py).unwrap(); |
| 1230 | let py_datetime = new_py_datetime_ob( |
| 1231 | py, |
| 1232 | "datetime" , |
| 1233 | (year, month, day, hour, minute, second, micro, py_tz), |
| 1234 | ); |
| 1235 | let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap(); |
| 1236 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
| 1237 | .unwrap() |
| 1238 | .and_hms_micro_opt(hour, minute, second, micro) |
| 1239 | .unwrap(); |
| 1240 | let datetime = datetime.and_local_timezone(offset).unwrap(); |
| 1241 | |
| 1242 | assert_eq!(datetime_from_py, datetime); |
| 1243 | assert!( |
| 1244 | py_datetime.extract::<DateTime<Utc>>().is_err(), |
| 1245 | "Extracting Utc from nonzero FixedOffset timezone will fail" |
| 1246 | ); |
| 1247 | |
| 1248 | let utc = python_utc(py); |
| 1249 | let py_datetime_utc = new_py_datetime_ob( |
| 1250 | py, |
| 1251 | "datetime" , |
| 1252 | (year, month, day, hour, minute, second, micro, utc), |
| 1253 | ); |
| 1254 | assert!( |
| 1255 | py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(), |
| 1256 | "Extracting FixedOffset from Utc timezone will succeed" |
| 1257 | ); |
| 1258 | }) |
| 1259 | } |
| 1260 | |
| 1261 | #[test ] |
| 1262 | fn test_pyo3_offset_fixed_into_pyobject() { |
| 1263 | Python::with_gil(|py| { |
| 1264 | // Chrono offset |
| 1265 | let offset = FixedOffset::east_opt(3600) |
| 1266 | .unwrap() |
| 1267 | .into_pyobject(py) |
| 1268 | .unwrap(); |
| 1269 | // Python timezone from timedelta |
| 1270 | let td = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
| 1271 | let py_timedelta = new_py_datetime_ob(py, "timezone" , (td,)); |
| 1272 | // Should be equal |
| 1273 | assert!(offset.eq(py_timedelta).unwrap()); |
| 1274 | |
| 1275 | // Same but with negative values |
| 1276 | let offset = FixedOffset::east_opt(-3600) |
| 1277 | .unwrap() |
| 1278 | .into_pyobject(py) |
| 1279 | .unwrap(); |
| 1280 | let td = new_py_datetime_ob(py, "timedelta" , (0, -3600, 0)); |
| 1281 | let py_timedelta = new_py_datetime_ob(py, "timezone" , (td,)); |
| 1282 | assert!(offset.eq(py_timedelta).unwrap()); |
| 1283 | }) |
| 1284 | } |
| 1285 | |
| 1286 | #[test ] |
| 1287 | fn test_pyo3_offset_fixed_frompyobject() { |
| 1288 | Python::with_gil(|py| { |
| 1289 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
| 1290 | let py_tzinfo = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
| 1291 | let offset: FixedOffset = py_tzinfo.extract().unwrap(); |
| 1292 | assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset); |
| 1293 | }) |
| 1294 | } |
| 1295 | |
| 1296 | #[test ] |
| 1297 | fn test_pyo3_offset_utc_into_pyobject() { |
| 1298 | Python::with_gil(|py| { |
| 1299 | let utc = Utc.into_pyobject(py).unwrap(); |
| 1300 | let py_utc = python_utc(py); |
| 1301 | assert!(utc.is(&py_utc)); |
| 1302 | }) |
| 1303 | } |
| 1304 | |
| 1305 | #[test ] |
| 1306 | fn test_pyo3_offset_utc_frompyobject() { |
| 1307 | Python::with_gil(|py| { |
| 1308 | let py_utc = python_utc(py); |
| 1309 | let py_utc: Utc = py_utc.extract().unwrap(); |
| 1310 | assert_eq!(Utc, py_utc); |
| 1311 | |
| 1312 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 0, 0)); |
| 1313 | let py_timezone_utc = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
| 1314 | let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap(); |
| 1315 | assert_eq!(Utc, py_timezone_utc); |
| 1316 | |
| 1317 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
| 1318 | let py_timezone = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
| 1319 | assert!(py_timezone.extract::<Utc>().is_err()); |
| 1320 | }) |
| 1321 | } |
| 1322 | |
| 1323 | #[test ] |
| 1324 | fn test_pyo3_time_into_pyobject() { |
| 1325 | Python::with_gil(|py| { |
| 1326 | let check_time = |name: &'static str, hour, minute, second, ms, py_ms| { |
| 1327 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms) |
| 1328 | .unwrap() |
| 1329 | .into_pyobject(py) |
| 1330 | .unwrap(); |
| 1331 | let py_time = new_py_datetime_ob(py, "time" , (hour, minute, second, py_ms)); |
| 1332 | assert!( |
| 1333 | time.eq(&py_time).unwrap(), |
| 1334 | "{}: {} != {}" , |
| 1335 | name, |
| 1336 | time, |
| 1337 | py_time |
| 1338 | ); |
| 1339 | }; |
| 1340 | |
| 1341 | check_time("regular" , 3, 5, 7, 999_999, 999_999); |
| 1342 | |
| 1343 | #[cfg (not(Py_GIL_DISABLED))] |
| 1344 | assert_warnings!( |
| 1345 | py, |
| 1346 | check_time("leap second" , 3, 5, 59, 1_999_999, 999_999), |
| 1347 | [( |
| 1348 | PyUserWarning, |
| 1349 | "ignored leap-second, `datetime` does not support leap-seconds" |
| 1350 | )] |
| 1351 | ); |
| 1352 | }) |
| 1353 | } |
| 1354 | |
| 1355 | #[test ] |
| 1356 | fn test_pyo3_time_frompyobject() { |
| 1357 | let hour = 3; |
| 1358 | let minute = 5; |
| 1359 | let second = 7; |
| 1360 | let micro = 999_999; |
| 1361 | Python::with_gil(|py| { |
| 1362 | let py_time = new_py_datetime_ob(py, "time" , (hour, minute, second, micro)); |
| 1363 | let py_time: NaiveTime = py_time.extract().unwrap(); |
| 1364 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap(); |
| 1365 | assert_eq!(py_time, time); |
| 1366 | }) |
| 1367 | } |
| 1368 | |
| 1369 | fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny> |
| 1370 | where |
| 1371 | A: IntoPyObject<'py, Target = PyTuple>, |
| 1372 | { |
| 1373 | py.import("datetime" ) |
| 1374 | .unwrap() |
| 1375 | .getattr(name) |
| 1376 | .unwrap() |
| 1377 | .call1( |
| 1378 | args.into_pyobject(py) |
| 1379 | .map_err(Into::into) |
| 1380 | .unwrap() |
| 1381 | .into_bound(), |
| 1382 | ) |
| 1383 | .unwrap() |
| 1384 | } |
| 1385 | |
| 1386 | fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> { |
| 1387 | py.import("datetime" ) |
| 1388 | .unwrap() |
| 1389 | .getattr("timezone" ) |
| 1390 | .unwrap() |
| 1391 | .getattr("utc" ) |
| 1392 | .unwrap() |
| 1393 | } |
| 1394 | |
| 1395 | #[cfg (all(Py_3_9, feature = "chrono-tz" , not(windows)))] |
| 1396 | fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> { |
| 1397 | py.import("zoneinfo" ) |
| 1398 | .unwrap() |
| 1399 | .getattr("ZoneInfo" ) |
| 1400 | .unwrap() |
| 1401 | .call1((timezone,)) |
| 1402 | .unwrap() |
| 1403 | } |
| 1404 | |
| 1405 | #[cfg (not(any(target_arch = "wasm32" , Py_GIL_DISABLED)))] |
| 1406 | mod proptests { |
| 1407 | use super::*; |
| 1408 | use crate::tests::common::CatchWarnings; |
| 1409 | use crate::types::IntoPyDict; |
| 1410 | use proptest::prelude::*; |
| 1411 | use std::ffi::CString; |
| 1412 | |
| 1413 | proptest! { |
| 1414 | |
| 1415 | // Range is limited to 1970 to 2038 due to windows limitations |
| 1416 | #[test] |
| 1417 | fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { |
| 1418 | Python::with_gil(|py| { |
| 1419 | |
| 1420 | let globals = [("datetime" , py.import("datetime" ).unwrap())].into_py_dict(py).unwrap(); |
| 1421 | let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))" , timestamp, timedelta); |
| 1422 | let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); |
| 1423 | |
| 1424 | // Get ISO 8601 string from python |
| 1425 | let py_iso_str = t.call_method0("isoformat" ).unwrap(); |
| 1426 | |
| 1427 | // Get ISO 8601 string from rust |
| 1428 | let t = t.extract::<DateTime<FixedOffset>>().unwrap(); |
| 1429 | // Python doesn't print the seconds of the offset if they are 0 |
| 1430 | let rust_iso_str = if timedelta % 60 == 0 { |
| 1431 | t.format("%Y-%m-%dT%H:%M:%S%:z" ).to_string() |
| 1432 | } else { |
| 1433 | t.format("%Y-%m-%dT%H:%M:%S%::z" ).to_string() |
| 1434 | }; |
| 1435 | |
| 1436 | // They should be equal |
| 1437 | assert_eq!(py_iso_str.to_string(), rust_iso_str); |
| 1438 | }) |
| 1439 | } |
| 1440 | |
| 1441 | #[test] |
| 1442 | fn test_duration_roundtrip(days in -999999999i64..=999999999i64) { |
| 1443 | // Test roundtrip conversion rust->python->rust for all allowed |
| 1444 | // python values of durations (from -999999999 to 999999999 days), |
| 1445 | Python::with_gil(|py| { |
| 1446 | let dur = Duration::days(days); |
| 1447 | let py_delta = dur.into_pyobject(py).unwrap(); |
| 1448 | let roundtripped: Duration = py_delta.extract().expect("Round trip" ); |
| 1449 | assert_eq!(dur, roundtripped); |
| 1450 | }) |
| 1451 | } |
| 1452 | |
| 1453 | #[test] |
| 1454 | fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) { |
| 1455 | Python::with_gil(|py| { |
| 1456 | let offset = FixedOffset::east_opt(secs).unwrap(); |
| 1457 | let py_offset = offset.into_pyobject(py).unwrap(); |
| 1458 | let roundtripped: FixedOffset = py_offset.extract().expect("Round trip" ); |
| 1459 | assert_eq!(offset, roundtripped); |
| 1460 | }) |
| 1461 | } |
| 1462 | |
| 1463 | #[test] |
| 1464 | fn test_naive_date_roundtrip( |
| 1465 | year in 1i32..=9999i32, |
| 1466 | month in 1u32..=12u32, |
| 1467 | day in 1u32..=31u32 |
| 1468 | ) { |
| 1469 | // Test roundtrip conversion rust->python->rust for all allowed |
| 1470 | // python dates (from year 1 to year 9999) |
| 1471 | Python::with_gil(|py| { |
| 1472 | // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s. |
| 1473 | // This is to skip the test if we are creating an invalid date, like February 31. |
| 1474 | if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { |
| 1475 | let py_date = date.into_pyobject(py).unwrap(); |
| 1476 | let roundtripped: NaiveDate = py_date.extract().expect("Round trip" ); |
| 1477 | assert_eq!(date, roundtripped); |
| 1478 | } |
| 1479 | }) |
| 1480 | } |
| 1481 | |
| 1482 | #[test] |
| 1483 | fn test_naive_time_roundtrip( |
| 1484 | hour in 0u32..=23u32, |
| 1485 | min in 0u32..=59u32, |
| 1486 | sec in 0u32..=59u32, |
| 1487 | micro in 0u32..=1_999_999u32 |
| 1488 | ) { |
| 1489 | // Test roundtrip conversion rust->python->rust for naive times. |
| 1490 | // Python time has a resolution of microseconds, so we only test |
| 1491 | // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond |
| 1492 | // resolution. |
| 1493 | Python::with_gil(|py| { |
| 1494 | if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) { |
| 1495 | // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second |
| 1496 | let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap(); |
| 1497 | let roundtripped: NaiveTime = py_time.extract().expect("Round trip" ); |
| 1498 | // Leap seconds are not roundtripped |
| 1499 | 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); |
| 1500 | assert_eq!(expected_roundtrip_time, roundtripped); |
| 1501 | } |
| 1502 | }) |
| 1503 | } |
| 1504 | |
| 1505 | #[test] |
| 1506 | fn test_naive_datetime_roundtrip( |
| 1507 | year in 1i32..=9999i32, |
| 1508 | month in 1u32..=12u32, |
| 1509 | day in 1u32..=31u32, |
| 1510 | hour in 0u32..=24u32, |
| 1511 | min in 0u32..=60u32, |
| 1512 | sec in 0u32..=60u32, |
| 1513 | micro in 0u32..=999_999u32 |
| 1514 | ) { |
| 1515 | Python::with_gil(|py| { |
| 1516 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
| 1517 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
| 1518 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
| 1519 | let dt = NaiveDateTime::new(date, time); |
| 1520 | let pydt = dt.into_pyobject(py).unwrap(); |
| 1521 | let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip" ); |
| 1522 | assert_eq!(dt, roundtripped); |
| 1523 | } |
| 1524 | }) |
| 1525 | } |
| 1526 | |
| 1527 | #[test] |
| 1528 | fn test_utc_datetime_roundtrip( |
| 1529 | year in 1i32..=9999i32, |
| 1530 | month in 1u32..=12u32, |
| 1531 | day in 1u32..=31u32, |
| 1532 | hour in 0u32..=23u32, |
| 1533 | min in 0u32..=59u32, |
| 1534 | sec in 0u32..=59u32, |
| 1535 | micro in 0u32..=1_999_999u32 |
| 1536 | ) { |
| 1537 | Python::with_gil(|py| { |
| 1538 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
| 1539 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
| 1540 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
| 1541 | let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc(); |
| 1542 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
| 1543 | let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); |
| 1544 | let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip" ); |
| 1545 | // Leap seconds are not roundtripped |
| 1546 | 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); |
| 1547 | let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc(); |
| 1548 | assert_eq!(expected_roundtrip_dt, roundtripped); |
| 1549 | } |
| 1550 | }) |
| 1551 | } |
| 1552 | |
| 1553 | #[test] |
| 1554 | fn test_fixed_offset_datetime_roundtrip( |
| 1555 | year in 1i32..=9999i32, |
| 1556 | month in 1u32..=12u32, |
| 1557 | day in 1u32..=31u32, |
| 1558 | hour in 0u32..=23u32, |
| 1559 | min in 0u32..=59u32, |
| 1560 | sec in 0u32..=59u32, |
| 1561 | micro in 0u32..=1_999_999u32, |
| 1562 | offset_secs in -86399i32..=86399i32 |
| 1563 | ) { |
| 1564 | Python::with_gil(|py| { |
| 1565 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
| 1566 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
| 1567 | let offset = FixedOffset::east_opt(offset_secs).unwrap(); |
| 1568 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
| 1569 | let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap(); |
| 1570 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
| 1571 | let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); |
| 1572 | let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip" ); |
| 1573 | // Leap seconds are not roundtripped |
| 1574 | 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); |
| 1575 | let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap(); |
| 1576 | assert_eq!(expected_roundtrip_dt, roundtripped); |
| 1577 | } |
| 1578 | }) |
| 1579 | } |
| 1580 | } |
| 1581 | } |
| 1582 | } |
| 1583 | |