Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UUID conversion #1

Open
wants to merge 1 commit into
base: build-config-0.23.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 newsfragments/4806.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix generating import lib for PyPy when `abi3` feature is enabled.
6 changes: 5 additions & 1 deletion pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
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
Loading