Skip to main content

pyo3/conversions/std/
path.rs

1use crate::conversion::IntoPyObject;
2use crate::ffi_ptr_ext::FfiPtrExt;
3#[cfg(feature = "experimental-inspect")]
4use crate::inspect::{type_hint_identifier, type_hint_subscript, type_hint_union, PyStaticExpr};
5use crate::sync::PyOnceLock;
6use crate::types::any::PyAnyMethods;
7use crate::{ffi, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python};
8use std::borrow::Cow;
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11
12impl FromPyObject<'_, '_> for PathBuf {
13    type Error = PyErr;
14
15    #[cfg(feature = "experimental-inspect")]
16    const INPUT_TYPE: PyStaticExpr = type_hint_union!(
17        OsString::INPUT_TYPE,
18        type_hint_subscript!(
19            type_hint_identifier!("os", "PathLike"),
20            OsString::INPUT_TYPE
21        )
22    );
23
24    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
25        // We use os.fspath to get the underlying path as bytes or str
26        let path = unsafe { ffi::PyOS_FSPath(ob.as_ptr()).assume_owned_or_err(ob.py())? };
27        Ok(path.extract::<OsString>()?.into())
28    }
29}
30
31impl<'py> IntoPyObject<'py> for &Path {
32    type Target = PyAny;
33    type Output = Bound<'py, Self::Target>;
34    type Error = PyErr;
35
36    #[cfg(feature = "experimental-inspect")]
37    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("pathlib", "Path");
38
39    #[inline]
40    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
41        static PY_PATH: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
42        PY_PATH
43            .import(py, "pathlib", "Path")?
44            .call((self.as_os_str(),), None)
45    }
46}
47
48impl<'py> IntoPyObject<'py> for &&Path {
49    type Target = PyAny;
50    type Output = Bound<'py, Self::Target>;
51    type Error = PyErr;
52
53    #[cfg(feature = "experimental-inspect")]
54    const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
55
56    #[inline]
57    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
58        (*self).into_pyobject(py)
59    }
60}
61
62impl<'py> IntoPyObject<'py> for Cow<'_, Path> {
63    type Target = PyAny;
64    type Output = Bound<'py, Self::Target>;
65    type Error = PyErr;
66
67    #[cfg(feature = "experimental-inspect")]
68    const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
69
70    #[inline]
71    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
72        (*self).into_pyobject(py)
73    }
74}
75
76impl<'py> IntoPyObject<'py> for &Cow<'_, Path> {
77    type Target = PyAny;
78    type Output = Bound<'py, Self::Target>;
79    type Error = PyErr;
80
81    #[cfg(feature = "experimental-inspect")]
82    const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
83
84    #[inline]
85    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
86        (&**self).into_pyobject(py)
87    }
88}
89
90impl<'a> FromPyObject<'a, '_> for Cow<'a, Path> {
91    type Error = PyErr;
92
93    #[cfg(feature = "experimental-inspect")]
94    const INPUT_TYPE: PyStaticExpr = PathBuf::INPUT_TYPE;
95
96    fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
97        #[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
98        if let Ok(s) = obj.extract::<&str>() {
99            return Ok(Cow::Borrowed(s.as_ref()));
100        }
101
102        obj.extract::<PathBuf>().map(Cow::Owned)
103    }
104}
105
106impl<'py> IntoPyObject<'py> for PathBuf {
107    type Target = PyAny;
108    type Output = Bound<'py, Self::Target>;
109    type Error = PyErr;
110
111    #[cfg(feature = "experimental-inspect")]
112    const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
113
114    #[inline]
115    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
116        (&self).into_pyobject(py)
117    }
118}
119
120impl<'py> IntoPyObject<'py> for &PathBuf {
121    type Target = PyAny;
122    type Output = Bound<'py, Self::Target>;
123    type Error = PyErr;
124
125    #[cfg(feature = "experimental-inspect")]
126    const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
127
128    #[inline]
129    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
130        (&**self).into_pyobject(py)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::{
138        types::{PyAnyMethods, PyString},
139        IntoPyObjectExt,
140    };
141    #[cfg(not(target_os = "wasi"))]
142    use std::ffi::OsStr;
143    use std::fmt::Debug;
144    #[cfg(any(unix, target_os = "emscripten"))]
145    use std::os::unix::ffi::OsStringExt;
146    #[cfg(windows)]
147    use std::os::windows::ffi::OsStringExt;
148
149    #[test]
150    #[cfg(any(unix, target_os = "emscripten"))]
151    fn test_non_utf8_conversion() {
152        Python::attach(|py| {
153            use std::os::unix::ffi::OsStrExt;
154
155            // this is not valid UTF-8
156            let payload = &[250, 251, 252, 253, 254, 255, 0, 255];
157            let path = Path::new(OsStr::from_bytes(payload));
158
159            // do a roundtrip into Pythonland and back and compare
160            let py_str = path.into_pyobject(py).unwrap();
161            let path_2: PathBuf = py_str.extract().unwrap();
162            assert_eq!(path, path_2);
163        });
164    }
165
166    #[test]
167    fn test_intopyobject_roundtrip() {
168        Python::attach(|py| {
169            fn test_roundtrip<'py, T>(py: Python<'py>, obj: T)
170            where
171                T: IntoPyObject<'py> + AsRef<Path> + Debug + Clone,
172                T::Error: Debug,
173            {
174                let pyobject = obj.clone().into_bound_py_any(py).unwrap();
175                let roundtripped_obj: PathBuf = pyobject.extract().unwrap();
176                assert_eq!(obj.as_ref(), roundtripped_obj.as_path());
177            }
178            let path = Path::new("Hello\0\nšŸ");
179            test_roundtrip::<&Path>(py, path);
180            test_roundtrip::<Cow<'_, Path>>(py, Cow::Borrowed(path));
181            test_roundtrip::<Cow<'_, Path>>(py, Cow::Owned(path.to_path_buf()));
182            test_roundtrip::<PathBuf>(py, path.to_path_buf());
183        });
184    }
185
186    #[test]
187    fn test_from_pystring() {
188        Python::attach(|py| {
189            let path = "Hello\0\nšŸ";
190            let pystring = PyString::new(py, path);
191            let roundtrip: PathBuf = pystring.extract().unwrap();
192            assert_eq!(roundtrip, Path::new(path));
193        });
194    }
195
196    #[test]
197    fn test_extract_cow() {
198        Python::attach(|py| {
199            fn test_extract<'py, T>(py: Python<'py>, path: &T, is_borrowed: bool)
200            where
201                for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>,
202                for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug,
203                T: AsRef<Path> + ?Sized,
204            {
205                let pystring = path.into_pyobject(py).unwrap();
206                let cow: Cow<'_, Path> = pystring.extract().unwrap();
207                assert_eq!(cow, path.as_ref());
208                assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_)));
209            }
210
211            // On Python 3.10+ or when not using the limited API, we can borrow strings from python
212            let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
213            // This can be borrowed because it is valid UTF-8
214            test_extract::<str>(py, "Hello\0\nšŸ", can_borrow_str);
215            test_extract::<str>(py, "Hello, world!", can_borrow_str);
216
217            #[cfg(windows)]
218            let os_str = {
219                // 'A', unpaired surrogate, 'B'
220                OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16])
221            };
222
223            #[cfg(any(unix, target_os = "emscripten"))]
224            let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) };
225
226            // This cannot be borrowed because it is not valid UTF-8
227            #[cfg(any(unix, windows, target_os = "emscripten"))]
228            test_extract::<OsStr>(py, &os_str, false);
229        });
230    }
231}