From 5bcaaa0423c6757ca1c2738d0a54947dacdb03a1 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 2 Jul 2021 16:00:50 +0200 Subject: [PATCH] Add a std::filesystem::path <-> os.PathLike caster. (#2730) --- docs/advanced/cast/overview.rst | 6 +++ include/pybind11/stl.h | 81 +++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 37 +++++++++++++++ tests/test_stl.cpp | 6 +++ tests/test_stl.py | 19 ++++++++ 5 files changed, 149 insertions(+) diff --git a/docs/advanced/cast/overview.rst b/docs/advanced/cast/overview.rst index b0e32a52f..6341fce6d 100644 --- a/docs/advanced/cast/overview.rst +++ b/docs/advanced/cast/overview.rst @@ -151,6 +151,8 @@ as arguments and return values, refer to the section on binding :ref:`classes`. +------------------------------------+---------------------------+-------------------------------+ | ``std::variant<...>`` | Type-safe union (C++17) | :file:`pybind11/stl.h` | +------------------------------------+---------------------------+-------------------------------+ +| ``std::filesystem::path`` | STL path (C++17) [#]_ | :file:`pybind11/stl.h` | ++------------------------------------+---------------------------+-------------------------------+ | ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` | +------------------------------------+---------------------------+-------------------------------+ | ``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`. +------------------------------------+---------------------------+-------------------------------+ | ``Eigen::SparseMatrix<...>`` | Eigen: sparse matrix | :file:`pybind11/eigen.h` | +------------------------------------+---------------------------+-------------------------------+ + +.. [#] ``std::filesystem::path`` is converted to ``pathlib.Path`` and + ``os.PathLike`` is converted to ``std::filesystem::path``, but this requires + Python 3.6 (for ``__fspath__`` support). diff --git a/include/pybind11/stl.h b/include/pybind11/stl.h index ca20b7483..2350a5247 100644 --- a/include/pybind11/stl.h +++ b/include/pybind11/stl.h @@ -41,11 +41,21 @@ # include # define PYBIND11_HAS_VARIANT 1 # endif +// std::filesystem::path +# if defined(PYBIND11_CPP17) && __has_include() && \ + PY_VERSION_HEX >= 0x03060000 +# include +# define PYBIND11_HAS_FILESYSTEM 1 +# endif #elif defined(_MSC_VER) && defined(PYBIND11_CPP17) # include # include # define PYBIND11_HAS_OPTIONAL 1 # define PYBIND11_HAS_VARIANT 1 +# if PY_VERSION_HEX >= 0x03060000 +# include +# define PYBIND11_HAS_FILESYSTEM 1 +# endif #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) @@ -377,6 +387,77 @@ template struct type_caster> : variant_caster> { }; #endif +#if defined(PYBIND11_HAS_FILESYSTEM) +template struct path_caster { + +private: + static PyObject* unicode_from_fs_native(const std::string& w) { +#if !defined(PYPY_VERSION) + return PyUnicode_DecodeFSDefaultAndSize(w.c_str(), ssize_t(w.size())); +#else + // PyPy mistakenly declares the first parameter as non-const. + return PyUnicode_DecodeFSDefaultAndSize( + const_cast(w.c_str()), ssize_t(w.size())); +#endif + } + + static PyObject* unicode_from_fs_native(const std::wstring& w) { + return PyUnicode_FromWideChar(w.c_str(), ssize_t(w.size())); + } + +public: + static handle cast(const T& path, return_value_policy, handle) { + if (auto py_str = unicode_from_fs_native(path.native())) { + return module::import("pathlib").attr("Path")(reinterpret_steal(py_str)) + .release(); + } + return nullptr; + } + + bool load(handle handle, bool) { + // PyUnicode_FSConverter and PyUnicode_FSDecoder normally take care of + // calling PyOS_FSPath themselves, but that's broken on PyPy (PyPy + // issue #3168) so we do it ourselves instead. + PyObject* buf = PyOS_FSPath(handle.ptr()); + if (!buf) { + PyErr_Clear(); + return false; + } + PyObject* native = nullptr; + if constexpr (std::is_same_v) { + if (PyUnicode_FSConverter(buf, &native)) { + if (auto c_str = PyBytes_AsString(native)) { + // AsString returns a pointer to the internal buffer, which + // must not be free'd. + value = c_str; + } + } + } else if constexpr (std::is_same_v) { + if (PyUnicode_FSDecoder(buf, &native)) { + if (auto c_str = PyUnicode_AsWideCharString(native, nullptr)) { + // AsWideCharString returns a new string that must be free'd. + value = c_str; // Copies the string. + PyMem_Free(c_str); + } + } + } + Py_XDECREF(native); + Py_DECREF(buf); + if (PyErr_Occurred()) { + PyErr_Clear(); + return false; + } else { + return true; + } + } + + PYBIND11_TYPE_CASTER(T, _("os.PathLike")); +}; + +template<> struct type_caster + : public path_caster {}; +#endif + PYBIND11_NAMESPACE_END(detail) inline std::ostream &operator<<(std::ostream &os, const handle &obj) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3d8940491..3729b5c7d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -247,6 +247,41 @@ if(Boost_FOUND) endif() endif() +# Check if we need to add -lstdc++fs or -lc++fs or nothing +if(MSVC) + set(STD_FS_NO_LIB_NEEDED TRUE) +else() + file( + WRITE ${CMAKE_CURRENT_BINARY_DIR}/main.cpp + "#include \nint main(int argc, char ** argv) {\n std::filesystem::path p(argv[0]);\n return p.string().length();\n}" + ) + try_compile( + STD_FS_NO_LIB_NEEDED ${CMAKE_CURRENT_BINARY_DIR} + SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp + COMPILE_DEFINITIONS -std=c++17) + try_compile( + STD_FS_NEEDS_STDCXXFS ${CMAKE_CURRENT_BINARY_DIR} + SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp + COMPILE_DEFINITIONS -std=c++17 + LINK_LIBRARIES stdc++fs) + try_compile( + STD_FS_NEEDS_CXXFS ${CMAKE_CURRENT_BINARY_DIR} + SOURCES ${CMAKE_CURRENT_BINARY_DIR}/main.cpp + COMPILE_DEFINITIONS -std=c++17 + LINK_LIBRARIES c++fs) +endif() + +if(${STD_FS_NEEDS_STDCXXFS}) + set(STD_FS_LIB stdc++fs) +elseif(${STD_FS_NEEDS_CXXFS}) + set(STD_FS_LIB c++fs) +elseif(${STD_FS_NO_LIB_NEEDED}) + set(STD_FS_LIB "") +else() + message(WARNING "Unknown compiler - not passing -lstdc++fs") + set(STD_FS_LIB "") +endif() + # Compile with compiler warnings turned on function(pybind11_enable_warnings target_name) if(MSVC) @@ -357,6 +392,8 @@ foreach(target ${test_targets}) target_compile_definitions(${target} PRIVATE -DPYBIND11_TEST_BOOST) endif() + target_link_libraries(${target} PRIVATE ${STD_FS_LIB}) + # Always write the output file directly into the 'tests' directory (even on MSVC) if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY diff --git a/tests/test_stl.cpp b/tests/test_stl.cpp index 07899b84e..7183c56b7 100644 --- a/tests/test_stl.cpp +++ b/tests/test_stl.cpp @@ -238,6 +238,12 @@ TEST_SUBMODULE(stl, m) { .def("member_initialized", &opt_exp_holder::member_initialized); #endif +#ifdef PYBIND11_HAS_FILESYSTEM + // test_fs_path + m.attr("has_filesystem") = true; + m.def("parent_path", [](const std::filesystem::path& p) { return p.parent_path(); }); +#endif + #ifdef PYBIND11_HAS_VARIANT static_assert(std::is_same::value, "visitor::result_type is required by boost::variant in C++11 mode"); diff --git a/tests/test_stl.py b/tests/test_stl.py index 330017544..96939257c 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -162,6 +162,25 @@ def test_exp_optional(): assert holder.member_initialized() +@pytest.mark.skipif(not hasattr(m, "has_filesystem"), reason="no ") +def test_fs_path(): + from pathlib import Path + + class PseudoStrPath: + def __fspath__(self): + return "foo/bar" + + class PseudoBytesPath: + def __fspath__(self): + return b"foo/bar" + + assert m.parent_path(Path("foo/bar")) == Path("foo") + assert m.parent_path("foo/bar") == Path("foo") + assert m.parent_path(b"foo/bar") == Path("foo") + assert m.parent_path(PseudoStrPath()) == Path("foo") + assert m.parent_path(PseudoBytesPath()) == Path("foo") + + @pytest.mark.skipif(not hasattr(m, "load_variant"), reason="no ") def test_variant(doc): assert m.load_variant(1) == "int"