Add argument wrapper for directory and files.

This commit is contained in:
Mikaël Capelle
2022-05-04 20:54:06 +02:00
parent b42ce25b6e
commit 65ccc4af9c
11 changed files with 437 additions and 27 deletions
+6 -3
View File
@@ -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:
+72 -2
View File
@@ -1,21 +1,91 @@
#ifndef PYTHON_PYBIND11_ALL_H
#define PYTHON_PYBIND11_ALL_H
#include <filesystem>
#include <QDir>
#include <pybind11/operators.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/stl/filesystem.h>
#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 <isavegame.h>
#include <pluginrequirements.h>
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 <std::size_t... Is, class Fn>
auto wrap_for_filepath(Fn&& fn)
{
return mo2::python::wrap_arguments<FileWrapper, Is...>(std::forward<Fn>(fn));
}
template <std::size_t... Is, class Fn>
auto wrap_for_directory(Fn&& fn)
{
return mo2::python::wrap_arguments<DirectoryWrapper, Is...>(
std::forward<Fn>(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
+17 -12
View File
@@ -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_<ExecutableInfo>(m, "ExecutableInfo")
.def(py::init<const QString&, const QFileInfo&>(), "title"_a, "binary"_a)
.def(py::init<const QString&, const FileWrapper&>(), "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<bool(QString const&)> 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<QString> modName,
QString archive, int modId) {
FileWrapper archive, int modId) {
auto result = m->installArchive(modName, archive, modId);
return std::make_tuple(result, static_cast<QString>(modName),
modId);
+9 -5
View File
@@ -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<FileWrapper>, UnmanagedMods,
secondaryFiles, modName);
}();
return QList<QString>(result.begin(), result.end());
}
};
+4 -4
View File
@@ -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<ExecutableInfo> executables() const override
{
+1 -1
View File
@@ -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
@@ -80,6 +80,15 @@ namespace pybind11::detail::qt {
}
}
const sipTypeDef* type =
qt::sipAPI()->api_find_type(MetaData<QClass>::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
@@ -0,0 +1,142 @@
#ifndef PYTHON_PYBIND11_UTILS_FUNCTION_WRAPPER_H
#define PYTHON_PYBIND11_UTILS_FUNCTION_WRAPPER_H
#include <functional>
#include <type_traits>
#include <pybind11/pybind11.h>
namespace mo2::python {
namespace detail {
// simple helper class that expose a ::type attribute which is U is I is in Is,
// V otherwise
template <class U, class V, std::size_t I, class Is>
struct wrap_arg;
template <class U, class V, std::size_t I, std::size_t... Is>
struct wrap_arg<U, V, I, std::index_sequence<Is...>> {
using type =
std::conditional_t<std::disjunction_v<std::bool_constant<I == Is>...>,
U, V>;
};
// helper type for wrap_arg
template <class U, class A, std::size_t I, class Is>
using wrap_arg_t = typename wrap_arg<U, A, I, Is>::type;
template <class T, std::size_t... Is, std::size_t... AIs, class Fn, class R,
class... Args>
auto wrap_fn_impl(std::index_sequence<Is...>, Fn&& fn, R (*sg)(Args...),
std::index_sequence<AIs...>)
{
return [fn = std::forward<Fn>(fn)](
wrap_arg_t<T, Args, AIs, std::index_sequence<Is...>>... args) {
return std::invoke(fn, std::forward<decltype(args)>(args)...);
};
}
template <class T, class... Args, std::size_t... Is>
auto make_convertible_index_sequence(std::index_sequence<Is...>)
{
return std::index_sequence<(std::is_convertible_v<Args, T> ? Is : -1)...>{};
}
template <class T, std::size_t... Is, class Fn, class R, class... Args>
auto wrap_fn_impl(Fn&& fn, R (*sg)(Args...))
{
if constexpr (sizeof...(Is) == 0) {
return wrap_fn_impl<T>(make_convertible_index_sequence<T, Args...>(
std::make_index_sequence<sizeof...(Args)>{}),
std::forward<Fn>(fn), sg,
std::make_index_sequence<sizeof...(Args)>{});
}
else {
return wrap_fn_impl<T>(std::index_sequence<Is...>{},
std::forward<Fn>(fn), sg,
std::make_index_sequence<sizeof...(Args)>{});
}
}
template <class Type, class... WrappedTypes>
struct load_wrapped_argument_helper;
template <class Type>
struct load_wrapped_argument_helper<Type> {
static bool load(Type& value, pybind11::handle src, bool convert)
{
return false;
}
};
template <class Type, class WrappedType, class... WrappedTypes>
struct load_wrapped_argument_helper<Type, WrappedType, WrappedTypes...> {
static bool load(Type& value, pybind11::handle src, bool convert)
{
pybind11::detail::make_caster<WrappedType> caster;
if (caster.load(src, convert)) {
value = Type{static_cast<WrappedType>(caster)};
return true;
}
return load_wrapped_argument_helper<Type, WrappedTypes...>::load(
value, src, convert);
}
};
} // namespace detail
template <class T, std::size_t... Is, class Fn>
auto wrap_arguments(Fn&& fn)
{
return detail::wrap_fn_impl<T, Is...>(
std::forward<Fn>(fn), (pybind11::detail::function_signature_t<Fn>*)nullptr);
}
template <class T, std::size_t... Is, class R, class... Args>
auto wrap_arguments(R (*fn)(Args...))
{
return detail::wrap_fn_impl<T, Is...>(fn, fn);
}
template <class T, std::size_t... Is, class R, class C, class... Args>
auto wrap_arguments(R (C::*fn)(Args...))
{
return detail::wrap_fn_impl<T, Is...>(fn, (R(*)(C*, Args...)) nullptr);
}
template <class T, std::size_t... Is, class R, class C, class... Args>
auto wrap_arguments(R (C::*fn)(Args...) &)
{
return detail::wrap_fn_impl<T, Is...>(fn, (R(*)(C*, Args...)) nullptr);
}
template <class T, std::size_t... Is, class R, class C, class... Args>
auto wrap_arguments(R (C::*fn)(Args...) const)
{
return detail::wrap_fn_impl<T, Is...>(fn, (R(*)(const C*, Args...)) nullptr);
}
template <class T, std::size_t... Is, class R, class C, class... Args>
auto wrap_arguments(R (C::*fn)(Args...) const&)
{
return detail::wrap_fn_impl<T, Is...>(fn, (R(*)(const C*, Args...)) nullptr);
}
} // namespace mo2::python
#define MO2_PYBIND11_WRAP_ARGUMENT_CASTER(Type, ...) \
namespace pybind11::detail { \
template <> \
struct type_caster<Type> { \
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
+69
View File
@@ -0,0 +1,69 @@
#include "pybind11_utils/arg_wrapper.h"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <string>
// 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 <class U, std::enable_if_t<std::is_convertible_v<U, std::string>, int> = 0>
Wrapper(U&& u) : value{std::forward<U>(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 <std::size_t... Is, class Fn>
auto wrap(Fn&& fn)
{
return mo2::python::wrap_arguments<Wrapper, Is...>(std::forward<Fn>(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<int> 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));
}
+63
View File
@@ -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)
+45
View File
@@ -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(".")