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 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 let payload = &[250, 251, 252, 253, 254, 255, 0, 255];
157 let path = Path::new(OsStr::from_bytes(payload));
158
159 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 let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
213 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 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 #[cfg(any(unix, windows, target_os = "emscripten"))]
228 test_extract::<OsStr>(py, &os_str, false);
229 });
230 }
231}