Skip to content

Commit 5bcaaa0

Browse files
authored
Add a std::filesystem::path <-> os.PathLike caster. (#2730)
1 parent f067deb commit 5bcaaa0

File tree

5 files changed

+149
-0
lines changed

5 files changed

+149
-0
lines changed

docs/advanced/cast/overview.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
151151
+------------------------------------+---------------------------+-------------------------------+
152152
| ``std::variant<...>`` | Type-safe union (C++17) | :file:`pybind11/stl.h` |
153153
+------------------------------------+---------------------------+-------------------------------+
154+
| ``std::filesystem::path<T>`` | STL path (C++17) [#]_ | :file:`pybind11/stl.h` |
155+
+------------------------------------+---------------------------+-------------------------------+
154156
| ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` |
155157
+------------------------------------+---------------------------+-------------------------------+
156158
| ``std::chrono::duration<...>`` | STL time duration | :file:`pybind11/chrono.h` |
@@ -163,3 +165,7 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
163165
+------------------------------------+---------------------------+-------------------------------+
164166
| ``Eigen::SparseMatrix<...>`` | Eigen: sparse matrix | :file:`pybind11/eigen.h` |
165167
+------------------------------------+---------------------------+-------------------------------+
168+
169+
.. [#] ``std::filesystem::path`` is converted to ``pathlib.Path`` and
170+
``os.PathLike`` is converted to ``std::filesystem::path``, but this requires
171+
Python 3.6 (for ``__fspath__`` support).

include/pybind11/stl.h

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,21 @@
4141
# include <variant>
4242
# define PYBIND11_HAS_VARIANT 1
4343
# endif
44+
// std::filesystem::path
45+
# if defined(PYBIND11_CPP17) && __has_include(<filesystem>) && \
46+
PY_VERSION_HEX >= 0x03060000
47+
# include <filesystem>
48+
# define PYBIND11_HAS_FILESYSTEM 1
49+
# endif
4450
#elif defined(_MSC_VER) && defined(PYBIND11_CPP17)
4551
# include <optional>
4652
# include <variant>
4753
# define PYBIND11_HAS_OPTIONAL 1
4854
# define PYBIND11_HAS_VARIANT 1
55+
# if PY_VERSION_HEX >= 0x03060000
56+
# include <filesystem>
57+
# define PYBIND11_HAS_FILESYSTEM 1
58+
# endif
4959
#endif
5060

5161
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
@@ -377,6 +387,77 @@ template <typename... Ts>
377387
struct type_caster<std::variant<Ts...>> : variant_caster<std::variant<Ts...>> { };
378388
#endif
379389

390+
#if defined(PYBIND11_HAS_FILESYSTEM)
391+
template<typename T> struct path_caster {
392+
393+
private:
394+
static PyObject* unicode_from_fs_native(const std::string& w) {
395+
#if !defined(PYPY_VERSION)
396+
return PyUnicode_DecodeFSDefaultAndSize(w.c_str(), ssize_t(w.size()));
397+
#else
398+
// PyPy mistakenly declares the first parameter as non-const.
399+
return PyUnicode_DecodeFSDefaultAndSize(
400+
const_cast<char*>(w.c_str()), ssize_t(w.size()));
401+
#endif
402+
}
403+
404+
static PyObject* unicode_from_fs_native(const std::wstring& w) {
405+
return PyUnicode_FromWideChar(w.c_str(), ssize_t(w.size()));
406+
}
407+
408+
public:
409+
static handle cast(const T& path, return_value_policy, handle) {
410+
if (auto py_str = unicode_from_fs_native(path.native())) {
411+
return module::import("pathlib").attr("Path")(reinterpret_steal<object>(py_str))
412+
.release();
413+
}
414+
return nullptr;
415+
}
416+
417+
bool load(handle handle, bool) {
418+
// PyUnicode_FSConverter and PyUnicode_FSDecoder normally take care of
419+
// calling PyOS_FSPath themselves, but that's broken on PyPy (PyPy
420+
// issue #3168) so we do it ourselves instead.
421+
PyObject* buf = PyOS_FSPath(handle.ptr());
422+
if (!buf) {
423+
PyErr_Clear();
424+
return false;
425+
}
426+
PyObject* native = nullptr;
427+
if constexpr (std::is_same_v<typename T::value_type, char>) {
428+
if (PyUnicode_FSConverter(buf, &native)) {
429+
if (auto c_str = PyBytes_AsString(native)) {
430+
// AsString returns a pointer to the internal buffer, which
431+
// must not be free'd.
432+
value = c_str;
433+
}
434+
}
435+
} else if constexpr (std::is_same_v<typename T::value_type, wchar_t>) {
436+
if (PyUnicode_FSDecoder(buf, &native)) {
437+
if (auto c_str = PyUnicode_AsWideCharString(native, nullptr)) {
438+
// AsWideCharString returns a new string that must be free'd.
439+
value = c_str; // Copies the string.
440+
PyMem_Free(c_str);
441+
}
442+
}
443+
}
444+
Py_XDECREF(native);
445+
Py_DECREF(buf);
446+
if (PyErr_Occurred()) {
447+
PyErr_Clear();
448+
return false;
449+
} else {
450+
return true;
451+
}
452+
}
453+
454+
PYBIND11_TYPE_CASTER(T, _("os.PathLike"));
455+
};
456+
457+
template<> struct type_caster<std::filesystem::path>
458+
: public path_caster<std::filesystem::path> {};
459+
#endif
460+
380461
PYBIND11_NAMESPACE_END(detail)
381462

382463
inline std::ostream &operator<<(std::ostream &os, const handle &obj) {

tests/CMakeLists.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,41 @@ if(Boost_FOUND)
247247
endif()
248248
endif()
249249

250+
# Check if we need to add -lstdc++fs or -lc++fs or nothing
251+
if(MSVC)
252+
set(STD_FS_NO_LIB_NEEDED TRUE)
253+
else()
254+
file(
255+
WRITE ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
256+
"#include <filesystem>\nint main(int argc, char ** argv) {\n std::filesystem::path p(argv[0]);\n return p.string().length();\n}"
257+
)
258+
try_compile(
259+
STD_FS_NO_LIB_NEEDED ${CMAKE_CURRENT_BINARY_DIR}
260+
SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
261+
COMPILE_DEFINITIONS -std=c++17)
262+
try_compile(
263+
STD_FS_NEEDS_STDCXXFS ${CMAKE_CURRENT_BINARY_DIR}
264+
SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
265+
COMPILE_DEFINITIONS -std=c++17
266+
LINK_LIBRARIES stdc++fs)
267+
try_compile(
268+
STD_FS_NEEDS_CXXFS ${CMAKE_CURRENT_BINARY_DIR}
269+
SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
270+
COMPILE_DEFINITIONS -std=c++17
271+
LINK_LIBRARIES c++fs)
272+
endif()
273+
274+
if(${STD_FS_NEEDS_STDCXXFS})
275+
set(STD_FS_LIB stdc++fs)
276+
elseif(${STD_FS_NEEDS_CXXFS})
277+
set(STD_FS_LIB c++fs)
278+
elseif(${STD_FS_NO_LIB_NEEDED})
279+
set(STD_FS_LIB "")
280+
else()
281+
message(WARNING "Unknown compiler - not passing -lstdc++fs")
282+
set(STD_FS_LIB "")
283+
endif()
284+
250285
# Compile with compiler warnings turned on
251286
function(pybind11_enable_warnings target_name)
252287
if(MSVC)
@@ -357,6 +392,8 @@ foreach(target ${test_targets})
357392
target_compile_definitions(${target} PRIVATE -DPYBIND11_TEST_BOOST)
358393
endif()
359394

395+
target_link_libraries(${target} PRIVATE ${STD_FS_LIB})
396+
360397
# Always write the output file directly into the 'tests' directory (even on MSVC)
361398
if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
362399
set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY

tests/test_stl.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ TEST_SUBMODULE(stl, m) {
238238
.def("member_initialized", &opt_exp_holder::member_initialized);
239239
#endif
240240

241+
#ifdef PYBIND11_HAS_FILESYSTEM
242+
// test_fs_path
243+
m.attr("has_filesystem") = true;
244+
m.def("parent_path", [](const std::filesystem::path& p) { return p.parent_path(); });
245+
#endif
246+
241247
#ifdef PYBIND11_HAS_VARIANT
242248
static_assert(std::is_same<py::detail::variant_caster_visitor::result_type, py::handle>::value,
243249
"visitor::result_type is required by boost::variant in C++11 mode");

tests/test_stl.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,25 @@ def test_exp_optional():
162162
assert holder.member_initialized()
163163

164164

165+
@pytest.mark.skipif(not hasattr(m, "has_filesystem"), reason="no <filesystem>")
166+
def test_fs_path():
167+
from pathlib import Path
168+
169+
class PseudoStrPath:
170+
def __fspath__(self):
171+
return "foo/bar"
172+
173+
class PseudoBytesPath:
174+
def __fspath__(self):
175+
return b"foo/bar"
176+
177+
assert m.parent_path(Path("foo/bar")) == Path("foo")
178+
assert m.parent_path("foo/bar") == Path("foo")
179+
assert m.parent_path(b"foo/bar") == Path("foo")
180+
assert m.parent_path(PseudoStrPath()) == Path("foo")
181+
assert m.parent_path(PseudoBytesPath()) == Path("foo")
182+
183+
165184
@pytest.mark.skipif(not hasattr(m, "load_variant"), reason="no <variant>")
166185
def test_variant(doc):
167186
assert m.load_variant(1) == "int"

0 commit comments

Comments
 (0)