Adds std::duration::Duration from/to Python conversions
This commit is contained in:
parent
ee54132ff6
commit
8b614745cf
|
@ -33,7 +33,7 @@ The table below contains the Python type and the corresponding function argument
|
||||||
| `datetime.date` | - | `&PyDate` |
|
| `datetime.date` | - | `&PyDate` |
|
||||||
| `datetime.time` | - | `&PyTime` |
|
| `datetime.time` | - | `&PyTime` |
|
||||||
| `datetime.tzinfo` | - | `&PyTzInfo` |
|
| `datetime.tzinfo` | - | `&PyTzInfo` |
|
||||||
| `datetime.timedelta` | - | `&PyDelta` |
|
| `datetime.timedelta` | `Duration` | `&PyDelta` |
|
||||||
| `decimal.Decimal` | `rust_decimal::Decimal`[^5] | - |
|
| `decimal.Decimal` | `rust_decimal::Decimal`[^5] | - |
|
||||||
| `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - |
|
| `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - |
|
||||||
| `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - |
|
| `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - |
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
`FromPyObject`, `IntoPy<PyObject>` and `ToPyObject` are implemented on `std::duration::Duration`
|
|
@ -564,7 +564,7 @@ fn timezone_utc(py: Python<'_>) -> &PyAny {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{tests::common::CatchWarnings, types::PyTuple, Py, PyTypeInfo};
|
use crate::{types::PyTuple, Py};
|
||||||
use std::{cmp::Ordering, panic};
|
use std::{cmp::Ordering, panic};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1090,9 +1090,10 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod proptests {
|
mod proptests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::tests::common::CatchWarnings;
|
||||||
use crate::types::IntoPyDict;
|
use crate::types::IntoPyDict;
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
use crate::exceptions::{PyUserWarning, PyValueError};
|
||||||
|
#[cfg(Py_LIMITED_API)]
|
||||||
|
use crate::sync::GILOnceCell;
|
||||||
|
#[cfg(Py_LIMITED_API)]
|
||||||
|
use crate::types::PyType;
|
||||||
|
#[cfg(not(Py_LIMITED_API))]
|
||||||
|
use crate::types::{PyDelta, PyDeltaAccess};
|
||||||
|
#[cfg(Py_LIMITED_API)]
|
||||||
|
use crate::{intern, Py};
|
||||||
|
use crate::{FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
|
||||||
|
|
||||||
|
impl FromPyObject<'_> for Duration {
|
||||||
|
fn extract(obj: &PyAny) -> PyResult<Self> {
|
||||||
|
#[cfg(not(Py_LIMITED_API))]
|
||||||
|
let (days, seconds, microseconds) = {
|
||||||
|
let delta: &PyDelta = obj.downcast()?;
|
||||||
|
(
|
||||||
|
delta.get_days(),
|
||||||
|
delta.get_seconds(),
|
||||||
|
delta.get_microseconds(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
#[cfg(Py_LIMITED_API)]
|
||||||
|
let (days, seconds, microseconds): (i32, i32, i32) = {
|
||||||
|
(
|
||||||
|
obj.getattr(intern!(obj.py(), "days"))?.extract()?,
|
||||||
|
obj.getattr(intern!(obj.py(), "seconds"))?.extract()?,
|
||||||
|
obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// We cast
|
||||||
|
let days = u64::try_from(days).map_err(|_| {
|
||||||
|
PyValueError::new_err(
|
||||||
|
"It is not possible to convert a negative timedelta to a Rust Duration",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24
|
||||||
|
let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000
|
||||||
|
|
||||||
|
// We convert
|
||||||
|
let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow
|
||||||
|
let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000
|
||||||
|
|
||||||
|
Ok(Duration::new(total_seconds, nanoseconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToPyObject for Duration {
|
||||||
|
fn to_object(&self, py: Python<'_>) -> PyObject {
|
||||||
|
let days = self.as_secs() / SECONDS_PER_DAY;
|
||||||
|
let seconds = self.as_secs() % SECONDS_PER_DAY;
|
||||||
|
let microseconds = self.subsec_micros();
|
||||||
|
|
||||||
|
#[cfg(not(Py_LIMITED_API))]
|
||||||
|
let delta = PyDelta::new(
|
||||||
|
py,
|
||||||
|
days.try_into()
|
||||||
|
.expect("Too large Rust duration for timedelta"),
|
||||||
|
seconds.try_into().unwrap(),
|
||||||
|
microseconds.try_into().unwrap(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("failed to construct timedelta (overflow?)");
|
||||||
|
#[cfg(Py_LIMITED_API)]
|
||||||
|
let delta = {
|
||||||
|
static TIMEDELTA: GILOnceCell<Py<PyType>> = GILOnceCell::new();
|
||||||
|
TIMEDELTA
|
||||||
|
.get_or_try_init_type_ref(py, "datetime", "timedelta")
|
||||||
|
.unwrap()
|
||||||
|
.call1((days, seconds, microseconds))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.subsec_nanos() % 1_000 != 0 {
|
||||||
|
warn_truncated_nanoseconds(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
delta.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoPy<PyObject> for Duration {
|
||||||
|
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||||
|
self.to_object(py)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warn_truncated_nanoseconds(obj: &PyAny) {
|
||||||
|
let py = obj.py();
|
||||||
|
if let Err(e) = PyErr::warn(
|
||||||
|
py,
|
||||||
|
py.get_type::<PyUserWarning>(),
|
||||||
|
"ignored nanoseconds, `datetime.timedelta` does not support nanoseconds",
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
e.write_unraisable(py, Some(obj))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frompyobject() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 0, 0, 0).extract::<Duration>().unwrap(),
|
||||||
|
Duration::new(0, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 1, 0, 0).extract::<Duration>().unwrap(),
|
||||||
|
Duration::new(86400, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 0, 1, 0).extract::<Duration>().unwrap(),
|
||||||
|
Duration::new(1, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 0, 0, 1).extract::<Duration>().unwrap(),
|
||||||
|
Duration::new(0, 1_000)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 1, 1, 1).extract::<Duration>().unwrap(),
|
||||||
|
Duration::new(86401, 1_000)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
timedelta_class(py)
|
||||||
|
.getattr("max")
|
||||||
|
.unwrap()
|
||||||
|
.extract::<Duration>()
|
||||||
|
.unwrap(),
|
||||||
|
Duration::new(86399999999999, 999999000)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_frompyobject_negative() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
assert_eq!(
|
||||||
|
new_timedelta(py, 0, -1, 0)
|
||||||
|
.extract::<Duration>()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
"ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_topyobject() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let assert_eq = |l: PyObject, r: &PyAny| {
|
||||||
|
assert!(l.as_ref(py).eq(r).unwrap());
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(0, 0).to_object(py),
|
||||||
|
new_timedelta(py, 0, 0, 0),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(86400, 0).to_object(py),
|
||||||
|
new_timedelta(py, 1, 0, 0),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(1, 0).to_object(py),
|
||||||
|
new_timedelta(py, 0, 1, 0),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(0, 1_000).to_object(py),
|
||||||
|
new_timedelta(py, 0, 0, 1),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(0, 1).to_object(py),
|
||||||
|
new_timedelta(py, 0, 0, 0),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(86401, 1_000).to_object(py),
|
||||||
|
new_timedelta(py, 1, 1, 1),
|
||||||
|
);
|
||||||
|
assert_eq(
|
||||||
|
Duration::new(86399999999999, 999999000).to_object(py),
|
||||||
|
timedelta_class(py).getattr("max").unwrap(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_topyobject_overflow() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_topyobject_precision_loss() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
assert_warnings!(
|
||||||
|
py,
|
||||||
|
Duration::new(0, 1).to_object(py),
|
||||||
|
[(
|
||||||
|
PyUserWarning,
|
||||||
|
"ignored nanoseconds, `datetime.timedelta` does not support nanoseconds"
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_timedelta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> &PyAny {
|
||||||
|
timedelta_class(py)
|
||||||
|
.call1((days, seconds, microseconds))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timedelta_class(py: Python<'_>) -> &PyAny {
|
||||||
|
py.import("datetime").unwrap().getattr("timedelta").unwrap()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
mod array;
|
mod array;
|
||||||
|
mod duration;
|
||||||
mod ipaddr;
|
mod ipaddr;
|
||||||
mod map;
|
mod map;
|
||||||
mod num;
|
mod num;
|
||||||
|
|
|
@ -908,7 +908,6 @@ impl_signed_integer!(isize);
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::PyErrState;
|
use super::PyErrState;
|
||||||
use crate::exceptions::{self, PyTypeError, PyValueError};
|
use crate::exceptions::{self, PyTypeError, PyValueError};
|
||||||
use crate::tests::common::CatchWarnings;
|
|
||||||
use crate::{PyErr, PyTypeInfo, Python};
|
use crate::{PyErr, PyTypeInfo, Python};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -129,9 +129,9 @@ mod inner {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! assert_warnings {
|
macro_rules! assert_warnings {
|
||||||
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
|
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
|
||||||
CatchWarnings::enter($py, |w| {
|
$crate::tests::common::CatchWarnings::enter($py, |w| {
|
||||||
$body;
|
$body;
|
||||||
let expected_warnings = [$((<$category>::type_object($py), $message)),+];
|
let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+];
|
||||||
assert_eq!(w.len(), expected_warnings.len());
|
assert_eq!(w.len(), expected_warnings.len());
|
||||||
for (warning, (category, message)) in w.iter().zip(expected_warnings) {
|
for (warning, (category, message)) in w.iter().zip(expected_warnings) {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue