Skip to content

Commit 5c363b5

Browse files
authored
Add python uuid conversions (#4864)
* feat: add uuid support * fix: clippy error * fix: apply review feedback and correct lint issues * fix: cargo fmt * refactor: apply code review feedback * refactor: apply code review feedback
1 parent e7041ba commit 5c363b5

File tree

7 files changed

+195
-0
lines changed

7 files changed

+195
-0
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ num-rational = {version = "0.4.1", optional = true }
4646
rust_decimal = { version = "1.15", default-features = false, optional = true }
4747
serde = { version = "1.0", optional = true }
4848
smallvec = { version = "1.0", optional = true }
49+
uuid = { version = "1.11.0", optional = true }
4950

5051
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
5152
portable-atomic = "1.0"
@@ -134,6 +135,7 @@ full = [
134135
"rust_decimal",
135136
"serde",
136137
"smallvec",
138+
"uuid",
137139
]
138140

139141
[workspace]

guide/src/features.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,7 @@ struct User {
211211
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.
212212

213213
[set-configuration-options]: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
214+
215+
### `uuid`
216+
217+
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.

newsfragments/4864.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add uuid to/from python conversions.

src/conversions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ pub mod rust_decimal;
1515
pub mod serde;
1616
pub mod smallvec;
1717
mod std;
18+
pub mod uuid;

src/conversions/uuid.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#![cfg(feature = "uuid")]
2+
3+
//! Conversions to and from [uuid](https://docs.rs/uuid/latest/uuid/)'s [`Uuid`] type.
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
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+
//! ```
66+
use uuid::Uuid;
67+
68+
use crate::conversion::IntoPyObject;
69+
use crate::exceptions::PyTypeError;
70+
use crate::instance::Bound;
71+
use crate::sync::GILOnceCell;
72+
use crate::types::any::PyAnyMethods;
73+
use crate::types::PyType;
74+
use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
75+
76+
fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
77+
static UUID_CLS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
78+
UUID_CLS.import(py, "uuid", "UUID")
79+
}
80+
81+
impl FromPyObject<'_> for Uuid {
82+
fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
83+
let py = obj.py();
84+
let uuid_cls = get_uuid_cls(py)?;
85+
86+
if obj.is_instance(uuid_cls)? {
87+
let uuid_int: u128 = obj.getattr(intern!(py, "int"))?.extract()?;
88+
Ok(Uuid::from_u128(uuid_int.to_le()))
89+
} else {
90+
Err(PyTypeError::new_err("Expected a `uuid.UUID` instance."))
91+
}
92+
}
93+
}
94+
95+
impl<'py> IntoPyObject<'py> for Uuid {
96+
type Target = PyAny;
97+
type Output = Bound<'py, Self::Target>;
98+
type Error = PyErr;
99+
100+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
101+
let uuid_cls = get_uuid_cls(py)?;
102+
103+
uuid_cls.call1((py.None(), py.None(), py.None(), py.None(), self.as_u128()))
104+
}
105+
}
106+
107+
impl<'py> IntoPyObject<'py> for &Uuid {
108+
type Target = PyAny;
109+
type Output = Bound<'py, Self::Target>;
110+
type Error = PyErr;
111+
112+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
113+
(*self).into_pyobject(py)
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
use crate::types::dict::PyDictMethods;
121+
use crate::types::PyDict;
122+
use std::ffi::CString;
123+
use uuid::Uuid;
124+
125+
macro_rules! convert_constants {
126+
($name:ident, $rs:expr, $py:literal) => {
127+
#[test]
128+
fn $name() -> PyResult<()> {
129+
Python::with_gil(|py| {
130+
let rs_orig = $rs;
131+
let rs_uuid = rs_orig.into_pyobject(py).unwrap();
132+
let locals = PyDict::new(py);
133+
locals.set_item("rs_uuid", &rs_uuid).unwrap();
134+
135+
py.run(
136+
&CString::new(format!(
137+
"import uuid\npy_uuid = uuid.UUID('{}')\nassert py_uuid == rs_uuid",
138+
$py
139+
))
140+
.unwrap(),
141+
None,
142+
Some(&locals),
143+
)
144+
.unwrap();
145+
146+
let py_uuid = locals.get_item("py_uuid").unwrap().unwrap();
147+
let py_result: Uuid = py_uuid.extract().unwrap();
148+
assert_eq!(rs_orig, py_result);
149+
150+
Ok(())
151+
})
152+
}
153+
};
154+
}
155+
156+
convert_constants!(
157+
convert_nil,
158+
Uuid::nil(),
159+
"00000000-0000-0000-0000-000000000000"
160+
);
161+
convert_constants!(
162+
convert_max,
163+
Uuid::max(),
164+
"ffffffff-ffff-ffff-ffff-ffffffffffff"
165+
);
166+
167+
convert_constants!(
168+
convert_uuid_v4,
169+
Uuid::parse_str("a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00").unwrap(),
170+
"a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00"
171+
);
172+
173+
convert_constants!(
174+
convert_uuid_v3,
175+
Uuid::parse_str("6fa459ea-ee8a-3ca4-894e-db77e160355e").unwrap(),
176+
"6fa459ea-ee8a-3ca4-894e-db77e160355e"
177+
);
178+
179+
convert_constants!(
180+
convert_uuid_v1,
181+
Uuid::parse_str("a6cc5730-2261-11ee-9c43-2eb5a363657c").unwrap(),
182+
"a6cc5730-2261-11ee-9c43-2eb5a363657c"
183+
);
184+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@
286286
//! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html
287287
//! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html
288288
//! [`SmallVec`]: https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html
289+
//! [`Uuid`]: https://docs.rs/uuid/latest/uuid/struct.Uuid.html
289290
//! [`IndexMap`]: https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html
290291
//! [`BigInt`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html
291292
//! [`BigUint`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html
@@ -319,6 +320,7 @@
319320
//! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock
320321
//! [hashbrown]: https://docs.rs/hashbrown
321322
//! [smallvec]: https://docs.rs/smallvec
323+
//! [uuid]: https://docs.rs/uuid
322324
//! [indexmap]: https://docs.rs/indexmap
323325
#![doc = concat!("[manual_builds]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution.html#manual-builds \"Manual builds - Building and Distribution - PyO3 user guide\"")]
324326
//! [num-bigint]: https://docs.rs/num-bigint

tests/test_compile_error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn test_compile_errors() {
2929
t.compile_fail("tests/ui/invalid_frompy_derive.rs");
3030
t.compile_fail("tests/ui/static_ref.rs");
3131
t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs");
32+
#[cfg(not(any(feature = "uuid")))]
3233
t.compile_fail("tests/ui/invalid_pyfunctions.rs");
3334
#[cfg(not(any(feature = "hashbrown", feature = "indexmap")))]
3435
t.compile_fail("tests/ui/invalid_pymethods.rs");

0 commit comments

Comments
 (0)