From 1e5a49557d56108c8e6cb20e0a0a20ed861b3156 Mon Sep 17 00:00:00 2001 From: Tpt Date: Tue, 8 Aug 2023 21:57:46 +0200 Subject: [PATCH] Makes PathBuf FromPyObject implementation work on all os.PathLike PyOS_FSPath is in abi3-py36 --- guide/src/conversions/tables.md | 1 + newsfragments/3374.added.md | 1 + pytests/tests/test_path.py | 24 ++++++++++++++++++++++++ src/conversions/std/path.rs | 24 +++++++----------------- 4 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 newsfragments/3374.added.md diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 01a5c5a3..bd77450a 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -37,6 +37,7 @@ The table below contains the Python type and the corresponding function argument | `decimal.Decimal` | `rust_decimal::Decimal`[^5] | - | | `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - | | `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - | +| `os.PathLike ` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | | `pathlib.Path` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | | `typing.Optional[T]` | `Option` | - | | `typing.Sequence[T]` | `Vec` | `&PySequence` | diff --git a/newsfragments/3374.added.md b/newsfragments/3374.added.md new file mode 100644 index 00000000..d26db978 --- /dev/null +++ b/newsfragments/3374.added.md @@ -0,0 +1 @@ +`PathBuf` `FromPyObject` implementation now works on all `os.PathLike` values. \ No newline at end of file diff --git a/pytests/tests/test_path.py b/pytests/tests/test_path.py index 07c4cb35..21240187 100644 --- a/pytests/tests/test_path.py +++ b/pytests/tests/test_path.py @@ -1,5 +1,7 @@ import pathlib +import pytest + import pyo3_pytests.path as rpath @@ -16,3 +18,25 @@ def test_take_pathbuf(): def test_take_pathlib(): p = pathlib.Path("/root") assert rpath.take_pathbuf(p) == str(p) + + +def test_take_pathlike(): + assert rpath.take_pathbuf(PathLike("/root")) == "/root" + + +def test_take_invalid_pathlike(): + with pytest.raises(TypeError): + assert rpath.take_pathbuf(PathLike(1)) + + +def test_take_invalid(): + with pytest.raises(TypeError): + assert rpath.take_pathbuf(3) + + +class PathLike: + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 8cb679fd..0685e491 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,5 +1,7 @@ -use crate::intern; -use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + ffi, AsPyPointer, FromPyObject, FromPyPointer, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, +}; use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -14,21 +16,9 @@ impl ToPyObject for Path { impl FromPyObject<'_> for PathBuf { fn extract(ob: &PyAny) -> PyResult { - let os_str = match OsString::extract(ob) { - Ok(s) => s, - Err(err) => { - let py = ob.py(); - let pathlib = py.import(intern!(py, "pathlib"))?; - let pathlib_path = pathlib.getattr(intern!(py, "Path"))?; - if ob.is_instance(pathlib_path)? { - let path_str = ob.call_method0(intern!(py, "__str__"))?; - OsString::extract(path_str)? - } else { - return Err(err); - } - } - }; - Ok(PathBuf::from(os_str)) + // We use os.fspath to get the underlying path as bytes or str + let path = unsafe { PyAny::from_owned_ptr_or_err(ob.py(), ffi::PyOS_FSPath(ob.as_ptr())) }?; + Ok(OsString::extract(path)?.into()) } }