Skip to content

Commit d9e2c50

Browse files
Add FromPyObject impl for Cow<Path> & Cow<OsStr> (#5497)
* Add `FromPyObject` impl for `Cow<Path>` & `Cow<OsStr>` * Remove comments about `&OsStr` & `&Path` * Test if result is borrowed * Fix test on wasm
1 parent 4906a61 commit d9e2c50

File tree

3 files changed

+114
-12
lines changed

3 files changed

+114
-12
lines changed

newsfragments/5497.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `FromPyObject` impl for `Cow<Path>` & `Cow<OsStr>`

src/conversions/std/osstr.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ impl<'py> IntoPyObject<'py> for &&OsStr {
6767
}
6868
}
6969

70-
// There's no FromPyObject implementation for &OsStr because albeit possible on Unix, this would
71-
// be impossible to implement on Windows. Hence it's omitted entirely
72-
7370
impl FromPyObject<'_, '_> for OsString {
7471
type Error = PyErr;
7572

@@ -153,6 +150,19 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> {
153150
}
154151
}
155152

153+
impl<'a> FromPyObject<'a, '_> for Cow<'a, OsStr> {
154+
type Error = PyErr;
155+
156+
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
157+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
158+
if let Ok(s) = obj.extract::<&str>() {
159+
return Ok(Cow::Borrowed(s.as_ref()));
160+
}
161+
162+
obj.extract::<OsString>().map(Cow::Owned)
163+
}
164+
}
165+
156166
impl<'py> IntoPyObject<'py> for OsString {
157167
type Target = PyString;
158168
type Output = Bound<'py, Self::Target>;
@@ -178,8 +188,12 @@ impl<'py> IntoPyObject<'py> for &OsString {
178188
#[cfg(test)]
179189
mod tests {
180190
use crate::types::{PyAnyMethods, PyString, PyStringMethods};
181-
use crate::{BoundObject, IntoPyObject, Python};
191+
use crate::{Bound, BoundObject, IntoPyObject, Python};
182192
use std::fmt::Debug;
193+
#[cfg(unix)]
194+
use std::os::unix::ffi::OsStringExt;
195+
#[cfg(windows)]
196+
use std::os::windows::ffi::OsStringExt;
183197
use std::{
184198
borrow::Cow,
185199
ffi::{OsStr, OsString},
@@ -254,4 +268,40 @@ mod tests {
254268
assert_eq!(encoded, wide);
255269
});
256270
}
271+
272+
#[test]
273+
fn test_extract_cow() {
274+
Python::attach(|py| {
275+
fn test_extract<'py, T>(py: Python<'py>, input: &T, is_borrowed: bool)
276+
where
277+
for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>,
278+
for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug,
279+
T: AsRef<OsStr> + ?Sized,
280+
{
281+
let pystring = input.into_pyobject(py).unwrap();
282+
let cow: Cow<'_, OsStr> = pystring.extract().unwrap();
283+
assert_eq!(cow, input.as_ref());
284+
assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_)));
285+
}
286+
287+
// On Python 3.10+ or when not using the limited API, we can borrow strings from python
288+
let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
289+
// This can be borrowed because it is valid UTF-8
290+
test_extract::<str>(py, "Hello\0\n🐍", can_borrow_str);
291+
test_extract::<str>(py, "Hello, world!", can_borrow_str);
292+
293+
#[cfg(windows)]
294+
let os_str = {
295+
// 'A', unpaired surrogate, 'B'
296+
OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16])
297+
};
298+
299+
#[cfg(unix)]
300+
let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) };
301+
302+
// This cannot be borrowed because it is not valid UTF-8
303+
#[cfg(any(windows, unix))]
304+
test_extract::<OsStr>(py, &os_str, false);
305+
});
306+
}
257307
}

src/conversions/std/path.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ use std::borrow::Cow;
77
use std::ffi::OsString;
88
use std::path::{Path, PathBuf};
99

10-
// See osstr.rs for why there's no FromPyObject impl for &Path
11-
1210
impl FromPyObject<'_, '_> for PathBuf {
1311
type Error = PyErr;
1412

@@ -66,6 +64,19 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, Path> {
6664
}
6765
}
6866

67+
impl<'a> FromPyObject<'a, '_> for Cow<'a, Path> {
68+
type Error = PyErr;
69+
70+
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
71+
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
72+
if let Ok(s) = obj.extract::<&str>() {
73+
return Ok(Cow::Borrowed(s.as_ref()));
74+
}
75+
76+
obj.extract::<PathBuf>().map(Cow::Owned)
77+
}
78+
}
79+
6980
impl<'py> IntoPyObject<'py> for PathBuf {
7081
type Target = PyAny;
7182
type Output = Bound<'py, Self::Target>;
@@ -90,18 +101,22 @@ impl<'py> IntoPyObject<'py> for &PathBuf {
90101

91102
#[cfg(test)]
92103
mod tests {
93-
use crate::types::{PyAnyMethods, PyString};
94-
use crate::{IntoPyObject, IntoPyObjectExt, Python};
95-
use std::borrow::Cow;
104+
use super::*;
105+
use crate::{
106+
types::{PyAnyMethods, PyString},
107+
IntoPyObjectExt,
108+
};
109+
use std::ffi::OsStr;
96110
use std::fmt::Debug;
97-
use std::path::{Path, PathBuf};
111+
#[cfg(unix)]
112+
use std::os::unix::ffi::OsStringExt;
113+
#[cfg(windows)]
114+
use std::os::windows::ffi::OsStringExt;
98115

99116
#[test]
100117
#[cfg(not(windows))]
101118
fn test_non_utf8_conversion() {
102119
Python::attach(|py| {
103-
use crate::types::PyAnyMethods;
104-
use std::ffi::OsStr;
105120
#[cfg(not(target_os = "wasi"))]
106121
use std::os::unix::ffi::OsStrExt;
107122
#[cfg(target_os = "wasi")]
@@ -147,4 +162,40 @@ mod tests {
147162
assert_eq!(roundtrip, Path::new(path));
148163
});
149164
}
165+
166+
#[test]
167+
fn test_extract_cow() {
168+
Python::attach(|py| {
169+
fn test_extract<'py, T>(py: Python<'py>, path: &T, is_borrowed: bool)
170+
where
171+
for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>,
172+
for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug,
173+
T: AsRef<Path> + ?Sized,
174+
{
175+
let pystring = path.into_pyobject(py).unwrap();
176+
let cow: Cow<'_, Path> = pystring.extract().unwrap();
177+
assert_eq!(cow, path.as_ref());
178+
assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_)));
179+
}
180+
181+
// On Python 3.10+ or when not using the limited API, we can borrow strings from python
182+
let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
183+
// This can be borrowed because it is valid UTF-8
184+
test_extract::<str>(py, "Hello\0\n🐍", can_borrow_str);
185+
test_extract::<str>(py, "Hello, world!", can_borrow_str);
186+
187+
#[cfg(windows)]
188+
let os_str = {
189+
// 'A', unpaired surrogate, 'B'
190+
OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16])
191+
};
192+
193+
#[cfg(unix)]
194+
let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) };
195+
196+
// This cannot be borrowed because it is not valid UTF-8
197+
#[cfg(any(unix, windows))]
198+
test_extract::<OsStr>(py, &os_str, false);
199+
});
200+
}
150201
}

0 commit comments

Comments
 (0)