Skip to main content

pyo3/conversions/
uuid.rs

1#![cfg(feature = "uuid")]
2
3//! Conversions to and from [uuid](https://docs.rs/uuid/latest/uuid/)'s [`Uuid`] and [`NonNilUuid`] types.
4//!
5//! This is useful for converting Python's uuid.UUID into and from a native Rust type.
6//!
7//! # Setup
8//!
9//! To use this feature, add to your **`Cargo.toml`**:
10//!
11//! ```toml
12//! [dependencies]
13#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"uuid\"] }")]
14//! uuid = "1.11.0"
15//! ```
16//!
17//! Note that you must use a compatible version of uuid and PyO3.
18//! The required uuid version may vary based on the version of PyO3.
19//!
20//! # Example
21//!
22//! Rust code to create a function that parses a UUID string and returns it as a `Uuid`:
23//!
24//! ```rust,no_run
25//! use pyo3::prelude::*;
26//! use pyo3::exceptions::PyValueError;
27//! use uuid::Uuid;
28//!
29//! /// Parse a UUID from a string.
30//! #[pyfunction]
31//! fn get_uuid_from_str(s: &str) -> PyResult<Uuid> {
32//!     Uuid::parse_str(s).map_err(|e| PyValueError::new_err(e.to_string()))
33//! }
34//!
35//! /// Passing a Python uuid.UUID directly to Rust.
36//! #[pyfunction]
37//! fn get_uuid(u: Uuid) -> Uuid {
38//!     u
39//! }
40//!
41//! #[pymodule]
42//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
43//!     m.add_function(wrap_pyfunction!(get_uuid_from_str, m)?)?;
44//!     m.add_function(wrap_pyfunction!(get_uuid, m)?)?;
45//!     Ok(())
46//! }
47//! ```
48//!
49//! Python code that validates the functionality
50//!
51//!
52//! ```python
53//! from my_module import get_uuid_from_str, get_uuid
54//! import uuid
55//!
56//! py_uuid = uuid.uuid4()
57//!
58//! # Convert string to Rust Uuid
59//! rust_uuid = get_uuid_from_str(str(py_uuid))
60//! assert py_uuid == rust_uuid
61//!
62//! # Pass Python UUID directly to Rust
63//! returned_uuid = get_uuid(py_uuid)
64//! assert py_uuid == returned_uuid
65//! ```
66use uuid::{NonNilUuid, Uuid};
67
68use crate::conversion::IntoPyObject;
69use crate::exceptions::{PyTypeError, PyValueError};
70#[cfg(feature = "experimental-inspect")]
71use crate::inspect::PyStaticExpr;
72use crate::instance::Bound;
73use crate::sync::PyOnceLock;
74#[cfg(feature = "experimental-inspect")]
75use crate::type_hint_identifier;
76use crate::types::any::PyAnyMethods;
77use crate::types::PyType;
78use crate::{intern, Borrowed, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
79
80fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
81    static UUID_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
82    UUID_CLS.import(py, "uuid", "UUID")
83}
84
85impl FromPyObject<'_, '_> for Uuid {
86    type Error = PyErr;
87
88    #[cfg(feature = "experimental-inspect")]
89    const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("uuid", "UUID");
90
91    fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
92        let py = obj.py();
93        let uuid_cls = get_uuid_cls(py)?;
94
95        if obj.is_instance(uuid_cls)? {
96            let uuid_int: u128 = obj.getattr(intern!(py, "int"))?.extract()?;
97            Ok(Uuid::from_u128(uuid_int))
98        } else {
99            Err(PyTypeError::new_err("Expected a `uuid.UUID` instance."))
100        }
101    }
102}
103
104impl<'py> IntoPyObject<'py> for Uuid {
105    type Target = PyAny;
106    type Output = Bound<'py, Self::Target>;
107    type Error = PyErr;
108
109    #[cfg(feature = "experimental-inspect")]
110    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("uuid", "UUID");
111
112    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
113        let uuid_cls = get_uuid_cls(py)?;
114
115        uuid_cls.call1((py.None(), py.None(), py.None(), py.None(), self.as_u128()))
116    }
117}
118
119impl<'py> IntoPyObject<'py> for &Uuid {
120    type Target = PyAny;
121    type Output = Bound<'py, Self::Target>;
122    type Error = PyErr;
123
124    #[cfg(feature = "experimental-inspect")]
125    const OUTPUT_TYPE: PyStaticExpr = Uuid::OUTPUT_TYPE;
126
127    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
128        (*self).into_pyobject(py)
129    }
130}
131
132impl FromPyObject<'_, '_> for NonNilUuid {
133    type Error = PyErr;
134
135    #[cfg(feature = "experimental-inspect")]
136    const INPUT_TYPE: PyStaticExpr = Uuid::INPUT_TYPE;
137
138    fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
139        let uuid: Uuid = obj.extract()?;
140        NonNilUuid::new(uuid).ok_or_else(|| PyValueError::new_err("UUID is nil"))
141    }
142}
143
144impl<'py> IntoPyObject<'py> for NonNilUuid {
145    type Target = PyAny;
146    type Output = Bound<'py, Self::Target>;
147    type Error = PyErr;
148
149    #[cfg(feature = "experimental-inspect")]
150    const OUTPUT_TYPE: PyStaticExpr = Uuid::OUTPUT_TYPE;
151
152    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
153        Uuid::from(self).into_pyobject(py)
154    }
155}
156
157impl<'py> IntoPyObject<'py> for &NonNilUuid {
158    type Target = PyAny;
159    type Output = Bound<'py, Self::Target>;
160    type Error = PyErr;
161
162    #[cfg(feature = "experimental-inspect")]
163    const OUTPUT_TYPE: PyStaticExpr = NonNilUuid::OUTPUT_TYPE;
164
165    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
166        (*self).into_pyobject(py)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::types::dict::PyDictMethods;
174    use crate::types::PyDict;
175    use std::ffi::CString;
176    use uuid::Uuid;
177
178    macro_rules! convert_constants {
179        ($name:ident, $rs:expr, $py:literal) => {
180            #[test]
181            fn $name() -> PyResult<()> {
182                Python::attach(|py| {
183                    let rs_orig = $rs;
184                    let rs_uuid = rs_orig.into_pyobject(py).unwrap();
185                    let locals = PyDict::new(py);
186                    locals.set_item("rs_uuid", &rs_uuid).unwrap();
187
188                    py.run(
189                        &CString::new(format!(
190                            "import uuid\npy_uuid = uuid.UUID('{}')\nassert py_uuid == rs_uuid",
191                            $py
192                        ))
193                        .unwrap(),
194                        None,
195                        Some(&locals),
196                    )
197                    .unwrap();
198
199                    let py_uuid = locals.get_item("py_uuid").unwrap().unwrap();
200                    let py_result: Uuid = py_uuid.extract().unwrap();
201                    assert_eq!(rs_orig, py_result);
202
203                    Ok(())
204                })
205            }
206        };
207    }
208
209    convert_constants!(
210        convert_nil,
211        Uuid::nil(),
212        "00000000-0000-0000-0000-000000000000"
213    );
214    convert_constants!(
215        convert_max,
216        Uuid::max(),
217        "ffffffff-ffff-ffff-ffff-ffffffffffff"
218    );
219
220    convert_constants!(
221        convert_uuid_v4,
222        Uuid::parse_str("a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00").unwrap(),
223        "a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00"
224    );
225
226    convert_constants!(
227        convert_uuid_v3,
228        Uuid::parse_str("6fa459ea-ee8a-3ca4-894e-db77e160355e").unwrap(),
229        "6fa459ea-ee8a-3ca4-894e-db77e160355e"
230    );
231
232    convert_constants!(
233        convert_uuid_v1,
234        Uuid::parse_str("a6cc5730-2261-11ee-9c43-2eb5a363657c").unwrap(),
235        "a6cc5730-2261-11ee-9c43-2eb5a363657c"
236    );
237
238    #[test]
239    fn test_non_nil_uuid() {
240        Python::attach(|py| {
241            let rs_uuid = NonNilUuid::new(Uuid::max()).unwrap();
242            let py_uuid = rs_uuid.into_pyobject(py).unwrap();
243
244            let extract_uuid: NonNilUuid = py_uuid.extract().unwrap();
245            assert_eq!(extract_uuid, rs_uuid);
246
247            let nil_uuid = Uuid::nil().into_pyobject(py).unwrap();
248            let extract_nil: PyResult<NonNilUuid> = nil_uuid.extract();
249            assert!(extract_nil.is_err());
250        })
251    }
252}