From 65ccc4af9c1aa9ca89f67afa1bd492774d03a5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 4 May 2022 20:54:06 +0200 Subject: [PATCH] Add argument wrapper for directory and files. --- src/mobase/mobase.cpp | 9 +- src/mobase/pybind11_all.h | 74 ++++++++- src/mobase/wrappers/basic_classes.cpp | 29 ++-- src/mobase/wrappers/game_features.cpp | 14 +- src/mobase/wrappers/pyplugins.h | 8 +- src/mobase/wrappers/wrappers.cpp | 2 +- .../pybind11_qt/details/pybind11_qt_sip.h | 9 ++ .../include/pybind11_utils/arg_wrapper.h | 142 ++++++++++++++++++ tests/python/test_argument_wrapper.cpp | 69 +++++++++ tests/python/test_argument_wrapper.py | 63 ++++++++ tests/python/test_path_wrappers.py | 45 ++++++ 11 files changed, 437 insertions(+), 27 deletions(-) create mode 100644 src/pybind11-utils/include/pybind11_utils/arg_wrapper.h create mode 100644 tests/python/test_argument_wrapper.cpp create mode 100644 tests/python/test_argument_wrapper.py create mode 100644 tests/python/test_path_wrappers.py diff --git a/src/mobase/mobase.cpp b/src/mobase/mobase.cpp index 8edcb39..ba3e82d 100644 --- a/src/mobase/mobase.cpp +++ b/src/mobase/mobase.cpp @@ -56,9 +56,12 @@ PYBIND11_MODULE(mobase, m) // functions // - m.def("getFileVersion", &MOBase::getFileVersion, py::arg("filepath")); - m.def("getProductVersion", &MOBase::getProductVersion, py::arg("executable")); - m.def("getIconForExecutable", &MOBase::iconForExecutable, py::arg("executable")); + m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion), + py::arg("filepath")); + m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion), + py::arg("executable")); + m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable), + py::arg("executable")); // expose MoVariant - MoVariant is a fake object whose only purpose is to be // used as a type-hint on the python side (e.g., def foo(x: diff --git a/src/mobase/pybind11_all.h b/src/mobase/pybind11_all.h index 8d34288..9f317bd 100644 --- a/src/mobase/pybind11_all.h +++ b/src/mobase/pybind11_all.h @@ -1,21 +1,91 @@ #ifndef PYTHON_PYBIND11_ALL_H #define PYTHON_PYBIND11_ALL_H +#include + +#include + #include #include #include #include -#include "pybind11_utils/functional.h" - #include "pybind11_qt/pybind11_qt.h" +#include "pybind11_utils/arg_wrapper.h" +#include "pybind11_utils/functional.h" #include "pybind11_utils/shared_cpp_owner.h" #include #include +namespace mo2::python { + + // struct to wrap "path" object between C++ and Python, allowing to mix + // pathlib.Path, QString, QFileInfo and QDir when possible + // + class BasePathWrapper { + protected: + // we store a std::filesystem::path because it can be converted to most thing, + // even though we lose basic functionality on QDir (name filter, etc.) + std::filesystem::path path_; + + public: + BasePathWrapper() = default; + BasePathWrapper(BasePathWrapper const&) = default; + BasePathWrapper& operator=(BasePathWrapper const&) = default; + BasePathWrapper(BasePathWrapper&&) = default; + BasePathWrapper& operator=(BasePathWrapper&&) = default; + + BasePathWrapper(std::filesystem::path const& path) : path_{path} {} + BasePathWrapper(QString const& path) : path_{path.toStdWString()} {} + + operator QString() const { return QString::fromStdWString(path_.native()); } + operator std::filesystem::path() const { return path_; } + }; + + class FileWrapper : public BasePathWrapper { + public: + using BasePathWrapper::BasePathWrapper; + + FileWrapper(QFileInfo const& fileInfo) + : BasePathWrapper(fileInfo.filesystemFilePath()) + { + } + + operator QFileInfo() const { return QFileInfo(path_); } + }; + + class DirectoryWrapper : public BasePathWrapper { + public: + using BasePathWrapper::BasePathWrapper; + + DirectoryWrapper(QDir const& dir) : BasePathWrapper(dir.filesystemPath()) {} + + operator QDir() const { return QDir(path_); } + }; + + template + auto wrap_for_filepath(Fn&& fn) + { + return mo2::python::wrap_arguments(std::forward(fn)); + } + + template + auto wrap_for_directory(Fn&& fn) + { + return mo2::python::wrap_arguments( + std::forward(fn)); + } + +} // namespace mo2::python + MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement) MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame) +MO2_PYBIND11_WRAP_ARGUMENT_CASTER(mo2::python::FileWrapper, QFileInfo, + std::filesystem::path, QString); +MO2_PYBIND11_WRAP_ARGUMENT_CASTER(mo2::python::DirectoryWrapper, QDir, + std::filesystem::path, QString); + #endif diff --git a/src/mobase/wrappers/basic_classes.cpp b/src/mobase/wrappers/basic_classes.cpp index 1668685..4d5cc3d 100644 --- a/src/mobase/wrappers/basic_classes.cpp +++ b/src/mobase/wrappers/basic_classes.cpp @@ -30,6 +30,7 @@ namespace mo2::python { namespace py = pybind11; using namespace pybind11::literals; + using namespace mo2::python; void add_versioninfo_classes(py::module_ m) { @@ -90,9 +91,10 @@ namespace mo2::python { void add_executable_classes(py::module_ m) { py::class_(m, "ExecutableInfo") - .def(py::init(), "title"_a, "binary"_a) + .def(py::init(), "title"_a, "binary"_a) .def("withArgument", &ExecutableInfo::withArgument, "argument"_a) - .def("withWorkingDirectory", &ExecutableInfo::withWorkingDirectory, + .def("withWorkingDirectory", + wrap_for_directory(&ExecutableInfo::withWorkingDirectory), "directory"_a) .def("withSteamAppId", &ExecutableInfo::withSteamAppId, "app_id"_a) .def("asCustom", &ExecutableInfo::asCustom) @@ -448,16 +450,17 @@ namespace mo2::python { .def("setPersistent", &IOrganizer::setPersistent, "plugin_name"_a, "key"_a, "value"_a, "sync"_a = true) .def("pluginDataPath", &IOrganizer::pluginDataPath) - .def("installMod", &IOrganizer::installMod, + .def("installMod", wrap_for_filepath<1>(&IOrganizer::installMod), py::return_value_policy::reference, "filename"_a, "name_suggestion"_a = "") - .def("resolvePath", &IOrganizer::resolvePath, "filename"_a) + .def("resolvePath", wrap_for_filepath(&IOrganizer::resolvePath), + "filename"_a) .def("listDirectories", &IOrganizer::listDirectories, "directory"_a) // "provide multiple overloads of findFiles .def( "findFiles", - [](const IOrganizer* o, QString const& p, + [](const IOrganizer* o, DirectoryWrapper const& p, std::function const& f) { return o->findFiles(p, f); }, @@ -474,19 +477,21 @@ namespace mo2::python { // single-character strings: .def( "findFiles", - [](const IOrganizer* o, QString const& p, const QStringList& gf) { + [](const IOrganizer* o, DirectoryWrapper const& p, + const QStringList& gf) { return o->findFiles(p, gf); }, "path"_a, "patterns"_a) .def( "findFiles", - [](const IOrganizer* o, QString const& p, const QString& f) { + [](const IOrganizer* o, DirectoryWrapper const& p, const QString& f) { return o->findFiles(p, QStringList{f}); }, "path"_a, "pattern"_a) .def("getFileOrigins", &IOrganizer::getFileOrigins, "filename"_a) - .def("findFileInfos", &IOrganizer::findFileInfos, "path"_a, "filter"_a) + .def("findFileInfos", wrap_for_directory(&IOrganizer::findFileInfos), + "path"_a, "filter"_a) .def("virtualFileTree", &IOrganizer::virtualFileTree) @@ -503,9 +508,9 @@ namespace mo2::python { // argument to a return-tuple for waitForApplication .def( "startApplication", - [](IOrganizer* o, const QString& executable, const QStringList& args, - const QString& cwd, const QString& profile, - const QString& forcedCustomOverwrite, + [](IOrganizer* o, const FileWrapper& executable, + const QStringList& args, const DirectoryWrapper& cwd, + const QString& profile, const QString& forcedCustomOverwrite, bool ignoreCustomOverwrite) -> std::uintptr_t { return (std::uintptr_t)o->startApplication( executable, args, cwd, profile, forcedCustomOverwrite, @@ -659,7 +664,7 @@ namespace mo2::python { .def( "installArchive", [](IInstallationManager* m, GuessedValue modName, - QString archive, int modId) { + FileWrapper archive, int modId) { auto result = m->installArchive(modName, archive, modId); return std::make_tuple(result, static_cast(modName), modId); diff --git a/src/mobase/wrappers/game_features.cpp b/src/mobase/wrappers/game_features.cpp index c08bdf8..ec95f43 100644 --- a/src/mobase/wrappers/game_features.cpp +++ b/src/mobase/wrappers/game_features.cpp @@ -155,12 +155,12 @@ namespace mo2::python { public: QString BinaryName() const override { - PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, BinaryName, ); + PYBIND11_OVERRIDE_PURE(FileWrapper, ScriptExtender, BinaryName, ); } QString PluginPath() const override { - PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, PluginPath, ); + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, ScriptExtender, PluginPath, ); } QString loaderName() const override @@ -170,7 +170,7 @@ namespace mo2::python { QString loaderPath() const override { - PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderPath, ); + PYBIND11_OVERRIDE_PURE(FileWrapper, ScriptExtender, loaderPath, ); } QString savegameExtension() const override @@ -206,11 +206,15 @@ namespace mo2::python { } QFileInfo referenceFile(const QString& modName) const override { - PYBIND11_OVERRIDE_PURE(QFileInfo, UnmanagedMods, referenceFile, modName); + PYBIND11_OVERRIDE_PURE(FileWrapper, UnmanagedMods, referenceFile, modName); } QStringList secondaryFiles(const QString& modName) const override { - PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, secondaryFiles, modName); + auto result = [&] { + PYBIND11_OVERRIDE_PURE(QList, UnmanagedMods, + secondaryFiles, modName); + }(); + return QList(result.begin(), result.end()); } }; diff --git a/src/mobase/wrappers/pyplugins.h b/src/mobase/wrappers/pyplugins.h index 1c15e6a..f6bd94c 100644 --- a/src/mobase/wrappers/pyplugins.h +++ b/src/mobase/wrappers/pyplugins.h @@ -370,11 +370,11 @@ namespace mo2::python { } QDir gameDirectory() const override { - PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, gameDirectory, ); + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, IPluginGame, gameDirectory, ); } QDir dataDirectory() const override { - PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, ); + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, IPluginGame, dataDirectory, ); } void setGamePath(const QString& path) override { @@ -382,11 +382,11 @@ namespace mo2::python { } QDir documentsDirectory() const override { - PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, documentsDirectory, ); + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, IPluginGame, documentsDirectory, ); } QDir savesDirectory() const override { - PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, savesDirectory, ); + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, IPluginGame, savesDirectory, ); } QList executables() const override { diff --git a/src/mobase/wrappers/wrappers.cpp b/src/mobase/wrappers/wrappers.cpp index 42e04e2..56711e1 100644 --- a/src/mobase/wrappers/wrappers.cpp +++ b/src/mobase/wrappers/wrappers.cpp @@ -33,7 +33,7 @@ namespace mo2::python { public: QString getFilepath() const override { - PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getFilepath, ); + PYBIND11_OVERRIDE_PURE(FileWrapper, ISaveGame, getFilepath, ); } QDateTime getCreationTime() const override diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h index 6e5d265..4a7debb 100644 --- a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -80,6 +80,15 @@ namespace pybind11::detail::qt { } } + const sipTypeDef* type = + qt::sipAPI()->api_find_type(MetaData::class_name); + if (type == nullptr) { + return false; + } + if (!qt::sipAPI()->api_can_convert_to_type(src.ptr(), type, 0)) { + return false; + } + // this would transfer responsibility for deconstructing the // object to C++, but pybind11 assumes l-value converters (such // as this) don't do that instead, this should be called within diff --git a/src/pybind11-utils/include/pybind11_utils/arg_wrapper.h b/src/pybind11-utils/include/pybind11_utils/arg_wrapper.h new file mode 100644 index 0000000..9f92b78 --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/arg_wrapper.h @@ -0,0 +1,142 @@ +#ifndef PYTHON_PYBIND11_UTILS_FUNCTION_WRAPPER_H +#define PYTHON_PYBIND11_UTILS_FUNCTION_WRAPPER_H + +#include +#include + +#include + +namespace mo2::python { + + namespace detail { + + // simple helper class that expose a ::type attribute which is U is I is in Is, + // V otherwise + template + struct wrap_arg; + + template + struct wrap_arg> { + using type = + std::conditional_t...>, + U, V>; + }; + + // helper type for wrap_arg + template + using wrap_arg_t = typename wrap_arg::type; + + template + auto wrap_fn_impl(std::index_sequence, Fn&& fn, R (*sg)(Args...), + std::index_sequence) + { + return [fn = std::forward(fn)]( + wrap_arg_t>... args) { + return std::invoke(fn, std::forward(args)...); + }; + } + template + auto make_convertible_index_sequence(std::index_sequence) + { + return std::index_sequence<(std::is_convertible_v ? Is : -1)...>{}; + } + + template + auto wrap_fn_impl(Fn&& fn, R (*sg)(Args...)) + { + if constexpr (sizeof...(Is) == 0) { + return wrap_fn_impl(make_convertible_index_sequence( + std::make_index_sequence{}), + std::forward(fn), sg, + std::make_index_sequence{}); + } + else { + return wrap_fn_impl(std::index_sequence{}, + std::forward(fn), sg, + std::make_index_sequence{}); + } + } + + template + struct load_wrapped_argument_helper; + + template + struct load_wrapped_argument_helper { + static bool load(Type& value, pybind11::handle src, bool convert) + { + return false; + } + }; + + template + struct load_wrapped_argument_helper { + static bool load(Type& value, pybind11::handle src, bool convert) + { + pybind11::detail::make_caster caster; + + if (caster.load(src, convert)) { + value = Type{static_cast(caster)}; + return true; + } + + return load_wrapped_argument_helper::load( + value, src, convert); + } + }; + + } // namespace detail + + template + auto wrap_arguments(Fn&& fn) + { + return detail::wrap_fn_impl( + std::forward(fn), (pybind11::detail::function_signature_t*)nullptr); + } + + template + auto wrap_arguments(R (*fn)(Args...)) + { + return detail::wrap_fn_impl(fn, fn); + } + + template + auto wrap_arguments(R (C::*fn)(Args...)) + { + return detail::wrap_fn_impl(fn, (R(*)(C*, Args...)) nullptr); + } + + template + auto wrap_arguments(R (C::*fn)(Args...) &) + { + return detail::wrap_fn_impl(fn, (R(*)(C*, Args...)) nullptr); + } + + template + auto wrap_arguments(R (C::*fn)(Args...) const) + { + return detail::wrap_fn_impl(fn, (R(*)(const C*, Args...)) nullptr); + } + + template + auto wrap_arguments(R (C::*fn)(Args...) const&) + { + return detail::wrap_fn_impl(fn, (R(*)(const C*, Args...)) nullptr); + } + +} // namespace mo2::python + +#define MO2_PYBIND11_WRAP_ARGUMENT_CASTER(Type, ...) \ + namespace pybind11::detail { \ + template <> \ + struct type_caster { \ + PYBIND11_TYPE_CASTER(Type, const_name(#Type)); \ + bool load(handle src, bool convert) \ + { \ + return mo2::python::detail::load_wrapped_argument_helper< \ + Type, __VA_ARGS__>::load(value, src, convert); \ + } \ + }; \ + } + +#endif diff --git a/tests/python/test_argument_wrapper.cpp b/tests/python/test_argument_wrapper.cpp new file mode 100644 index 0000000..77a04ec --- /dev/null +++ b/tests/python/test_argument_wrapper.cpp @@ -0,0 +1,69 @@ +#include "pybind11_utils/arg_wrapper.h" + +#include +#include + +#include + +// wrapper that can be constructed from +class Wrapper { + std::string value; + +public: + Wrapper() = default; + + Wrapper(Wrapper const&) = default; + Wrapper(Wrapper&&) = default; + Wrapper& operator=(Wrapper const&) = default; + Wrapper& operator=(Wrapper&&) = default; + + template , int> = 0> + Wrapper(U&& u) : value{std::forward(u)} + { + } + + Wrapper(int u) : value{std::to_string(u)} {} + + operator int() const { return std::stoi(value); } + operator std::string() const { return value; } +}; + +MO2_PYBIND11_WRAP_ARGUMENT_CASTER(Wrapper, int, std::string); + +template +auto wrap(Fn&& fn) +{ + return mo2::python::wrap_arguments(std::forward(fn)); +} + +std::string fn1(std::string const& value) +{ + return value + "-" + value; +} + +int fn2(int value) +{ + return value * 2; +} + +std::string fn3(int value, std::vector values, std::string const& name) +{ + return name + "-" + std::to_string(value + values.size()); +} + +PYBIND11_MODULE(argument_wrapper, m) +{ + m.def("fn1_raw", &fn1); + m.def("fn1_wrap", wrap(&fn1)); + m.def("fn1_wrap_0", wrap<0>(&fn1)); + + m.def("fn2_raw", &fn2); + m.def("fn2_wrap", wrap(&fn2)); + m.def("fn2_wrap_0", wrap<0>(&fn2)); + + m.def("fn3_raw", &fn3); + m.def("fn3_wrap", wrap(&fn3)); + m.def("fn3_wrap_0", wrap<0>(&fn3)); + m.def("fn3_wrap_2", wrap<2>(&fn3)); + m.def("fn3_wrap_0_2", wrap<0, 2>(&fn3)); +} diff --git a/tests/python/test_argument_wrapper.py b/tests/python/test_argument_wrapper.py new file mode 100644 index 0000000..37868e5 --- /dev/null +++ b/tests/python/test_argument_wrapper.py @@ -0,0 +1,63 @@ +import mobase +import pytest + +m = pytest.importorskip("mobase_tests.argument_wrapper") + + +def test_argument_wrapper_fn1(): + assert m.fn1_raw("hello") == "hello-hello" + + with pytest.raises(TypeError): + m.fn1_raw(1) + + assert m.fn1_wrap("hello") == "hello-hello" + assert m.fn1_wrap(32) == "32-32" + + assert m.fn1_wrap_0("world") == "world-world" + assert m.fn1_wrap_0(45) == "45-45" + + +def test_argument_wrapper_fn2(): + assert m.fn2_raw(33) == 66 + + with pytest.raises(TypeError): + m.fn2_raw("12") + + assert m.fn2_wrap("15") == 30 + assert m.fn2_wrap(32) == 64 + + assert m.fn2_wrap_0("-15") == -30 + assert m.fn2_wrap_0(45) == 90 + + +def test_argument_wrapper_fn3(): + assert m.fn3_raw(33, [], "hello") == "hello-33" + assert m.fn3_raw(33, [1, 2], "hello") == "hello-35" + + with pytest.raises(TypeError): + m.fn3_raw("12", [], "hello") + + with pytest.raises(TypeError): + m.fn3_raw(36, [], 136) + + assert m.fn3_wrap(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap("15", [0], "woot") == "woot-16" + assert m.fn3_wrap(17, [], 33) == "33-17" + assert m.fn3_wrap("15", [], 44) == "44-15" + + assert m.fn3_wrap_0_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0_2("15", [0], "woot") == "woot-16" + assert m.fn3_wrap_0_2(17, [], 33) == "33-17" + assert m.fn3_wrap_0_2("15", [], 44) == "44-15" + + assert m.fn3_wrap_0(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0("15", [], "w00t") == "w00t-15" + + with pytest.raises(TypeError): + m.fn3_wrap_0(14, [], 12) + + assert m.fn3_wrap_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_2(15, [], 18) == "18-15" + + with pytest.raises(TypeError): + m.fn3_wrap_2("14", [], 12) diff --git a/tests/python/test_path_wrappers.py b/tests/python/test_path_wrappers.py new file mode 100644 index 0000000..1975656 --- /dev/null +++ b/tests/python/test_path_wrappers.py @@ -0,0 +1,45 @@ +import sys +from pathlib import Path + +import mobase +import pytest +from PyQt6.QtCore import QDir, QFileInfo + + +def test_filepath_wrappers(): + + # TBC that this works everywhere + version = ".".join(map(str, sys.version_info[:3])) + + # from string, ok + assert mobase.getProductVersion(sys.executable) == version + + # from path, ok + assert mobase.getProductVersion(Path(sys.executable)) == version + + # from QDir, ko + with pytest.raises(TypeError): + mobase.getProductVersion(QDir(sys.executable)) + + +def test_executableinfo(): + info = mobase.ExecutableInfo("exe", QFileInfo(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", sys.executable) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", Path(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info.withWorkingDirectory(Path(__file__).parent) + assert info.workingDirectory() == QFileInfo(__file__).dir() + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(Path(".")) + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".")