diff --git a/Cargo.toml b/Cargo.toml index 18cceffbd0c..807395006c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ num-rational = {version = "0.4.1", optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } +uuid = { version = "1.11.0", optional = true } [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -133,6 +134,7 @@ full = [ "rust_decimal", "serde", "smallvec", + "uuid", ] [workspace] diff --git a/guide/src/features.md b/guide/src/features.md index d801e2dd1e4..931275af0fa 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -199,3 +199,7 @@ struct User { Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversions into its [`SmallVec`](https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html) type. [set-configuration-options]: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options + +### `uuid` + +Adds a dependency on [uuid](https://docs.rs/uuid) and enables conversions into its [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) type. \ No newline at end of file diff --git a/newsfragments/4806.fixed.md b/newsfragments/4806.fixed.md new file mode 100644 index 00000000000..b5f3d8a554d --- /dev/null +++ b/newsfragments/4806.fixed.md @@ -0,0 +1 @@ +Fix generating import lib for PyPy when `abi3` feature is enabled. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 685600df096..2d1de478390 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -565,7 +565,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // Auto generate python3.dll import libraries for Windows targets. if self.lib_dir.is_none() { let target = target_triple_from_env(); - let py_version = if self.abi3 { None } else { Some(self.version) }; + let py_version = if self.implementation == PythonImplementation::CPython && self.abi3 { + None + } else { + Some(self.version) + }; let abiflags = if self.is_free_threaded() { Some("t") } else { diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 53ecf849c07..72a60b9c873 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -14,3 +14,4 @@ pub mod rust_decimal; pub mod serde; pub mod smallvec; mod std; +pub mod uuid; diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs new file mode 100644 index 00000000000..1ba16462b8e --- /dev/null +++ b/src/conversions/uuid.rs @@ -0,0 +1,301 @@ +#![cfg(feature = "uuid")] + +//! Conversions to and from [uuid](https://docs.rs/uuid/latest/uuid/)'s [`Uuid`] type. +//! +//! This is useful for converting Python's uuid.UUID into and from a native Rust type. +//! +//! # Setup +//! +//! To use this feature, add to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"uuid\"] }")] +//! uuid = "1.11.0" +//! ``` +//! +//! Note that you must use a compatible version of uuid and PyO3. +//! The required uuid version may vary based on the version of PyO3. +//! +//! # Example +//! +//! Rust code to create a function that parses a UUID string and returns it as a `Uuid`: +//! +//! ```rust +//! use pyo3::prelude::*; +//! use pyo3::exceptions::PyValueError; +//! use uuid::Uuid; +//! +//! #[pyfunction] +//! fn parse_uuid(s: &str) -> PyResult { +//! Uuid::parse_str(s).map_err(|e| PyValueError::new_err(e.to_string())) +//! } +//! +//! #[pymodule] +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(parse_uuid, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code that validates the functionality +//! +//! +//! ```python +//! from my_module import parse_uuid +//! import uuid +//! +//! py_uuid = uuid.uuid4() +//! rust_uuid = parse_uuid(str(py_uuid)) +//! +//! assert py_uuid == rust_uuid +//! ``` +use uuid::Uuid; + +use crate::conversion::IntoPyObject; +use crate::exceptions::{PyTypeError, PyValueError}; +use crate::instance::Bound; +use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +use crate::types::bytearray::PyByteArrayMethods; +use crate::types::{ + IntoPyDict, PyByteArray, PyBytes, PyBytesMethods, PyInt, PyStringMethods, PyType, +}; +use crate::{FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; + +static UUID_CLS: GILOnceCell> = GILOnceCell::new(); + +fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + UUID_CLS.import(py, "uuid", "UUID") +} + +impl FromPyObject<'_> for Uuid { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let py = obj.py(); + + if let Ok(uuid_cls) = get_uuid_cls(py) { + if obj.is_exact_instance(&uuid_cls) { + let uuid_int: u128 = obj.getattr("int")?.extract()?; + return Ok(Uuid::from_u128(uuid_int.to_le())); + } + } + + if obj.is_instance_of::() || obj.is_instance_of::() { + let bytes = if let Ok(py_bytes) = obj.downcast::() { + py_bytes.as_bytes() + } else if let Ok(py_bytearray) = obj.downcast::() { + &py_bytearray.to_vec() + } else { + return Err(PyTypeError::new_err( + "Expected bytes or bytearray for UUID extraction.", + )); + }; + + return Uuid::from_slice(bytes) + .map_err(|_| PyValueError::new_err("The given bytes value is not a valid UUID.")); + } + + if obj.is_instance_of::() { + let uuid_int: u128 = obj.extract().map_err(|_| { + PyTypeError::new_err( + "Expected integer for UUID extraction but got an incompatible type.", + ) + })?; + return Ok(Uuid::from_u128(uuid_int)); + } + + let py_str = &obj.str()?; + let rs_str = &py_str.to_cow()?; + Uuid::parse_str(&rs_str) + .map_err(|e| PyValueError::new_err(format!("Invalid UUID string: {e}"))) + } +} + +#[allow(deprecated)] +impl ToPyObject for Uuid { + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +#[allow(deprecated)] +impl IntoPy for Uuid { + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for Uuid { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let uuid_cls = get_uuid_cls(py)?; + let kwargs = [("int", self.as_u128())].into_py_dict(py)?; + + Ok(uuid_cls + .call((), Some(&kwargs)) + .expect("failed to call uuid.UUID") + .into_pyobject(py)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::dict::PyDictMethods; + use crate::types::{PyDict, PyString}; + use std::ffi::CString; + use uuid::Uuid; + + macro_rules! convert_constants { + ($name:ident, $rs:expr, $py:literal) => { + #[test] + fn $name() -> PyResult<()> { + Python::with_gil(|py| { + let rs_orig = $rs; + let rs_uuid = rs_orig.into_pyobject(py).unwrap(); + let locals = PyDict::new(py); + locals.set_item("rs_uuid", &rs_uuid).unwrap(); + + py.run( + &CString::new(format!( + "import uuid\npy_uuid = uuid.UUID('{}')\nassert py_uuid == rs_uuid", + $py + )) + .unwrap(), + None, + Some(&locals), + ) + .unwrap(); + + let py_uuid = locals.get_item("py_uuid").unwrap().unwrap(); + let py_result: Uuid = py_uuid.extract().unwrap(); + assert_eq!(rs_orig, py_result); + + Ok(()) + }) + } + }; + } + + convert_constants!( + convert_nil, + Uuid::nil(), + "00000000-0000-0000-0000-000000000000" + ); + convert_constants!( + convert_max, + Uuid::max(), + "ffffffff-ffff-ffff-ffff-ffffffffffff" + ); + + convert_constants!( + convert_uuid_v4, + Uuid::parse_str("a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00").unwrap(), + "a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00" + ); + + convert_constants!( + convert_uuid_v3, + Uuid::parse_str("6fa459ea-ee8a-3ca4-894e-db77e160355e").unwrap(), + "6fa459ea-ee8a-3ca4-894e-db77e160355e" + ); + + convert_constants!( + convert_uuid_v1, + Uuid::parse_str("a6cc5730-2261-11ee-9c43-2eb5a363657c").unwrap(), + "a6cc5730-2261-11ee-9c43-2eb5a363657c" + ); + + #[test] + fn test_uuid_str() { + Python::with_gil(|py| { + let s = PyString::new(py, "a6cc5730-2261-11ee-9c43-2eb5a363657c"); + let uuid: Uuid = s.extract().unwrap(); + assert_eq!( + uuid, + Uuid::parse_str("a6cc5730-2261-11ee-9c43-2eb5a363657c").unwrap() + ); + }); + } + + #[test] + fn test_uuid_bytes() { + Python::with_gil(|py| { + let s = PyBytes::new( + py, + &[ + 0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, + 0xd6, 0xd7, 0xd8, + ], + ); + let uuid: Uuid = s.extract().unwrap(); + assert_eq!( + uuid, + Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8").unwrap() + ); + }); + } + + #[test] + fn test_invalid_uuid_bytes() { + Python::with_gil(|py| { + let s = PyBytes::new( + py, + &[ + 0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, + 0xd6, 0xd7, + ], + ); + let uuid: Result = s.extract(); + assert!(uuid.is_err()) + }); + } + + #[test] + fn test_uuid_int() { + Python::with_gil(|py| { + let v = 0xa1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8u128; + let obj: Bound<'_, PyInt> = v.into_pyobject(py).unwrap(); + let uuid: Uuid = obj.extract().unwrap(); + assert_eq!( + uuid, + Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8").unwrap() + ); + }); + } + + #[test] + fn test_invalid_uuid_int() { + Python::with_gil(|py| { + let v = -42; + let obj: Bound<'_, PyInt> = v.into_pyobject(py).unwrap(); + let uuid: Result = obj.extract(); + assert!(uuid.is_err()) + }); + } + + #[test] + fn test_uuid_incorrect_length() { + Python::with_gil(|py| { + let s = PyString::new(py, "123e4567-e89b-12d3-a456-42661417400"); + let uuid: Result = s.extract(); + assert!(uuid.is_err()) + }); + } + + #[test] + fn test_invalid_uuid_string() { + Python::with_gil(|py| { + let s = PyString::new(py, "invalid-uuid-str"); + let uuid: Result = s.extract(); + assert!(uuid.is_err()) + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 46a2fd53d32..5196a05330b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,7 @@ //! - [`num-rational`]: Enables conversions between Python's fractions.Fraction and [num-rational]'s types //! - [`rust_decimal`]: Enables conversions between Python's decimal.Decimal and [rust_decimal]'s //! [`Decimal`] type. +//! - [`uuid`]: Enables conversions between Python's uuid.UUID and [uuid]'s [`Uuid`] type. //! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for //! [`Py`]`` for all `T` that implement [`Serialize`] and [`Deserialize`]. //! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`]. @@ -286,6 +287,7 @@ //! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html //! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html //! [`SmallVec`]: https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html +//! [`Uuid`]: https://docs.rs/uuid/latest/uuid/struct.Uuid.html //! [`IndexMap`]: https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html //! [`BigInt`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html //! [`BigUint`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html