Merge pull request #103 from ModOrganizer2/python-rework

Python rework
This commit is contained in:
Mikaël Capelle
2022-05-02 20:32:05 +02:00
committed by GitHub
86 changed files with 5864 additions and 4871 deletions
+21
View File
@@ -0,0 +1,21 @@
---
# We'll use defaults from the LLVM style, but with 4 columns indentation.
BasedOnStyle: LLVM
IndentWidth: 4
---
Language: Cpp
# Force pointers to the type for C++.
DerivePointerAlignment: false
PointerAlignment: Left
AlignConsecutiveAssignments: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Empty
AlwaysBreakTemplateDeclarations: Yes
AccessModifierOffset: -4
AlignTrailingComments: true
SpacesBeforeTrailingComments: 2
NamespaceIndentation: All
MaxEmptyLinesToKeep: 1
BreakBeforeBraces: Stroustrup
ColumnLimit: 88
+7
View File
@@ -1,5 +1,12 @@
# build
edit
CMakeLists.txt.user
/msbuild.log
/*std*.log
/*build
# python
tests/**/__pycache__
# IDE
.vscode
+22
View File
@@ -6,8 +6,30 @@ else()
include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake)
endif()
set(PYTHON_BUILD_PATH ${PYTHON_ROOT}/PCBuild/amd64)
set(PYVERSION 310)
set(PYBIND11_FINDPYTHON true)
set(PYTHON_EXECUTABLE ${PYTHON_BUILD_PATH}/python.exe)
set(PYTHON_INCLUDE_DIRS ${PYTHON_ROOT}/Include)
set(PYTHON_LIBRARIES ${MO2_INSTALL_LIBS_PATH}/python${PYVERSION}.lib)
add_subdirectory(${MO2_BUILD_PATH}/pybind11 ${CMAKE_CURRENT_BINARY_DIR}/pybind11)
project(plugin_python)
# order matters!
add_subdirectory(src/pybind11-qt)
add_subdirectory(src/mobase)
add_subdirectory(src/runner)
add_subdirectory(src/proxy)
# force plugin_python to build mobase
add_dependencies(plugin_python mobase)
set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT plugin_python)
set(PLUGIN_PYTHON_TESTS ${PLUGIN_PYTHON_TESTS} CACHE BOOL "build tests for plugin_python")
if (PLUGIN_PYTHON_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
+14
View File
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.16)
pybind11_add_module(mobase MODULE)
mo2_configure_library(mobase
SOURCE_TREE
WARNINGS OFF
AUTOMOC ON
TRANSLATIONS OFF
PRIVATE_DEPENDS uibase Qt::Core
)
target_link_libraries(mobase PRIVATE pybind11::qt)
target_include_directories(mobase PRIVATE ${PYTHON_ROOT}/Include)
install(TARGETS mobase DESTINATION bin/plugins/plugin_python/libs)
+54
View File
@@ -0,0 +1,54 @@
#include "deprecation.h"
#include <filesystem>
#include <set>
#include <pybind11/pybind11.h>
#include <QCoreApplication>
#include "log.h"
namespace py = pybind11;
namespace mo2::python {
void show_deprecation_warning(std::string_view name, std::string_view message,
bool show_once)
{
// Contains the list of filename / line number for which a deprecation
// warning has already been shown.
static std::set<std::pair<std::string, int>> DeprecatedLines;
// Find the caller:
auto inspect = py::module_::import("inspect");
auto current_frame = inspect.attr("currentframe")();
py::sequence callable_frame = inspect.attr("getouterframes")(current_frame, 2);
auto filename = callable_frame[-1].attr("filename").cast<std::string>();
auto function = callable_frame[-1].attr("function").cast<std::string>();
auto lineno = callable_frame[-1].attr("lineno").cast<int>();
// Only show once if requested:
if (show_once && DeprecatedLines.contains({filename, lineno})) {
return;
}
// Register the deprecation:
DeprecatedLines.emplace(filename, lineno);
auto path = relative(std::filesystem::path(filename),
QCoreApplication::applicationDirPath().toStdWString());
// Show the message:
if (message.empty()) {
MOBase::log::warn("[deprecated] {} in {} [{}:{}].", name, function,
path.native(), lineno);
}
else {
MOBase::log::warn("[deprecated] {} in {} [{}:{}]: {}", name, function,
path.native(), lineno, message);
}
}
} // namespace mo2::python
+27
View File
@@ -0,0 +1,27 @@
#ifndef PYTHONRUNNER_UTILS_H
#define PYTHONRUNNER_UTILS_H
#include <string_view>
#include <pybind11/pybind11.h>
namespace mo2::python {
/**
* @brief Show a deprecation warning.
*
* This methods will print a warning in MO2 log containing the location of
* the call to the deprecated function. If show_once is true, the
* deprecation warning will only be logged the first time the function is
* called at this location.
*
* @param name Name of the deprecated function.
* @param message Deprecation message.
* @param show_once Only show the message once per call location.
*/
void show_deprecation_warning(std::string_view name, std::string_view message = "",
bool show_once = true);
} // namespace mo2::python
#endif
+82
View File
@@ -0,0 +1,82 @@
#pragma warning(disable : 4100)
#pragma warning(disable : 4996)
#include <tuple>
#include <variant>
#include "pybind11_all.h"
#include "wrappers/pyfiletree.h"
#include "wrappers/wrappers.h"
// TODO: remove these include (only for testing)
#include <QDir>
#include <QFile>
#include <QWidget>
#include <fmt/format.h>
#include <iplugin.h>
#include <iplugindiagnose.h>
#include <ipluginfilemapper.h>
#include <iplugingame.h>
#include <iplugininstaller.h>
#include <iplugininstallersimple.h>
#include <ipluginlist.h>
#include <ipluginmodpage.h>
#include <ipluginpreview.h>
#include <iplugintool.h>
#include <isavegame.h>
#include <isavegameinfowidget.h>
using namespace MOBase;
namespace py = pybind11;
PYBIND11_MODULE(mobase, m)
{
using namespace mo2::python;
py::module_::import("PyQt6.QtCore");
py::module_::import("PyQt6.QtWidgets");
// bindings
//
mo2::python::add_basic_bindings(m);
mo2::python::add_wrapper_bindings(m);
// game features must be added before plugins
mo2::python::add_game_feature_bindings(m);
mo2::python::add_plugins_bindings(m);
// widgets
//
py::module_ widgets(
py::reinterpret_borrow<py::module_>(PyImport_AddModule("mobase.widgets")));
m.attr("widgets") = widgets;
mo2::python::add_widget_bindings(widgets);
// 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"));
// 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:
// mobase.MoVariant))
//
// the real MoVariant is defined in the generated stubs, since it's only
// relevant when doing type-checking, but this needs to be defined,
// otherwise MoVariant is not found when actually running plugins through
// MO2, making them crash
m.attr("MoVariant") = py::none();
// private stuff for debugging/test
py::module_ moprivate(
py::reinterpret_borrow<py::module_>(PyImport_AddModule("mobase.private")));
m.attr("private") = moprivate;
// expose a function to create a particular tree, only for debugging
// purpose, not in mobase.
mo2::python::add_make_tree_function(moprivate);
moprivate.def("extract_plugins", &mo2::python::extract_plugins);
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef PYTHON_PYBIND11_ALL_H
#define PYTHON_PYBIND11_ALL_H
#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/shared_cpp_owner.h"
#endif
+7
View File
@@ -0,0 +1,7 @@
#ifndef PYTHON_PYBIND11_FUNCTIONAL_H
#define PYTHON_PYBIND11_FUNCTIONAL_H
// TODO
#include <pybind11/functional.h>
#endif
@@ -0,0 +1,98 @@
#ifndef PYTHON_PYBIND11_SHARED_CPP_OWNER_H
#define PYTHON_PYBIND11_SHARED_CPP_OWNER_H
// pybind11 has some issues when a Python classes extend a C++ wrapper since the Python
// object is not kept alive alongside the returned object
//
// there is a pybind11 branch called "smart_holder" that tries to solve this in a very
// complicated way (with many other features)
//
// here, we simply use a custom type_caster<> for the classes we need - see the actual
// definition in mo2::python::detail below
//
// IMPORTANT: this only works for classes that are managed by shared_ptr on the C++
// side, not Qt object
//
// TODO: WIP for Qt object
namespace mo2::python::detail {
template <class Type, class SharedType>
struct shared_cpp_owner_caster
: pybind11::detail::copyable_holder_caster<Type, SharedType> {
// note that the actual holder type might be different in term of constness
using type = Type;
using holder_type = SharedType;
using base = pybind11::detail::copyable_holder_caster<Type, SharedType>;
using base::holder;
using base::value;
// in load, we use the default type_caster<> to extract the shared pointer, then
// we replace it by a custom one
//
// the custom shared_ptr<> holds the py::object BUT does not really manage the
// C++ object because it will ref-count but not delete it
//
// this should work because here it's how it works:
// - the Python object holds a standard shared_ptr<> for the C++ object -> the
// C++ object remains alive as long as the Python one remains alive
// - the C++ object holds a shared_ptr<> that manages the python object -> the
// Python object remains alive as-long as there is a shared_ptr<> on the C++
// side
//
bool load(pybind11::handle src, bool convert)
{
namespace py = pybind11;
if (!base::load(src, convert)) {
return false;
}
holder.reset(holder.get(), [pyobj = py::reinterpret_borrow<py::object>(
src)](auto*) mutable {
py::gil_scoped_acquire s;
pyobj = std::move(py::none());
// we do NOT delete the object here - if this was the last reference to
// the Python object, the Python object will delete it
});
return true;
}
// cast simply forward to the original type_caster<>
//
static pybind11::handle cast(const holder_type& src,
pybind11::return_value_policy policy,
pybind11::handle parent)
{
return base::cast(src, policy, parent);
}
};
} // namespace mo2::python::detail
#define MO2_PYBIND11_SHARED_CPP_HOLDER(Type) \
namespace pybind11::detail { \
template <> \
struct type_caster<std::shared_ptr<Type>> \
: mo2::python::detail::shared_cpp_owner_caster<Type, \
std::shared_ptr<Type>> { \
}; \
template <> \
struct type_caster<std::shared_ptr<const Type>> \
: mo2::python::detail::shared_cpp_owner_caster< \
Type, std::shared_ptr<const Type>> { \
}; \
}
#include <isavegame.h>
#include <pluginrequirements.h>
MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement)
MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame)
#endif
File diff suppressed because it is too large Load Diff
+377
View File
@@ -0,0 +1,377 @@
#include "wrappers.h"
#include <tuple>
#include "../pybind11_all.h"
#include <ipluginlist.h>
#include <isavegameinfowidget.h>
#include <bsainvalidation.h>
#include <dataarchives.h>
#include <gameplugins.h>
#include <localsavegames.h>
#include <moddatachecker.h>
#include <moddatacontent.h>
#include <savegameinfo.h>
#include <scriptextender.h>
#include <unmanagedmods.h>
#include "pyfiletree.h"
namespace py = pybind11;
using namespace MOBase;
using namespace pybind11::literals;
namespace mo2::python {
class PyBSAInvalidation : public BSAInvalidation {
public:
bool isInvalidationBSA(const QString& bsaName) override
{
PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, isInvalidationBSA, bsaName);
}
void deactivate(MOBase::IProfile* profile) override
{
PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, deactivate, profile);
}
void activate(MOBase::IProfile* profile) override
{
PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, activate, profile);
}
bool prepareProfile(MOBase::IProfile* profile) override
{
PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, prepareProfile, profile);
}
};
class PyDataArchives : public DataArchives {
public:
QStringList vanillaArchives() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, vanillaArchives, );
}
QStringList archives(const MOBase::IProfile* profile) const override
{
PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, archives, profile);
}
void addArchive(MOBase::IProfile* profile, int index,
const QString& archiveName) override
{
PYBIND11_OVERRIDE_PURE(void, DataArchives, addArchive, profile, index,
archiveName);
}
void removeArchive(MOBase::IProfile* profile,
const QString& archiveName) override
{
PYBIND11_OVERRIDE_PURE(void, DataArchives, removeArchive, profile,
archiveName);
}
};
class PyGamePlugins : public GamePlugins {
public:
void writePluginLists(const MOBase::IPluginList* pluginList) override
{
PYBIND11_OVERRIDE_PURE(void, GamePlugins, writePluginLists, pluginList);
}
void readPluginLists(MOBase::IPluginList* pluginList) override
{
// TODO: cannot update plugin list or create one from Python so this is
// useless
PYBIND11_OVERRIDE_PURE(void, GamePlugins, readPluginLists, pluginList);
}
QStringList getLoadOrder() override
{
PYBIND11_OVERRIDE_PURE(QStringList, GamePlugins, getLoadOrder, );
}
bool lightPluginsAreSupported() override
{
PYBIND11_OVERRIDE_PURE(bool, GamePlugins, lightPluginsAreSupported, );
}
};
class PyLocalSavegames : public LocalSavegames {
public:
MappingType mappings(const QDir& profileSaveDir) const override
{
PYBIND11_OVERRIDE_PURE(MappingType, LocalSavegames, mappings,
profileSaveDir);
}
bool prepareProfile(MOBase::IProfile* profile) override
{
PYBIND11_OVERRIDE_PURE(bool, LocalSavegames, prepareProfile, profile);
}
};
class PyModDataChecker : public ModDataChecker {
public:
CheckReturn
dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override
{
PYBIND11_OVERRIDE_PURE(CheckReturn, ModDataChecker, dataLooksValid,
fileTree);
}
std::shared_ptr<MOBase::IFileTree>
fix(std::shared_ptr<MOBase::IFileTree> fileTree) const override
{
PYBIND11_OVERRIDE(std::shared_ptr<MOBase::IFileTree>, ModDataChecker, fix,
fileTree);
}
};
class PyModDataContent : public ModDataContent {
public:
std::vector<Content> getAllContents() const override
{
PYBIND11_OVERRIDE_PURE(std::vector<Content>, ModDataContent,
getAllContents, );
;
}
std::vector<int>
getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override
{
PYBIND11_OVERRIDE_PURE(std::vector<int>, ModDataContent, getContentsFor,
fileTree);
}
};
class PySaveGameInfo : public SaveGameInfo {
public:
MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override
{
PYBIND11_OVERRIDE_PURE(MissingAssets, SaveGameInfo, getMissingAssets,
&save);
}
ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const override
{
PYBIND11_OVERRIDE_PURE(ISaveGameInfoWidget*, SaveGameInfo,
getSaveGameWidget, parent);
}
};
class PyScriptExtender : public ScriptExtender {
public:
QString BinaryName() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, BinaryName, );
}
QString PluginPath() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, PluginPath, );
}
QString loaderName() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderName, );
}
QString loaderPath() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderPath, );
}
QString savegameExtension() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, savegameExtension, );
}
bool isInstalled() const override
{
PYBIND11_OVERRIDE_PURE(bool, ScriptExtender, isInstalled, );
}
QString getExtenderVersion() const override
{
PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, getExtenderVersion, );
}
WORD getArch() const override
{
PYBIND11_OVERRIDE_PURE(WORD, ScriptExtender, getArch, );
}
};
class PyPyUnmanagedMods : public UnmanagedMods {
public:
QStringList mods(bool onlyOfficial) const override
{
PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, mods, onlyOfficial);
}
QString displayName(const QString& modName) const override
{
PYBIND11_OVERRIDE_PURE(QString, UnmanagedMods, displayName, modName);
}
QFileInfo referenceFile(const QString& modName) const override
{
PYBIND11_OVERRIDE_PURE(QFileInfo, UnmanagedMods, referenceFile, modName);
}
QStringList secondaryFiles(const QString& modName) const override
{
PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, secondaryFiles, modName);
}
};
void add_game_feature_bindings(pybind11::module_ m)
{
// BSAInvalidation
py::class_<BSAInvalidation, PyBSAInvalidation>(m, "BSAInvalidation")
.def(py::init<>())
.def("isInvalidationBSA", &BSAInvalidation::isInvalidationBSA, "name"_a)
.def("deactivate", &BSAInvalidation::deactivate, "profile"_a)
.def("activate", &BSAInvalidation::activate, "profile"_a);
// DataArchives
py::class_<DataArchives, PyDataArchives>(m, "DataArchives")
.def(py::init<>())
.def("vanillaArchives", &DataArchives::vanillaArchives)
.def("archives", &DataArchives::archives, "profile"_a)
.def("addArchive", &DataArchives::addArchive,
("profile"_a, "index", "name"))
.def("removeArchive", &DataArchives::removeArchive, "profile"_a, "name"_a);
// GamePlugins
py::class_<GamePlugins, PyGamePlugins>(m, "GamePlugins")
.def(py::init<>())
.def("writePluginLists", &GamePlugins::writePluginLists, "plugin_list"_a)
.def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a)
.def("getLoadOrder", &GamePlugins::getLoadOrder)
.def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported);
// LocalSavegames
py::class_<LocalSavegames, PyLocalSavegames>(m, "LocalSavegames")
.def(py::init<>())
.def("mappings", &LocalSavegames::mappings, "profile_save_dir"_a)
.def("prepareProfile", &LocalSavegames::prepareProfile, "profile"_a);
// ModDataChecker
py::class_<ModDataChecker, PyModDataChecker> pyModDataChecker(m,
"ModDataChecker");
py::enum_<ModDataChecker::CheckReturn>(pyModDataChecker, "CheckReturn")
.value("INVALID", ModDataChecker::CheckReturn::INVALID)
.value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE)
.value("VALID", ModDataChecker::CheckReturn::VALID)
.export_values();
pyModDataChecker.def(py::init<>())
.def("dataLooksValid", &ModDataChecker::dataLooksValid, "filetree"_a)
.def("fix", &ModDataChecker::fix, "filetree"_a);
// ModDataContent
py::class_<ModDataContent, PyModDataContent> pyModDataContent(m,
"ModDataContent");
py::class_<ModDataContent::Content>(pyModDataContent, "Content")
.def(py::init<int, QString, QString, bool>(), "id"_a, "name"_a, "icon"_a,
"filter_only"_a = false)
.def_property_readonly("id", &ModDataContent::Content::id)
.def_property_readonly("name", &ModDataContent::Content::name)
.def_property_readonly("icon", &ModDataContent::Content::icon)
.def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter);
pyModDataContent.def(py::init<>())
.def("getAllContents", &ModDataContent::getAllContents)
.def("getContentsFor", &ModDataContent::getContentsFor, "filetree"_a);
// SaveGameInfo
py::class_<SaveGameInfo, PySaveGameInfo>(m, "SaveGameInfo")
.def(py::init<>())
.def("getMissingAssets", &SaveGameInfo::getMissingAssets, "save"_a)
.def("getSaveGameWidget", &SaveGameInfo::getSaveGameWidget,
py::return_value_policy::reference, "parent"_a, "[optional]");
// ScriptExtender
py::class_<ScriptExtender, PyScriptExtender>(m, "ScriptExtender")
.def(py::init<>())
.def("BinaryName", &ScriptExtender::BinaryName)
.def("PluginPath", &ScriptExtender::PluginPath)
.def("loaderName", &ScriptExtender::loaderName)
.def("loaderPath", &ScriptExtender::loaderPath)
.def("savegameExtension", &ScriptExtender::savegameExtension)
.def("isInstalled", &ScriptExtender::isInstalled)
.def("getExtenderVersion", &ScriptExtender::getExtenderVersion)
.def("getArch", &ScriptExtender::getArch);
// UnmanagedMods
py::class_<UnmanagedMods, PyPyUnmanagedMods>(m, "UnmanagedMods")
.def(py::init<>())
.def("mods", &UnmanagedMods::mods, "official_only"_a)
.def("displayName", &UnmanagedMods::displayName, "mod_name"_a)
.def("referenceFile", &UnmanagedMods::referenceFile, "mod_name"_a)
.def("secondaryFiles", &UnmanagedMods::secondaryFiles, "mod_name"_a);
}
} // namespace mo2::python
namespace mo2::python {
class GameFeaturesHelper {
using GameFeatures = std::tuple<BSAInvalidation, DataArchives, GamePlugins,
LocalSavegames, ModDataChecker, ModDataContent,
SaveGameInfo, ScriptExtender, UnmanagedMods>;
template <class F, std::size_t... Is>
static void helper(F&& f, std::index_sequence<Is...>)
{
(f(static_cast<std::tuple_element_t<Is, GameFeatures>*>(nullptr)), ...);
}
public:
// apply the function f on a null-pointer of type Feature* for each game
// feature
template <class F>
static void apply(F&& f)
{
helper(f, std::make_index_sequence<std::tuple_size_v<GameFeatures>>{});
}
};
pybind11::object extract_feature(IPluginGame const& game, pybind11::object type)
{
py::object py_feature = py::none();
GameFeaturesHelper::apply([&]<class Feature>(Feature* feature) {
if (py::type::of<Feature>().is(type)) {
py_feature = py::cast(game.feature<Feature>(),
py::return_value_policy::reference);
}
});
return py_feature;
}
pybind11::dict extract_feature_list(IPluginGame const& game)
{
// constructing a dict from class name to actual object
py::dict dict;
GameFeaturesHelper::apply([&]<class Feature>(Feature* feature) {
dict[py::type::of<Feature>()] =
py::cast(game.feature<Feature>(), py::return_value_policy::reference);
});
return dict;
}
std::map<std::type_index, std::any>
convert_feature_list(py::dict const& py_features)
{
std::map<std::type_index, std::any> features;
GameFeaturesHelper::apply([&]<class Feature>(Feature* feature) {
const auto py_type = py::type::of<Feature>();
if (py_features.contains(py_type)) {
features[std::type_index(typeid(Feature))] =
py_features[py_type].cast<Feature*>();
}
});
return features;
}
} // namespace mo2::python
+333
View File
@@ -0,0 +1,333 @@
#include "pyfiletree.h"
#include <tuple>
#include <variant>
#include "../pybind11_all.h"
#include <ifiletree.h>
#include <log.h>
namespace py = pybind11;
using namespace MOBase;
namespace mo2::detail {
// filetree implementation for testing purpose
//
class PyFileTree : public IFileTree {
public:
using callback_t = std::function<bool(QString, bool)>;
PyFileTree(std::shared_ptr<const IFileTree> parent, QString name,
callback_t callback)
: FileTreeEntry(parent, name), IFileTree(), m_Callback(callback)
{
}
std::shared_ptr<FileTreeEntry> addFile(QString name, bool) override
{
if (m_Callback && !m_Callback(name, false)) {
throw UnsupportedOperationException("File rejected by callback.");
}
return IFileTree::addFile(name);
}
std::shared_ptr<IFileTree> addDirectory(QString name) override
{
if (m_Callback && !m_Callback(name, true)) {
throw UnsupportedOperationException("Directory rejected by callback.");
}
return IFileTree::addDirectory(name);
}
protected:
std::shared_ptr<IFileTree>
makeDirectory(std::shared_ptr<const IFileTree> parent,
QString name) const override
{
return std::make_shared<PyFileTree>(parent, name, m_Callback);
}
bool
doPopulate(std::shared_ptr<const IFileTree> parent,
std::vector<std::shared_ptr<FileTreeEntry>>& entries) const override
{
return true;
}
std::shared_ptr<IFileTree> doClone() const override
{
return std::make_shared<PyFileTree>(nullptr, name(), m_Callback);
}
private:
callback_t m_Callback;
};
} // namespace mo2::detail
#pragma optimize("", off)
namespace pybind11 {
const void* polymorphic_type_hook<FileTreeEntry>::get(const FileTreeEntry* src,
const std::type_info*& type)
{
if (auto p = dynamic_cast<const IFileTree*>(src)) {
type = &typeid(IFileTree);
return p;
}
return src;
}
} // namespace pybind11
namespace mo2::python {
void add_ifiletree_bindings(pybind11::module_& m)
{
// FileTreeEntry Scope:
auto fileTreeEntryClass =
py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m,
"FileTreeEntry");
// we do not use the enum directly, we will mostly bind the FileTypes
// (with an S)
py::enum_<FileTreeEntry::FileType>(fileTreeEntryClass, "FileType",
py::arithmetic{});
py::class_<FileTreeEntry::FileTypes>(fileTreeEntryClass, "FileTypes")
.def_property_readonly_static("FILE",
[](py::object) {
return FileTreeEntry::FILE;
})
.def_property_readonly_static("DIRECTORY",
[](py::object) {
return FileTreeEntry::DIRECTORY;
})
.def_property_readonly_static("FILE_OR_DIRECTORY",
[](py::object) {
return FileTreeEntry::FILE_OR_DIRECTORY;
})
.def(py::self == py::self)
.def(py::self != py::self)
.def(py::self | py::self);
py::implicitly_convertible<FileTreeEntry::FileType, FileTreeEntry::FileTypes>();
fileTreeEntryClass
.def_property_readonly_static("FILE",
[](py::object) {
return FileTreeEntry::FILE;
})
.def_property_readonly_static("DIRECTORY",
[](py::object) {
return FileTreeEntry::DIRECTORY;
})
.def_property_readonly_static("FILE_OR_DIRECTORY", [](py::object) {
return FileTreeEntry::FILE_OR_DIRECTORY;
});
fileTreeEntryClass
.def("isFile", &FileTreeEntry::isFile)
.def("isDir", &FileTreeEntry::isDir)
// Forcing the conversion to FileTypeS to avoid having to expose
// FileType in python:
.def("fileType", &FileTreeEntry::fileType)
// This should probably not be exposed in python since we provide
// automatic downcast: .def("getTree",
// static_cast<std::shared_ptr<IFileTree>(FileTreeEntry::*)()>(&FileTreeEntry::astree))
.def("name", &FileTreeEntry::name)
.def("suffix", &FileTreeEntry::suffix)
.def(
"hasSuffix",
[](FileTreeEntry* entry, QStringList suffixes) {
return entry->hasSuffix(suffixes);
},
py::arg("suffixes"))
.def(
"hasSuffix",
[](FileTreeEntry* entry, QString suffix) {
return entry->hasSuffix(suffix);
},
py::arg("suffix"))
.def("parent", py::overload_cast<>(&FileTreeEntry::parent), "[optional]")
.def("path", &FileTreeEntry::path, py::arg("sep") = "\\")
.def("pathFrom", &FileTreeEntry::pathFrom, py::arg("tree"),
py::arg("sep") = "\\")
// Mutable operation:
.def("detach", &FileTreeEntry::detach)
.def("moveTo", &FileTreeEntry::moveTo, py::arg("tree"))
// Special methods:
.def("__eq__",
[](const FileTreeEntry* entry, QString other) {
return entry->compare(other) == 0;
})
.def("__eq__",
[](const FileTreeEntry* entry, std::shared_ptr<FileTreeEntry> other) {
return entry == other.get();
})
// Special methods for debug:
.def("__repr__", [](const FileTreeEntry* entry) {
return "FileTreeEntry(\"" + entry->name() + "\")";
});
// IFileTree scope:
auto iFileTreeClass =
py::class_<IFileTree, FileTreeEntry, std::shared_ptr<IFileTree>>(
m, "IFileTree", py::multiple_inheritance());
py::enum_<IFileTree::InsertPolicy>(iFileTreeClass, "InsertPolicy")
.value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS)
.value("REPLACE", IFileTree::InsertPolicy::REPLACE)
.value("MERGE", IFileTree::InsertPolicy::MERGE)
.export_values();
py::enum_<IFileTree::WalkReturn>(iFileTreeClass, "WalkReturn")
.value("CONTINUE", IFileTree::WalkReturn::CONTINUE)
.value("STOP", IFileTree::WalkReturn::STOP)
.value("SKIP", IFileTree::WalkReturn::SKIP)
.export_values();
// Non-mutable operations:
iFileTreeClass.def("exists",
py::overload_cast<QString, IFileTree::FileTypes>(
&IFileTree::exists, py::const_),
py::arg("path"),
py::arg("type") = IFileTree::FILE_OR_DIRECTORY);
iFileTreeClass.def(
"find", py::overload_cast<QString, IFileTree::FileTypes>(&IFileTree::find),
py::arg("path"), py::arg("type") = IFileTree::FILE_OR_DIRECTORY,
"[optional]");
iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"),
py::arg("sep") = "\\");
// Note: walk() would probably be better as a generator in python, but
// it is likely impossible to construct from the C++ walk() method.
iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"),
py::arg("sep") = "\\");
// Kind-of-static operations:
iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree,
py::arg("name") = "");
// addFile() and addDirectory throws exception instead of returning null
// pointer in order to have better traces.
iFileTreeClass.def(
"addFile",
[](IFileTree* w, QString path, bool replaceIfExists) {
auto result = w->addFile(path, replaceIfExists);
if (result == nullptr) {
throw std::logic_error("addFile failed");
}
return result;
},
py::arg("path"), py::arg("replace_if_exists") = false);
iFileTreeClass.def(
"addDirectory",
[](IFileTree* w, QString path) {
auto result = w->addDirectory(path);
if (result == nullptr) {
throw std::logic_error("addDirectory failed");
}
return result;
},
py::arg("path"));
// Merge needs custom return types depending if the user wants overrides
// or not. A failure is translated into an exception for easier tracing
// and handling.
iFileTreeClass.def(
"merge",
[](IFileTree* p, std::shared_ptr<IFileTree> other, bool returnOverwrites)
-> std::variant<IFileTree::OverwritesType, std::size_t> {
IFileTree::OverwritesType overwrites;
auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr);
if (result == IFileTree::MERGE_FAILED) {
throw std::logic_error("merge failed");
}
if (returnOverwrites) {
return {overwrites};
}
return {result};
},
py::arg("other"), py::arg("overwrites") = false);
// Insert and erase returns an iterator, which makes no sense in python,
// so we convert it to bool. Erase is also renamed "remove" since
// "erase" is very C++.
iFileTreeClass.def(
"insert",
[](IFileTree* p, std::shared_ptr<FileTreeEntry> entry,
IFileTree::InsertPolicy insertPolicy) {
return p->insert(entry, insertPolicy) == p->end();
},
py::arg("entry"),
py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
iFileTreeClass.def(
"remove",
[](IFileTree* p, QString name) {
return p->erase(name).first != p->end();
},
py::arg("name"));
iFileTreeClass.def(
"remove",
[](IFileTree* p, std::shared_ptr<FileTreeEntry> entry) {
return p->erase(entry) != p->end();
},
py::arg("entry"));
iFileTreeClass.def("move", &IFileTree::move, py::arg("entry"), py::arg("path"),
py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
iFileTreeClass.def(
"copy",
[](IFileTree* w, std::shared_ptr<FileTreeEntry> entry, QString path,
IFileTree::InsertPolicy insertPolicy) {
auto result = w->copy(entry, path, insertPolicy);
if (result == nullptr) {
throw std::logic_error("copy failed");
}
return result;
},
py::arg("entry"), py::arg("path") = "",
py::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
iFileTreeClass.def("clear", &IFileTree::clear);
iFileTreeClass.def("removeAll", &IFileTree::removeAll, py::arg("names"));
iFileTreeClass.def("removeIf", &IFileTree::removeIf, py::arg("filter"));
// Special methods:
iFileTreeClass.def("__getitem__",
py::overload_cast<std::size_t>(&IFileTree::at));
iFileTreeClass.def("__iter__", [](IFileTree* tree) {
return py::make_iterator(*tree);
});
iFileTreeClass.def("__len__", &IFileTree::size);
iFileTreeClass.def(
"__bool__", +[](const IFileTree* tree) {
return !tree->empty();
});
iFileTreeClass.def(
"__repr__", +[](const IFileTree* entry) {
return "IFileTree(\"" + entry->name() + "\")";
});
}
void add_make_tree_function(pybind11::module_& m)
{
m.def(
"makeTree",
[](mo2::detail::PyFileTree::callback_t callback)
-> std::shared_ptr<IFileTree> {
return std::make_shared<mo2::detail::PyFileTree>(nullptr, "", callback);
},
py::arg("callback") = mo2::detail::PyFileTree::callback_t{});
}
} // namespace mo2::python
#pragma optimize("", on)
+34
View File
@@ -0,0 +1,34 @@
#ifndef MO2_PYTHON_FILETREE_H
#define MO2_PYTHON_FILETREE_H
#include "../pybind11_all.h"
#include <ifiletree.h>
namespace pybind11 {
template <>
struct polymorphic_type_hook<MOBase::FileTreeEntry> {
static const void* get(const MOBase::FileTreeEntry* src,
const std::type_info*& type);
};
} // namespace pybind11
namespace mo2::python {
/**
* @brief Add bindings for FileTreeEntry andIFileTree to the given module.
*
* @param mobase Module to add the bindings to.
*/
void add_ifiletree_bindings(pybind11::module_& m);
/**
* @brief Add makeTree() function to the given module, useful for debugging.
*
* @param mobase Module to add the function to.
*/
void add_make_tree_function(pybind11::module_& m);
} // namespace mo2::python
#endif
+272
View File
@@ -0,0 +1,272 @@
#include "wrappers.h"
#include <tuple>
#include "pyplugins.h"
namespace py = pybind11;
using namespace pybind11::literals;
using namespace MOBase;
namespace mo2::python {
std::map<std::type_index, std::any> PyPluginGame::featureList() const
{
py::dict pyFeatures = [this]() {
PYBIND11_OVERRIDE_PURE(py::dict, IPluginGame, _featureList, );
}();
return convert_feature_list(pyFeatures);
}
// this one is kind of big so it has its own function
void add_iplugingame_bindings(pybind11::module_ m)
{
py::enum_<IPluginGame::LoadOrderMechanism>(m, "LoadOrderMechanism")
.value("FileTime", IPluginGame::LoadOrderMechanism::FileTime)
.value("PluginsTxt", IPluginGame::LoadOrderMechanism::PluginsTxt)
.value("FILE_TIME", IPluginGame::LoadOrderMechanism::FileTime)
.value("PLUGINS_TXT", IPluginGame::LoadOrderMechanism::PluginsTxt);
py::enum_<IPluginGame::SortMechanism>(m, "SortMechanism")
.value("NONE", IPluginGame::SortMechanism::NONE)
.value("MLOX", IPluginGame::SortMechanism::MLOX)
.value("BOSS", IPluginGame::SortMechanism::BOSS)
.value("LOOT", IPluginGame::SortMechanism::LOOT);
// this does not actually do the conversion, but might be convenient
// for accessing the names for enum bits
py::enum_<IPluginGame::ProfileSetting>(m, "ProfileSetting", py::arithmetic())
.value("mods", IPluginGame::MODS)
.value("configuration", IPluginGame::CONFIGURATION)
.value("savegames", IPluginGame::SAVEGAMES)
.value("preferDefaults", IPluginGame::PREFER_DEFAULTS)
.value("MODS", IPluginGame::MODS)
.value("CONFIGURATION", IPluginGame::CONFIGURATION)
.value("SAVEGAMES", IPluginGame::SAVEGAMES)
.value("PREFER_DEFAULTS", IPluginGame::PREFER_DEFAULTS);
py::class_<IPluginGame, PyPluginGame, IPlugin,
std::unique_ptr<IPluginGame, py::nodelete>>(
m, "IPluginGame", py::multiple_inheritance())
.def(py::init<>())
.def("featureList", &extract_feature_list)
.def("feature", &extract_feature, "feature_type"_a,
py::return_value_policy::reference)
.def("detectGame", &IPluginGame::detectGame)
.def("gameName", &IPluginGame::gameName)
.def("initializeProfile", &IPluginGame::initializeProfile, "directory"_a,
"settings"_a)
.def("listSaves", &IPluginGame::listSaves, "folder"_a)
.def("isInstalled", &IPluginGame::isInstalled)
.def("gameIcon", &IPluginGame::gameIcon)
.def("gameDirectory", &IPluginGame::gameDirectory)
.def("dataDirectory", &IPluginGame::dataDirectory)
.def("setGamePath", &IPluginGame::setGamePath, "path"_a)
.def("documentsDirectory", &IPluginGame::documentsDirectory)
.def("savesDirectory", &IPluginGame::savesDirectory)
.def("executables", &IPluginGame::executables)
.def("executableForcedLoads", &IPluginGame::executableForcedLoads)
.def("steamAPPId", &IPluginGame::steamAPPId)
.def("primaryPlugins", &IPluginGame::primaryPlugins)
.def("gameVariants", &IPluginGame::gameVariants)
.def("setGameVariant", &IPluginGame::setGameVariant, "variant"_a)
.def("binaryName", &IPluginGame::binaryName)
.def("gameShortName", &IPluginGame::gameShortName)
.def("primarySources", &IPluginGame::primarySources)
.def("validShortNames", &IPluginGame::validShortNames)
.def("gameNexusName", &IPluginGame::gameNexusName)
.def("iniFiles", &IPluginGame::iniFiles)
.def("DLCPlugins", &IPluginGame::DLCPlugins)
.def("CCPlugins", &IPluginGame::CCPlugins)
.def("loadOrderMechanism", &IPluginGame::loadOrderMechanism)
.def("sortMechanism", &IPluginGame::sortMechanism)
.def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID)
.def("nexusGameID", &IPluginGame::nexusGameID)
.def("looksValid", &IPluginGame::looksValid, "directory"_a)
.def("gameVersion", &IPluginGame::gameVersion)
.def("getLauncherName", &IPluginGame::getLauncherName)
.def("getSupportURL", &IPluginGame::getSupportURL);
}
// multiple installers
void add_iplugininstaller_bindings(pybind11::module_ m)
{
py::enum_<IPluginInstaller::EInstallResult>(m, "InstallResult")
.value("SUCCESS", IPluginInstaller::RESULT_SUCCESS)
.value("FAILED", IPluginInstaller::RESULT_FAILED)
.value("CANCELED", IPluginInstaller::RESULT_CANCELED)
.value("MANUAL_REQUESTED", IPluginInstaller::RESULT_MANUALREQUESTED)
.value("NOT_ATTEMPTED", IPluginInstaller::RESULT_NOTATTEMPTED);
// this is bind but should not be inherited in Python - does not make sense,
// having it makes it simpler to bind the Simple and Custom installers
py::class_<IPluginInstaller, PyPluginInstallerBase<IPluginInstaller>, IPlugin,
std::unique_ptr<IPluginInstaller, py::nodelete>>(
m, "IPluginInstaller", py::multiple_inheritance())
.def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, "tree"_a)
.def("priority", &IPluginInstaller::priority)
.def("onInstallationStart", &IPluginInstaller::onInstallationStart,
"archive"_a, "reinstallation"_a, "current_mod"_a)
.def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, "result"_a,
"new_mod"_a)
.def("isManualInstaller", &IPluginInstaller::isManualInstaller)
.def("setParentWidget", &IPluginInstaller::setParentWidget, "parent"_a)
.def("setInstallationManager", &IPluginInstaller::setInstallationManager,
"manager"_a)
.def("_parentWidget",
&PyPluginInstallerBase<IPluginInstaller>::parentWidget)
.def("_manager", &PyPluginInstallerBase<IPluginInstaller>::manager,
py::return_value_policy::reference);
py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller,
std::unique_ptr<IPluginInstallerSimple, py::nodelete>>(
m, "IPluginInstallerSimple", py::multiple_inheritance())
.def(py::init<>())
// note: keeping the variant here even if we always return a tuple
// to be consistent with the wrapper and have proper stubs generation.
.def(
"install",
[](IPluginInstallerSimple* p, GuessedValue<QString>& modName,
std::shared_ptr<IFileTree>& tree, QString& version,
int& nexusID) -> PyPluginInstallerSimple::py_install_return_type {
auto result = p->install(modName, tree, version, nexusID);
return std::make_tuple(result, tree, version, nexusID);
},
"name"_a, "tree"_a, "version"_a, "nexus_id"_a);
py::class_<IPluginInstallerCustom, PyPluginInstallerCustom, IPluginInstaller,
std::unique_ptr<IPluginInstallerCustom, py::nodelete>>(
m, "IPluginInstallerCustom", py::multiple_inheritance())
.def(py::init<>())
.def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported,
"archive_name"_a)
.def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions)
.def("install", &IPluginInstallerCustom::install, "mod_name"_a,
"game_name"_a, "archive_name"_a, "version"_a, "nexus_id"_a);
}
void add_plugins_bindings(pybind11::module_ m)
{
py::class_<IPlugin, PyPlugin, std::unique_ptr<IPlugin, py::nodelete>>(
m, "IPluginBase", py::multiple_inheritance())
.def(py::init<>())
.def("init", &IPlugin::init, "organizer"_a)
.def("name", &IPlugin::name)
.def("localizedName", &IPlugin::localizedName)
.def("master", &IPlugin::master)
.def("author", &IPlugin::author)
.def("description", &IPlugin::description)
.def("version", &IPlugin::version)
.def("requirements", &IPlugin::requirements)
.def("settings", &IPlugin::settings)
.def("enabledByDefault", &IPlugin::enabledByDefault);
py::class_<IPyPlugin, PyPlugin, IPlugin,
std::unique_ptr<IPyPlugin, py::nodelete>>(m, "IPlugin",
py::multiple_inheritance())
.def(py::init<>());
py::class_<IPyPluginFileMapper, PyPluginFileMapper, IPlugin,
std::unique_ptr<IPyPluginFileMapper, py::nodelete>>(
m, "IPluginFileMapper", py::multiple_inheritance())
.def(py::init<>())
.def("mappings", &IPluginFileMapper::mappings);
py::class_<IPyPluginDiagnose, PyPluginDiagnose, IPlugin,
std::unique_ptr<IPyPluginDiagnose, py::nodelete>>(
m, "IPluginDiagnose", py::multiple_inheritance())
.def(py::init<>())
.def("activeProblems", &IPluginDiagnose::activeProblems)
.def("shortDescription", &IPluginDiagnose::shortDescription, "key"_a)
.def("fullDescription", &IPluginDiagnose::fullDescription, "key"_a)
.def("hasGuidedFix", &IPluginDiagnose::hasGuidedFix, "key"_a)
.def("startGuidedFix", &IPluginDiagnose::startGuidedFix, "key"_a)
.def("_invalidate", &PyPluginDiagnose::invalidate);
py::class_<IPluginTool, PyPluginTool, IPlugin,
std::unique_ptr<IPluginTool, py::nodelete>>(
m, "IPluginTool", py::multiple_inheritance())
.def(py::init<>())
.def("displayName", &IPluginTool::displayName)
.def("tooltip", &IPluginTool::tooltip)
.def("icon", &IPluginTool::icon)
.def("display", &IPluginTool::display)
.def("setParentWidget", &IPluginTool::setParentWidget)
.def("_parentWidget", &PyPluginTool::parentWidget);
py::class_<IPluginPreview, PyPluginPreview, IPlugin,
std::unique_ptr<IPluginPreview, py::nodelete>>(
m, "IPluginPreview", py::multiple_inheritance())
.def(py::init<>())
.def("supportedExtensions", &IPluginPreview::supportedExtensions)
.def("genFilePreview", &IPluginPreview::genFilePreview, "filename"_a,
"max_size"_a);
py::class_<IPluginModPage, PyPluginModPage, IPluginModPage,
std::unique_ptr<IPluginModPage, py::nodelete>>(
m, "IPluginModPage", py::multiple_inheritance())
.def(py::init<>())
.def("displayName", &IPluginModPage::displayName)
.def("icon", &IPluginModPage::icon)
.def("pageURL", &IPluginModPage::pageURL)
.def("useIntegratedBrowser", &IPluginModPage::useIntegratedBrowser)
.def("handlesDownload", &IPluginModPage::handlesDownload, "page_url"_a,
"download_url"_a, "fileinfo"_a)
.def("setParentWidget", &IPluginModPage::setParentWidget, "parent"_a)
.def("_parentWidget", &PyPluginModPage::parentWidget);
add_iplugingame_bindings(m);
add_iplugininstaller_bindings(m);
}
struct extract_plugins_helper {
QList<QObject*> objects;
template <class IPluginClass>
void append_if_instance(pybind11::object plugin_obj)
{
if (py::isinstance<IPluginClass>(plugin_obj)) {
objects.append(plugin_obj.cast<IPluginClass*>());
}
}
};
QList<QObject*> extract_plugins(pybind11::object plugin_obj)
{
extract_plugins_helper helper;
// we need to check the trampoline class for these since the interfaces do not
// extend IPlugin
helper.append_if_instance<IPyPluginFileMapper>(plugin_obj);
helper.append_if_instance<IPyPluginDiagnose>(plugin_obj);
helper.append_if_instance<IPluginModPage>(plugin_obj);
helper.append_if_instance<IPluginPreview>(plugin_obj);
helper.append_if_instance<IPluginTool>(plugin_obj);
helper.append_if_instance<IPluginGame>(plugin_obj);
// we need to check the two installer types because IPluginInstaller does not
// inherit QObject, and the trampoline do not have a common ancestor
helper.append_if_instance<IPluginInstallerSimple>(plugin_obj);
helper.append_if_instance<IPluginInstallerCustom>(plugin_obj);
if (helper.objects.isEmpty()) {
helper.append_if_instance<IPyPlugin>(plugin_obj);
}
// tie the lifetime of the Python object to the lifetime of the QObject
for (auto* object : helper.objects) {
py::qt::set_qt_owner(object, plugin_obj);
}
return helper.objects;
}
} // namespace mo2::python
+493
View File
@@ -0,0 +1,493 @@
#ifndef PYTHON_WRAPPERS_PYPLUGINS_H
#define PYTHON_WRAPPERS_PYPLUGINS_H
#include "../pybind11_all.h"
#include <iinstallationmanager.h>
#include <iplugin.h>
#include <iplugindiagnose.h>
#include <ipluginfilemapper.h>
#include <iplugingame.h>
#include <iplugininstaller.h>
#include <iplugininstallercustom.h>
#include <iplugininstallersimple.h>
#include <ipluginmodpage.h>
#include <ipluginpreview.h>
#include <iplugintool.h>
// these needs to be defined in a header file for automoc - this file is included only
// in pyplugins.cpp
namespace mo2::python {
using namespace MOBase;
// we need two base trampoline because IPluginGame has some final methods.
template <class PluginBase>
class PyPluginBaseNoFinal : public PluginBase {
public:
using PluginBase::PluginBase;
~PyPluginBaseNoFinal()
{
std::cout << "~PyPluginBaseNoFinal() " << typeid(this).name() << std::endl;
}
bool init(IOrganizer* organizer) override
{
PYBIND11_OVERRIDE_PURE(bool, PluginBase, init, organizer);
}
QString name() const override
{
PYBIND11_OVERRIDE_PURE(QString, PluginBase, name, );
}
QString localizedName() const override
{
PYBIND11_OVERRIDE(QString, PluginBase, localizedName, );
}
QString master() const override
{
PYBIND11_OVERRIDE(QString, PluginBase, master, );
}
QString author() const override
{
PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, );
}
QString description() const override
{
PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, );
}
VersionInfo version() const override
{
PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, );
}
QList<PluginSetting> settings() const override
{
PYBIND11_OVERRIDE_PURE(QList<PluginSetting>, PluginBase, settings, );
}
};
template <class PluginBase>
class PyPluginBase : public PyPluginBaseNoFinal<PluginBase> {
public:
using PyPluginBaseNoFinal<PluginBase>::PyPluginBaseNoFinal;
std::vector<std::shared_ptr<const IPluginRequirement>> requirements() const
{
PYBIND11_OVERRIDE(std::vector<std::shared_ptr<const IPluginRequirement>>,
PluginBase, requirements, );
}
bool enabledByDefault() const override
{
PYBIND11_OVERRIDE(bool, PluginBase, enabledByDefault, );
}
};
// these classes do not inherit IPlugin or QObject so we need intermediate class to
// get proper bindings
class IPyPlugin : public QObject, public IPlugin {};
class IPyPluginFileMapper : public IPyPlugin, public IPluginFileMapper {};
class IPyPluginDiagnose : public IPyPlugin, public IPluginDiagnose {};
// PyXXX classes - trampoline classes for the plugins
class PyPlugin : public PyPluginBase<IPyPlugin> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin)
};
class PyPluginFileMapper : public PyPluginBase<IPyPluginFileMapper> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper)
public:
MappingType mappings() const override
{
PYBIND11_OVERRIDE_PURE(MappingType, IPluginFileMapper, mappings, );
}
};
class PyPluginDiagnose : public PyPluginBase<IPyPluginDiagnose> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose)
public:
std::vector<unsigned int> activeProblems() const
{
PYBIND11_OVERRIDE_PURE(std::vector<unsigned int>, IPluginDiagnose,
activeProblems, );
}
QString shortDescription(unsigned int key) const
{
PYBIND11_OVERRIDE_PURE(QString, IPluginDiagnose, shortDescription, key);
}
QString fullDescription(unsigned int key) const
{
PYBIND11_OVERRIDE_PURE(QString, IPluginDiagnose, fullDescription, key);
}
bool hasGuidedFix(unsigned int key) const
{
PYBIND11_OVERRIDE_PURE(bool, IPluginDiagnose, hasGuidedFix, key);
}
void startGuidedFix(unsigned int key) const
{
PYBIND11_OVERRIDE_PURE(void, IPluginDiagnose, startGuidedFix, key);
}
// we need to bring this in public scope
using IPluginDiagnose::invalidate;
};
class PyPluginTool : public PyPluginBase<IPluginTool> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool)
public:
QString displayName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginTool, displayName, );
}
QString tooltip() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginTool, tooltip, );
}
QIcon icon() const override
{
PYBIND11_OVERRIDE_PURE(QIcon, IPluginTool, icon, );
}
void setParentWidget(QWidget* widget) override
{
PYBIND11_OVERRIDE(void, IPluginTool, setParentWidget, widget);
}
void display() const override
{
PYBIND11_OVERRIDE_PURE(void, IPluginTool, display, );
}
// we need to bring this in public scope
using IPluginTool::parentWidget;
};
class PyPluginPreview : public PyPluginBase<IPluginPreview> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview)
public:
std::set<QString> supportedExtensions() const override
{
PYBIND11_OVERRIDE_PURE(std::set<QString>, IPluginPreview,
supportedExtensions, );
}
QWidget* genFilePreview(const QString& fileName,
const QSize& maxSize) const override
{
PYBIND11_OVERRIDE_PURE(QWidget*, IPluginPreview, genFilePreview, fileName,
maxSize);
}
};
class PyPluginModPage : public PyPluginBase<IPluginModPage> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage)
public:
QString displayName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginModPage, displayName, );
}
QIcon icon() const override
{
PYBIND11_OVERRIDE_PURE(QIcon, IPluginModPage, icon, );
}
QUrl pageURL() const override
{
PYBIND11_OVERRIDE_PURE(QUrl, IPluginModPage, pageURL, );
}
bool useIntegratedBrowser() const override
{
PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, useIntegratedBrowser, );
}
bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL,
ModRepositoryFileInfo& fileInfo) const override
{
// TODO: cannot modify fileInfo from Python
PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, handlesDownload, pageURL,
downloadURL, &fileInfo);
}
void setParentWidget(QWidget* widget) override
{
PYBIND11_OVERRIDE(void, IPluginModPage, setParentWidget, widget);
}
// we need to bring this in public scope
using IPluginModPage::parentWidget;
};
// installers
template <class PluginInstallerBase>
class PyPluginInstallerBase : public PyPluginBase<PluginInstallerBase> {
public:
using PyPluginBase<PluginInstallerBase>::PyPluginBase;
unsigned int priority() const override
{
PYBIND11_OVERRIDE_PURE(unsigned int, PluginInstallerBase, priority);
}
bool isManualInstaller() const override
{
PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isManualInstaller, );
}
void onInstallationStart(QString const& archive, bool reinstallation,
IModInterface* currentMod)
{
PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationStart, archive,
reinstallation, currentMod);
}
void onInstallationEnd(IPluginInstaller::EInstallResult result,
IModInterface* newMod)
{
PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationEnd, result,
newMod);
}
bool isArchiveSupported(std::shared_ptr<const IFileTree> tree) const override
{
PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isArchiveSupported, tree);
}
// we need to bring these in public scope
using PluginInstallerBase::manager;
using PluginInstallerBase::parentWidget;
};
class PyPluginInstallerCustom
: public PyPluginInstallerBase<IPluginInstallerCustom> {
Q_OBJECT
Q_INTERFACES(
MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom)
public:
bool isArchiveSupported(const QString& archiveName) const
{
PYBIND11_OVERRIDE_PURE(bool, IPluginInstallerCustom, isArchiveSupported,
archiveName);
}
std::set<QString> supportedExtensions() const
{
PYBIND11_OVERRIDE_PURE(std::set<QString>, IPluginInstallerCustom,
supportedExtensions, );
}
EInstallResult install(GuessedValue<QString>& modName, QString gameName,
const QString& archiveName, const QString& version,
int nexusID) override
{
PYBIND11_OVERRIDE_PURE(EInstallResult, IPluginInstallerCustom, install,
&modName, gameName, archiveName, version, nexusID);
}
};
class PyPluginInstallerSimple
: public PyPluginInstallerBase<IPluginInstallerSimple> {
Q_OBJECT
Q_INTERFACES(
MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple)
public:
using py_install_return_type =
std::variant<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>,
std::tuple<IPluginInstaller::EInstallResult,
std::shared_ptr<IFileTree>, QString, int>>;
EInstallResult install(GuessedValue<QString>& modName,
std::shared_ptr<IFileTree>& tree, QString& version,
int& nexusID) override
{
const auto result = [&, this]() {
PYBIND11_OVERRIDE_PURE(py_install_return_type, IPluginInstallerSimple,
install, &modName, tree, version, nexusID);
}();
return std::visit(
[&tree, &version, &nexusID](auto const& t) {
using type = std::decay_t<decltype(t)>;
if constexpr (std::is_same_v<type, EInstallResult>) {
return t;
}
else if constexpr (std::is_same_v<type,
std::shared_ptr<IFileTree>>) {
tree = t;
return RESULT_SUCCESS;
}
else if constexpr (std::is_same_v<
type, std::tuple<EInstallResult,
std::shared_ptr<IFileTree>,
QString, int>>) {
tree = std::get<1>(t);
version = std::get<2>(t);
nexusID = std::get<3>(t);
return std::get<0>(t);
}
},
result);
}
};
// game
class PyPluginGame : public PyPluginBaseNoFinal<IPluginGame> {
Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame)
public:
void detectGame() override
{
PYBIND11_OVERRIDE_PURE(void, IPluginGame, detectGame, );
}
QString gameName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameName, );
}
void initializeProfile(const QDir& directory,
ProfileSettings settings) const override
{
PYBIND11_OVERRIDE_PURE(void, IPluginGame, initializeProfile, directory,
settings);
}
std::vector<std::shared_ptr<const ISaveGame>>
listSaves(QDir folder) const override
{
PYBIND11_OVERRIDE_PURE(std::vector<std::shared_ptr<const ISaveGame>>,
IPluginGame, listSaves, folder);
}
bool isInstalled() const override
{
PYBIND11_OVERRIDE_PURE(bool, IPluginGame, isInstalled, );
}
QIcon gameIcon() const override
{
PYBIND11_OVERRIDE_PURE(QIcon, IPluginGame, gameIcon, );
}
QDir gameDirectory() const override
{
PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, gameDirectory, );
}
QDir dataDirectory() const override
{
PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, );
}
void setGamePath(const QString& path) override
{
PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGamePath, path);
}
QDir documentsDirectory() const override
{
PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, documentsDirectory, );
}
QDir savesDirectory() const override
{
PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, savesDirectory, );
}
QList<ExecutableInfo> executables() const override
{
PYBIND11_OVERRIDE_PURE(QList<ExecutableInfo>, IPluginGame, executables, );
}
QList<ExecutableForcedLoadSetting> executableForcedLoads() const override
{
PYBIND11_OVERRIDE_PURE(QList<ExecutableForcedLoadSetting>, IPluginGame,
executableForcedLoads, );
}
QString steamAPPId() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, steamAPPId, );
}
QStringList primaryPlugins() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, primaryPlugins, );
}
QStringList gameVariants() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, gameVariants, );
}
void setGameVariant(const QString& variant) override
{
PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGameVariant, variant);
}
QString binaryName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, binaryName, );
}
QString gameShortName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameShortName, );
}
QStringList primarySources() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, primarySources, );
}
QStringList validShortNames() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, validShortNames, );
}
QString gameNexusName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameNexusName, );
}
QStringList iniFiles() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, iniFiles, );
}
QStringList DLCPlugins() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, DLCPlugins, );
}
QStringList CCPlugins() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, CCPlugins, );
}
LoadOrderMechanism loadOrderMechanism() const override
{
PYBIND11_OVERRIDE_PURE(LoadOrderMechanism, IPluginGame,
loadOrderMechanism, );
}
SortMechanism sortMechanism() const override
{
PYBIND11_OVERRIDE_PURE(SortMechanism, IPluginGame, sortMechanism, );
}
int nexusModOrganizerID() const override
{
PYBIND11_OVERRIDE_PURE(int, IPluginGame, nexusModOrganizerID, );
}
int nexusGameID() const override
{
PYBIND11_OVERRIDE_PURE(int, IPluginGame, nexusGameID, );
}
bool looksValid(QDir const& dir) const override
{
PYBIND11_OVERRIDE_PURE(bool, IPluginGame, looksValid, dir);
}
QString gameVersion() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameVersion, );
}
QString getLauncherName() const override
{
PYBIND11_OVERRIDE_PURE(QString, IPluginGame, getLauncherName, );
}
QString getSupportURL() const override
{
PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, );
}
protected:
std::map<std::type_index, std::any> featureList() const override;
};
} // namespace mo2::python
#endif
+74
View File
@@ -0,0 +1,74 @@
#include "wrappers.h"
#include "../pybind11_all.h"
#include <report.h>
namespace py = pybind11;
using namespace MOBase;
namespace mo2::python {
void add_widget_bindings(pybind11::module_ m)
{
// TaskDialog is also in Windows System.
using TaskDialog = MOBase::TaskDialog;
// TaskDialog
py::class_<TaskDialogButton>(m, "TaskDialogButton")
.def(py::init<QString, QString, QMessageBox::StandardButton>(),
py::arg("text"), py::arg("description"), py::arg("button"))
.def(py::init<QString, QMessageBox::StandardButton>(), py::arg("text"),
py::arg("button"));
py::class_<TaskDialog>(m, "TaskDialog")
.def(py::init([](QWidget* parent, QString const& title, QString const& main,
QString const& content, QString const& details,
QMessageBox::Icon icon,
std::vector<TaskDialogButton> const& buttons,
std::variant<QString, std::tuple<QString, QString>> const&
remember) {
auto* dialog = new TaskDialog(parent, title);
dialog->main(main).content(content).details(details).icon(icon);
for (auto& button : buttons) {
dialog->button(button);
}
std::visit(
[dialog](auto const& item) {
QString action, file;
if constexpr (std::is_same_v<std::decay_t<decltype(item)>,
QString>) {
action = item;
}
else {
action = std::get<0>(item);
file = std::get<1>(item);
}
dialog->remember(action, file);
},
remember);
return dialog;
}),
py::return_value_policy::take_ownership,
py::arg("parent") = static_cast<QWidget*>(nullptr),
py::arg("title") = "", py::arg("main") = "", py::arg("content") = "",
py::arg("details") = "", py::arg("icon") = QMessageBox::NoIcon,
py::arg("buttons") = std::vector<TaskDialogButton>{},
py::arg("remember") = "")
.def("setTitle", &TaskDialog::title, py::arg("title"))
.def("setMain", &TaskDialog::main, py::arg("main"))
.def("setContent", &TaskDialog::content, py::arg("content"))
.def("setDetails", &TaskDialog::details, py::arg("details"))
.def("setIcon", &TaskDialog::icon, py::arg("icon"))
.def("addButton", &TaskDialog::button, py::arg("button"))
.def("setRemember", &TaskDialog::remember, py::arg("action"),
py::arg("file") = "")
.def("setWidth", &TaskDialog::setWidth, py::arg("widget"))
.def("addContent", &TaskDialog::addContent, py::arg("widget"))
.def("exec", &TaskDialog::exec);
}
} // namespace mo2::python
+116
View File
@@ -0,0 +1,116 @@
#include "wrappers.h"
#include "../pybind11_all.h"
#include <QDir>
#include <QIcon>
#include <QString>
#include <QUrl>
// IOrganizer must be bring here to properly compile the Python bindings of
// plugin requirements
#include <imoinfo.h>
#include <isavegame.h>
#include <isavegameinfowidget.h>
#include <pluginrequirements.h>
namespace py = pybind11;
using namespace MOBase;
namespace mo2::python {
class PyPluginRequirement : public IPluginRequirement {
public:
std::optional<Problem> check(IOrganizer* organizer) const override
{
PYBIND11_OVERRIDE_PURE(std::optional<Problem>, IPluginRequirement, check,
organizer);
};
};
class PySaveGame : public ISaveGame {
public:
QString getFilepath() const override
{
PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getFilepath, );
}
QDateTime getCreationTime() const override
{
PYBIND11_OVERRIDE_PURE(QDateTime, ISaveGame, getCreationTime, );
}
QString getName() const override
{
PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getName, );
}
QString getSaveGroupIdentifier() const override
{
PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getSaveGroupIdentifier, );
}
QStringList allFiles() const override
{
PYBIND11_OVERRIDE_PURE(QStringList, ISaveGame, allFiles, );
}
~PySaveGame() { std::cout << "~PySaveGame()" << std::endl; }
};
class PySaveGameInfoWidget : public ISaveGameInfoWidget {
public:
// Bring the constructor:
using ISaveGameInfoWidget::ISaveGameInfoWidget;
void setSave(ISaveGame const& save) override
{
PYBIND11_OVERRIDE_PURE(void, ISaveGameInfoWidget, setSave, &save);
}
~PySaveGameInfoWidget() { std::cout << "~PySaveGameInfoWidget()" << std::endl; }
};
void add_wrapper_bindings(pybind11::module_ m)
{
// ISaveGame - custom type_caster<> for shared_ptr<> to keep the Python object
// alive when returned from Python (see shared_cpp_owner.h)
py::class_<ISaveGame, PySaveGame, std::shared_ptr<ISaveGame>>(m, "ISaveGame")
.def(py::init<>())
.def("getFilepath", &ISaveGame::getFilepath)
.def("getCreationTime", &ISaveGame::getCreationTime)
.def("getName", &ISaveGame::getName)
.def("getSaveGroupIdentifier", &ISaveGame::getSaveGroupIdentifier)
.def("allFiles", &ISaveGame::allFiles);
// ISaveGameInfoWidget - custom holder to keep the Python object alive alongside
// the widget
py::class_<ISaveGameInfoWidget, PySaveGameInfoWidget,
py::qt::qholder<ISaveGameInfoWidget>>
iSaveGameInfoWidget(m, "ISaveGameInfoWidget");
iSaveGameInfoWidget.def(py::init<>())
.def(py::init<QWidget*>(), py::arg("parent"))
.def("setSave", &ISaveGameInfoWidget::setSave, py::arg("save"));
py::qt::add_qt_delegate<QWidget>(iSaveGameInfoWidget, "_widget");
// IPluginRequirement - custom type_caster<> for shared_ptr<> to keep the Python
// object alive when returned from Python (see shared_cpp_owner.h)
py::class_<IPluginRequirement, std::shared_ptr<IPluginRequirement>,
PyPluginRequirement>
iPluginRequirementClass(m, "IPluginRequirement");
py::class_<IPluginRequirement::Problem>(iPluginRequirementClass, "Problem")
.def(py::init<QString, QString>(), py::arg("short_description"),
py::arg("long_description") = "")
.def("shortDescription", &IPluginRequirement::Problem::shortDescription)
.def("longDescription", &IPluginRequirement::Problem::longDescription);
iPluginRequirementClass.def("check", &IPluginRequirement::check,
py::arg("organizer"));
}
} // namespace mo2::python
+105
View File
@@ -0,0 +1,105 @@
#ifndef PYTHON_WRAPPERS_WRAPPERS_H
#define PYTHON_WRAPPERS_WRAPPERS_H
#include <any>
#include <map>
#include <typeindex>
#include <pybind11/pybind11.h>
#include <QList>
#include <QObject>
#include <iplugingame.h>
namespace mo2::python {
/**
* @brief Add bindings for the various classes in uibase that are not
* wrappers (i.e., cannot be extended from Python).
*
* @param m Python module to add bindings to.
*/
void add_basic_bindings(pybind11::module_ m);
/**
* @brief Add bindings for the various custom widget classes in uibase that
* cannot be extended from Python.
*
* @param m Python module to add bindings to.
*/
void add_widget_bindings(pybind11::module_ m);
/**
* @brief Add bindings for the uibase wrappers to the given module. uibase
* wrappers include classes from uibase that can be extended from Python but
* are neither plugins nor game features (e.g., ISaveGame).
*
* @param m Python module to add bindings to.
*/
void add_wrapper_bindings(pybind11::module_ m);
/**
* @brief Add bindings for the various plugin classes in uibase that can be
* extended from Python.
*
* @param m Python module to add bindings to.
*/
void add_plugins_bindings(pybind11::module_ m);
/**
* @brief Extract plugins from the given object. For each plugin implemented, an
* object is returned.
*
* The returned QObject* are set as owner of the given object so that the Python
* object lifetime does not end immediately after returning to C++.
*
* @param object Python object to extract plugins from.
*
* @return a QObject* for each plugin implemented by the given object.
*/
QList<QObject*> extract_plugins(pybind11::object object);
/**
* @brief Add bindings for the various game features classes in uibase that
* can be extended from Python.
*
* @param m Python module to add bindings to.
*/
void add_game_feature_bindings(pybind11::module_ m);
/**
* @brief Create the game feature corresponding to the given Python type from the
* given game.
*
* @param game Game plugin to extract the feature from.
* @param type Type of the feature to extract.
*
* @return the feature from the game, or None is the game as no such feature.
*/
pybind11::object extract_feature(MOBase::IPluginGame const& game,
pybind11::object type);
/**
* @brief Create Python dictionary mapping game feature classes to the game feature
* instances for the given game.
*
* @param game Game plugin to extract features from.
*
* @return a python dictionary mapping feature types (in Python) to feature objects.
*/
pybind11::dict extract_feature_list(MOBase::IPluginGame const& game);
/**
* @brief Convert the given python map of features to a C++ one.
*
* @param py_features Python features to convert (type to feature).
*
* @return the map of features.
*/
std::map<std::type_index, std::any>
convert_feature_list(pybind11::dict const& py_features);
} // namespace mo2::python
#endif // PYTHON_WRAPPERS_WRAPPERS_H
+1 -1
View File
@@ -5,4 +5,4 @@ mo2_configure_plugin(plugin_python
WARNINGS OFF
EXTRA_TRANSLATIONS ${CMAKE_CURRENT_SOURCE_DIR}/../runner)
target_link_libraries(plugin_python PRIVATE pythonrunner)
mo2_install_target(plugin_python)
mo2_install_target(plugin_python FOLDER)

Some files were not shown because too many files have changed in this diff Show More