Skip to content

Commit

Permalink
feat: add uuid support
Browse files Browse the repository at this point in the history
  • Loading branch information
JeanArhancet committed Jan 4, 2025
1 parent ea8c461 commit bdfa56a
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -133,6 +134,7 @@ full = [
"rust_decimal",
"serde",
"smallvec",
"uuid",
]

[workspace]
Expand Down
4 changes: 4 additions & 0 deletions guide/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions src/conversions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ pub mod rust_decimal;
pub mod serde;
pub mod smallvec;
mod std;
pub mod uuid;
301 changes: 301 additions & 0 deletions src/conversions/uuid.rs
Original file line number Diff line number Diff line change
@@ -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> {
//! 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<Py<PyType>> = 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<Self> {
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::<PyBytes>() || obj.is_instance_of::<PyByteArray>() {
let bytes = if let Ok(py_bytes) = obj.downcast::<PyBytes>() {
py_bytes.as_bytes()
} else if let Ok(py_bytearray) = obj.downcast::<PyByteArray>() {
&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::<PyInt>() {
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<PyObject> 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<Self::Output, Self::Error> {
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<Uuid, PyErr> = 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<Uuid, PyErr> = 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<Uuid, PyErr> = 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<Uuid, PyErr> = s.extract();
assert!(uuid.is_err())
});
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]`<T>` for all `T` that implement [`Serialize`] and [`Deserialize`].
//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`].
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bdfa56a

Please sign in to comment.