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