Compare commits

..
14 Commits
Author SHA1 Message Date
Mikaël Capelle 79f585f483 Add extra classes for QVariant conversion. 2024-08-11 16:56:20 +02:00
Mikaël Capelle e85e69a7a1 Start adding bindings for extensions. 2024-08-11 13:30:07 +02:00
Mikaël Capelle 4c6ed4adc0 Add bindings for exceptions. 2024-08-11 12:53:28 +02:00
Mikaël Capelle 691a9a2f62 Remove unused import in test file. 2024-08-11 11:14:42 +02:00
Mikaël Capelle e155907d21 Fix logging for failed load. 2024-08-11 11:06:12 +02:00
Mikaël Capelle 54e05d31ef Add back IPlugin::requirements. 2024-08-10 15:38:09 +02:00
Mikaël Capelle bf05ac1593 Update following change to PluginSetting in uibase. 2024-08-10 12:23:12 +02:00
Mikaël Capelle e4576f67a6 Add some tests for QByteArray conversion. 2024-08-10 10:16:25 +02:00
Mikaël Capelle a4338db554 Fix after rebasing. 2024-08-09 22:13:43 +02:00
Mikaël Capelle 9b1793c726 Fix issue with unloading single .py file and fix tests. 2024-08-09 22:06:12 +02:00
Mikaël Capelle c658b396bc Properly load plugins. 2024-08-09 22:06:06 +02:00
Mikaël Capelle 8fa0d0ade8 Update following uibase changes for extensions. 2024-08-09 22:05:23 +02:00
Mikaël Capelle 2d151d0f66 Switch VersionInfo -> Version for ModOrganizer2. (#132) 2024-08-09 21:55:34 +02:00
Mikaël Capelle 7cca7018b3 Move to VCPKG.
* Minor cleaning of CMakeLists.txt. Fix C++ warnings and Qt deprecation.
* Avoid using Python from virtual environment.
2024-08-09 21:10:50 +02:00
36 changed files with 444 additions and 759 deletions
+30 -13
View File
@@ -2,37 +2,54 @@ name: Build & Test Plugin Python
on: on:
push: push:
branches: [master] branches: master
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
env: env:
VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
jobs: jobs:
build: build:
runs-on: windows-2022 runs-on: windows-2022
steps: steps:
# https://learn.microsoft.com/en-us/vcpkg/consume/binary-caching-github-actions-cache
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.12"
- name: Configure Plugin Python - name: Install Qt
id: configure-plugin-python uses: jurplel/install-qt-action@v3
uses: ModOrganizer2/build-with-mob-action@master
with: with:
mo2-dependencies: uibase setup-python: false
mo2-skip-build: true version: 6.7.1
modules:
cache: true
- uses: actions/checkout@v4
- name: "Set environmental variables"
shell: bash
run: |
echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV
- name: Configure Plugin Python build
shell: pwsh
run: |
cmake --preset vs2022-windows-standalone `
"-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2019_64" `
-DPLUGIN_PYTHON_TESTING=ON
- name: Build Plugin Python - name: Build Plugin Python
working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }}
run: cmake --build vsbuild --config RelWithDebInfo --verbose ` run: cmake --build vsbuild --config RelWithDebInfo --verbose `
--target python-tests --target runner-tests --target proxy --target python-tests --target runner-tests --target proxy
- name: Test Plugin Python - name: Test Plugin Python
working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }}
run: ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure run: ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure
- name: Install Plugin Python
working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }}
run: cmake --build vsbuild --config RelWithDebInfo --target INSTALL
-20
View File
@@ -1,20 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-case-conflict
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.5
hooks:
- id: clang-format
'types_or': [c++, c]
ci:
autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks."
autofix_prs: true
autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate."
autoupdate_schedule: quarterly
submodules: false
+5 -18
View File
@@ -4,27 +4,14 @@ cmake_policy(SET CMP0144 NEW)
project(plugin_python CXX) project(plugin_python CXX)
# we need mo2-cmake to obtain the Python version, but mo2-cmake will set set(Python_FIND_VIRTUALENV STANDARD)
# CMAKE_MAP_IMPORTED_CONFIG_* which will trigger a tons of CMP0111 warnings for Python
# below so we need to reset these before finding Python and then reset them after
find_package(mo2-cmake CONFIG REQUIRED)
set(_CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL ${CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL})
set(_CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO ${CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO})
set(_CMAKE_MAP_IMPORTED_CONFIG_RELEASE ${CMAKE_MAP_IMPORTED_CONFIG_RELEASE})
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL "")
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO "")
set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE "")
# find Python before include mo2-cmake, otherwise this will trigger a bunch of CMP0111 # find Python before include mo2-cmake, otherwise this will trigger a bunch of CMP0111
# due to the imported configuration mapping variables defined in mo2.cmake # due to the imported configuration mapping variables defined in mo2.cmake
find_package(Python ${MO2_PYTHON_VERSION} EXACT COMPONENTS Interpreter Development REQUIRED) find_package(Python ${MO2_PYTHON_VERSION} COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED) find_package(pybind11 CONFIG REQUIRED)
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL ${_CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL}) find_package(mo2-cmake CONFIG REQUIRED)
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO ${_CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO})
set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE ${_CMAKE_MAP_IMPORTED_CONFIG_RELEASE})
get_filename_component(Python_HOME ${Python_EXECUTABLE} PATH) get_filename_component(Python_HOME ${Python_EXECUTABLE} PATH)
set(Python_DLL_DIR "${Python_HOME}/DLLs") set(Python_DLL_DIR "${Python_HOME}/DLLs")
@@ -39,8 +26,8 @@ set(Python_VERSION_SHORT ${Python_VERSION_MAJOR}${Python_VERSION_MINOR})
add_subdirectory(src) add_subdirectory(src)
# tests (if requested) # tests (if requested)
set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python") set(PLUGIN_PYTHON_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python")
if (BUILD_TESTING) if (PLUGIN_PYTHON_TESTING)
enable_testing() enable_testing()
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
+12 -1
View File
@@ -5,6 +5,9 @@
#include <tuple> #include <tuple>
#include <variant> #include <variant>
#include <uibase/exceptions.h>
#include <uibase/versioning.h>
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include "pybind11_all.h" #include "pybind11_all.h"
@@ -24,6 +27,13 @@ PYBIND11_MODULE(mobase, m)
m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui")); m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui"));
m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets")); m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets"));
// exceptions
//
py::register_exception<Exception>(m, "MO2Exception");
py::register_exception<InvalidNXMLinkException>(m, "InvalidNXMLinkException");
py::register_exception<IncompatibilityException>(m, "IncompatibilityException");
py::register_exception<InvalidVersionException>(m, "InvalidVersionException");
// bindings // bindings
// //
mo2::python::add_basic_bindings(m); mo2::python::add_basic_bindings(m);
@@ -69,7 +79,8 @@ PYBIND11_MODULE(mobase, m)
// //
m.add_object( m.add_object(
"MoVariant", "MoVariant",
py::eval("None | bool | int | str | list[object] | dict[str, object]")); py::eval(
"None | bool | int | str | float | list[object] | dict[str, object]"));
// same thing for GameFeatureType // same thing for GameFeatureType
// //
-1
View File
@@ -13,7 +13,6 @@
#include "pybind11_qt/pybind11_qt.h" #include "pybind11_qt/pybind11_qt.h"
#include "pybind11_utils/functional.h" #include "pybind11_utils/functional.h"
#include "pybind11_utils/generator.h"
#include "pybind11_utils/shared_cpp_owner.h" #include "pybind11_utils/shared_cpp_owner.h"
#include "pybind11_utils/smart_variant_wrapper.h" #include "pybind11_utils/smart_variant_wrapper.h"
+74 -96
View File
@@ -3,19 +3,15 @@
#include "../pybind11_all.h" #include "../pybind11_all.h"
#include <format> #include <format>
#include <memory>
#include <pybind11_utils/generator.h>
#include <uibase/executableinfo.h> #include <uibase/executableinfo.h>
#include <uibase/extensions/extension.h>
#include <uibase/extensions/iextensionlist.h>
#include <uibase/filemapping.h> #include <uibase/filemapping.h>
#include <uibase/game_features/igamefeatures.h> #include <uibase/game_features/igamefeatures.h>
#include <uibase/guessedvalue.h> #include <uibase/guessedvalue.h>
#include <uibase/idownloadmanager.h> #include <uibase/idownloadmanager.h>
#include <uibase/iexecutable.h>
#include <uibase/iexecutableslist.h>
#include <uibase/iinstallationmanager.h> #include <uibase/iinstallationmanager.h>
#include <uibase/iinstance.h>
#include <uibase/iinstancemanager.h>
#include <uibase/imodinterface.h> #include <uibase/imodinterface.h>
#include <uibase/imodrepositorybridge.h> #include <uibase/imodrepositorybridge.h>
#include <uibase/imoinfo.h> #include <uibase/imoinfo.h>
@@ -60,24 +56,7 @@ namespace mo2::python {
.value("NO_METADATA", Version::FormatMode::NoMetadata) .value("NO_METADATA", Version::FormatMode::NoMetadata)
.value("CONDENSED", .value("CONDENSED",
static_cast<Version::FormatMode>(Version::FormatCondensed.toInt())) static_cast<Version::FormatMode>(Version::FormatCondensed.toInt()))
.export_values() .export_values();
.def("__xor__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator^))
.def("__and__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator&))
.def("__or__", py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator|))
.def("__rxor__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator^))
.def("__rand__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator&))
.def("__ror__",
py::overload_cast<Version::FormatMode, Version::FormatModes>(
&operator|));
pyVersion pyVersion
.def_static("parse", &Version::parse, "value"_a, .def_static("parse", &Version::parse, "value"_a,
@@ -109,11 +88,8 @@ namespace mo2::python {
.def_property_readonly("subpatch", &Version::subpatch) .def_property_readonly("subpatch", &Version::subpatch)
.def_property_readonly("prereleases", &Version::preReleases) .def_property_readonly("prereleases", &Version::preReleases)
.def_property_readonly("build_metadata", &Version::buildMetadata) .def_property_readonly("build_metadata", &Version::buildMetadata)
.def("string", &Version::string, "mode"_a = Version::FormatModes{}) .def("string", &Version::string, "mode"_a = Version::FormatCondensed)
.def("__str__", .def("__str__", &Version::string)
[](Version const& version) {
return version.string(Version::FormatCondensed);
})
.def(py::self < py::self) .def(py::self < py::self)
.def(py::self > py::self) .def(py::self > py::self)
.def(py::self <= py::self) .def(py::self <= py::self)
@@ -203,27 +179,6 @@ namespace mo2::python {
.def("forced", &ExecutableForcedLoadSetting::forced) .def("forced", &ExecutableForcedLoadSetting::forced)
.def("library", &ExecutableForcedLoadSetting::library) .def("library", &ExecutableForcedLoadSetting::library)
.def("process", &ExecutableForcedLoadSetting::process); .def("process", &ExecutableForcedLoadSetting::process);
py::class_<IExecutable>(m, "IExecutable")
.def("title", &IExecutable::title)
.def("binaryInfo", &IExecutable::binaryInfo)
.def("arguments", &IExecutable::arguments)
.def("steamAppID", &IExecutable::steamAppID)
.def("workingDirectory", &IExecutable::workingDirectory)
.def("isShownOnToolbar", &IExecutable::isShownOnToolbar)
.def("usesOwnIcon", &IExecutable::usesOwnIcon)
.def("minimizeToSystemTray", &IExecutable::minimizeToSystemTray)
.def("hide", &IExecutable::hide);
py::class_<IExecutablesList>(m, "IExecutablesList")
.def("executables",
[](IExecutablesList* executablesList) {
return make_generator(executablesList->executables(),
py::return_value_policy::reference);
})
.def("getByTitle", &IExecutablesList::getByTitle, "title"_a)
.def("getByBinary", &IExecutablesList::getByBinary, "info"_a)
.def("contains", &IExecutablesList::contains, "title"_a);
} }
void add_modinterface_classes(py::module_ m) void add_modinterface_classes(py::module_ m)
@@ -258,9 +213,6 @@ namespace mo2::python {
.def("url", &IModInterface::url) .def("url", &IModInterface::url)
.def("primaryCategory", &IModInterface::primaryCategory) .def("primaryCategory", &IModInterface::primaryCategory)
.def("categories", &IModInterface::categories) .def("categories", &IModInterface::categories)
.def("author", &IModInterface::author)
.def("uploader", &IModInterface::uploader)
.def("uploaderUrl", &IModInterface::uploaderUrl)
.def("trackedState", &IModInterface::trackedState) .def("trackedState", &IModInterface::trackedState)
.def("endorsedState", &IModInterface::endorsedState) .def("endorsedState", &IModInterface::endorsedState)
.def("fileTree", &IModInterface::fileTree) .def("fileTree", &IModInterface::fileTree)
@@ -328,10 +280,7 @@ namespace mo2::python {
.def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory) .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory)
.def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime) .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime)
.def_readwrite("repository", &ModRepositoryFileInfo::repository) .def_readwrite("repository", &ModRepositoryFileInfo::repository)
.def_readwrite("userData", &ModRepositoryFileInfo::userData) .def_readwrite("userData", &ModRepositoryFileInfo::userData);
.def_readwrite("author", &ModRepositoryFileInfo::author)
.def_readwrite("uploader", &ModRepositoryFileInfo::uploader)
.def_readwrite("uploaderUrl", &ModRepositoryFileInfo::uploaderUrl);
} }
void add_guessedstring_classes(py::module_ m) void add_guessedstring_classes(py::module_ m)
@@ -411,6 +360,38 @@ namespace mo2::python {
py::implicitly_convertible<QString, GuessedValue<QString>>(); py::implicitly_convertible<QString, GuessedValue<QString>>();
} }
void add_iextensionlist_classes(py::module_ m)
{
// TODO: add all bindings here
py::class_<IExtension>(m, "IExtension");
py::class_<IExtensionList>(m, "IExtensionList")
.def("installed", &IExtensionList::installed, "identifier"_a)
.def(
"enabled",
py::overload_cast<const QString&>(&IExtensionList::enabled, py::const_),
"identifier"_a)
.def(
"__getitem__",
[](IExtensionList const& self,
std::variant<std::size_t, QString> const& index) {
return std::visit(
[&self](auto&& value) {
if constexpr (std::is_same_v<std::decay_t<decltype(value)>,
std::size_t>) {
return self.at(value);
}
else {
return self.get(value);
}
},
index);
},
"index"_a, py::return_value_policy::reference)
.def("__len__", &IExtensionList::size);
}
void add_ipluginlist_classes(py::module_ m) void add_ipluginlist_classes(py::module_ m)
{ {
py::enum_<IPluginList::PluginState>(m, "PluginState", py::arithmetic()) py::enum_<IPluginList::PluginState>(m, "PluginState", py::arithmetic())
@@ -436,15 +417,9 @@ namespace mo2::python {
.def("hasMasterExtension", &IPluginList::hasMasterExtension, "name"_a) .def("hasMasterExtension", &IPluginList::hasMasterExtension, "name"_a)
.def("isMediumFlagged", &IPluginList::isMediumFlagged, "name"_a) .def("isMediumFlagged", &IPluginList::isMediumFlagged, "name"_a)
.def("isLightFlagged", &IPluginList::isLightFlagged, "name"_a) .def("isLightFlagged", &IPluginList::isLightFlagged, "name"_a)
.def("isBlueprintFlagged", &IPluginList::isBlueprintFlagged, "name"_a)
.def("hasLightExtension", &IPluginList::hasLightExtension, "name"_a) .def("hasLightExtension", &IPluginList::hasLightExtension, "name"_a)
.def("hasNoRecords", &IPluginList::hasNoRecords, "name"_a) .def("hasNoRecords", &IPluginList::hasNoRecords, "name"_a)
.def("formVersion", &IPluginList::formVersion, "name"_a)
.def("headerVersion", &IPluginList::headerVersion, "name"_a)
.def("author", &IPluginList::author, "name"_a)
.def("description", &IPluginList::description, "name"_a)
// Kept but deprecated for backward compatibility: // Kept but deprecated for backward compatibility:
.def( .def(
"onPluginStateChanged", "onPluginStateChanged",
@@ -563,7 +538,6 @@ namespace mo2::python {
py::class_<IOrganizer>(m, "IOrganizer") py::class_<IOrganizer>(m, "IOrganizer")
.def("createNexusBridge", &IOrganizer::createNexusBridge, .def("createNexusBridge", &IOrganizer::createNexusBridge,
py::return_value_policy::reference) py::return_value_policy::reference)
.def("instanceName", &IOrganizer::instanceName)
.def("profileName", &IOrganizer::profileName) .def("profileName", &IOrganizer::profileName)
.def("profilePath", &IOrganizer::profilePath) .def("profilePath", &IOrganizer::profilePath)
.def("downloadsPath", &IOrganizer::downloadsPath) .def("downloadsPath", &IOrganizer::downloadsPath)
@@ -646,20 +620,16 @@ namespace mo2::python {
.def("virtualFileTree", &IOrganizer::virtualFileTree) .def("virtualFileTree", &IOrganizer::virtualFileTree)
.def("instanceManager", &IOrganizer::instanceManager,
py::return_value_policy::reference)
.def("downloadManager", &IOrganizer::downloadManager, .def("downloadManager", &IOrganizer::downloadManager,
py::return_value_policy::reference) py::return_value_policy::reference)
.def("pluginList", &IOrganizer::pluginList, .def("pluginList", &IOrganizer::pluginList,
py::return_value_policy::reference) py::return_value_policy::reference)
.def("modList", &IOrganizer::modList, py::return_value_policy::reference) .def("extensionList", &IOrganizer::extensionList,
.def("executablesList", &IOrganizer::executablesList,
py::return_value_policy::reference) py::return_value_policy::reference)
.def("modList", &IOrganizer::modList, py::return_value_policy::reference)
.def("gameFeatures", &IOrganizer::gameFeatures, .def("gameFeatures", &IOrganizer::gameFeatures,
py::return_value_policy::reference) py::return_value_policy::reference)
.def("profile", &IOrganizer::profile) .def("profile", &IOrganizer::profile, py::return_value_policy::reference)
.def("profileNames", &IOrganizer::profileNames)
.def("getProfile", &IOrganizer::getProfile, "name"_a)
// custom implementation for startApplication and // custom implementation for startApplication and
// waitForApplication because 1) HANDLE (= void*) is not properly // waitForApplication because 1) HANDLE (= void*) is not properly
@@ -803,29 +773,12 @@ namespace mo2::python {
.def_static("getPluginDataPath", &IOrganizer::getPluginDataPath); .def_static("getPluginDataPath", &IOrganizer::getPluginDataPath);
} }
void add_iinstance_manager_classes(py::module_ m)
{
py::class_<IInstance, std::shared_ptr<IInstance>>(m, "IInstance")
.def("displayName", &IInstance::displayName)
.def("gameName", &IInstance::gameName)
.def("gameDirectory", &IInstance::gameDirectory)
.def("isPortable", &IInstance::isPortable);
py::class_<IInstanceManager>(m, "IInstanceManager")
.def("currentInstance", &IInstanceManager::currentInstance)
.def("globalInstancePaths", &IInstanceManager::globalInstancePaths)
.def("getGlobalInstance", &IInstanceManager::getGlobalInstance);
}
void add_idownload_manager_classes(py::module_ m) void add_idownload_manager_classes(py::module_ m)
{ {
py::class_<IDownloadManager>(m, "IDownloadManager") py::class_<IDownloadManager>(m, "IDownloadManager")
.def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a) .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a)
.def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile,
"mod_id"_a, "file_id"_a) "mod_id"_a, "file_id"_a)
.def("startDownloadNexusFileForGame",
&IDownloadManager::startDownloadNexusFileForGame, "game_name"_a,
"mod_id"_a, "file_id"_a)
.def("downloadPath", &IDownloadManager::downloadPath, "id"_a) .def("downloadPath", &IDownloadManager::downloadPath, "id"_a)
.def("onDownloadComplete", &IDownloadManager::onDownloadComplete, .def("onDownloadComplete", &IDownloadManager::onDownloadComplete,
"callback"_a) "callback"_a)
@@ -877,12 +830,37 @@ namespace mo2::python {
add_modinterface_classes(m); add_modinterface_classes(m);
add_modrepository_classes(m); add_modrepository_classes(m);
py::class_<PluginSetting>(m, "PluginSetting") py::class_<Setting>(m, "Setting")
.def(py::init<const QString&, const QString&, const QVariant&>(), "key"_a, .def(py::init([](const QString& name, const QString& description,
"description"_a, "default_value"_a) const QVariant& defaultValue) {
.def_readwrite("key", &PluginSetting::key) mo2::python::show_deprecation_warning(
.def_readwrite("description", &PluginSetting::description) "Setting(key, description, default)",
.def_readwrite("default_value", &PluginSetting::defaultValue); "Setting(key, description, default) is deprecated, use "
"Setting(name, title, description, default) instead.");
return Setting(name, description, defaultValue);
}),
"key"_a, "description"_a, "default_value"_a)
.def(py::init<const QString&, const QString&, const QString&,
const QVariant&>(),
"name"_a, "title"_a, "description"_a, "default_value"_a)
.def(py::init<const QString&, const QString&, const QString&,
const QString&, const QVariant&>(),
"name"_a, "title"_a, "description"_a, "group"_a, "default_value"_a)
.def_property_readonly("name", &Setting::name)
.def_property_readonly("title", &Setting::title)
.def_property_readonly("description", &Setting::description)
.def_property_readonly("group", &Setting::group)
.def_property_readonly("default_value", &Setting::defaultValue);
// deprecated alias
m.attr("PluginSetting") = m.attr("Setting");
py::class_<SettingGroup>(m, "SettingGroup")
.def(py::init<const QString&, const QString&, const QString&>(), "name"_a,
"title"_a, "description"_a)
.def_property_readonly("name", &SettingGroup::name)
.def_property_readonly("title", &SettingGroup::title)
.def_property_readonly("description", &SettingGroup::description);
py::class_<PluginRequirementFactory>(m, "PluginRequirementFactory") py::class_<PluginRequirementFactory>(m, "PluginRequirementFactory")
// pluginDependency // pluginDependency
@@ -928,7 +906,7 @@ namespace mo2::python {
// must be done BEFORE imodlist because there is a default argument to a // must be done BEFORE imodlist because there is a default argument to a
// IProfile* in the modlist class // IProfile* in the modlist class
py::class_<IProfile, std::shared_ptr<IProfile>>(m, "IProfile") py::class_<IProfile>(m, "IProfile")
.def("name", &IProfile::name) .def("name", &IProfile::name)
.def("absolutePath", &IProfile::absolutePath) .def("absolutePath", &IProfile::absolutePath)
.def("localSavesEnabled", &IProfile::localSavesEnabled) .def("localSavesEnabled", &IProfile::localSavesEnabled)
@@ -941,9 +919,9 @@ namespace mo2::python {
}) })
.def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a); .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a);
add_iextensionlist_classes(m);
add_ipluginlist_classes(m); add_ipluginlist_classes(m);
add_imodlist_classes(m); add_imodlist_classes(m);
add_iinstance_manager_classes(m);
add_idownload_manager_classes(m); add_idownload_manager_classes(m);
add_iinstallation_manager_classes(m); add_iinstallation_manager_classes(m);
add_iorganizer_classes(m); add_iorganizer_classes(m);
+3 -9
View File
@@ -89,15 +89,11 @@ namespace mo2::python {
} }
bool lightPluginsAreSupported() override bool lightPluginsAreSupported() override
{ {
PYBIND11_OVERRIDE(bool, GamePlugins, lightPluginsAreSupported, ); PYBIND11_OVERRIDE_PURE(bool, GamePlugins, lightPluginsAreSupported, );
} }
bool mediumPluginsAreSupported() override bool mediumPluginsAreSupported() override
{ {
PYBIND11_OVERRIDE(bool, GamePlugins, mediumPluginsAreSupported, ); PYBIND11_OVERRIDE_PURE(bool, GamePlugins, mediumPluginsAreSupported, );
}
bool blueprintPluginsAreSupported() override
{
PYBIND11_OVERRIDE(bool, GamePlugins, blueprintPluginsAreSupported, );
} }
}; };
@@ -263,9 +259,7 @@ namespace mo2::python {
.def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a) .def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a)
.def("getLoadOrder", &GamePlugins::getLoadOrder) .def("getLoadOrder", &GamePlugins::getLoadOrder)
.def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported) .def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported)
.def("mediumPluginsAreSupported", &GamePlugins::mediumPluginsAreSupported) .def("mediumPluginsAreSupported", &GamePlugins::mediumPluginsAreSupported);
.def("blueprintPluginsAreSupported",
&GamePlugins::blueprintPluginsAreSupported);
// LocalSavegames // LocalSavegames
+6 -31
View File
@@ -49,7 +49,7 @@ namespace mo2::detail {
return std::make_shared<PyFileTree>(parent, name, m_Callback); return std::make_shared<PyFileTree>(parent, name, m_Callback);
} }
bool doPopulate([[maybe_unused]] std::shared_ptr<const IFileTree> parent, bool doPopulate(std::shared_ptr<const IFileTree> parent,
std::vector<std::shared_ptr<FileTreeEntry>>&) const override std::vector<std::shared_ptr<FileTreeEntry>>&) const override
{ {
return true; return true;
@@ -83,6 +83,7 @@ namespace mo2::python {
void add_ifiletree_bindings(pybind11::module_& m) void add_ifiletree_bindings(pybind11::module_& m)
{ {
// FileTreeEntry class: // FileTreeEntry class:
auto fileTreeEntryClass = auto fileTreeEntryClass =
py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m, py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m,
@@ -163,13 +164,6 @@ namespace mo2::python {
.value("SKIP", IFileTree::WalkReturn::SKIP) .value("SKIP", IFileTree::WalkReturn::SKIP)
.export_values(); .export_values();
// in C++ this is not an inner enum due to the conditional feature of glob(),
// but in Python this makes more sense as a inner enum
py::enum_<GlobPatternType>(iFileTreeClass, "GlobPatternType")
.value("GLOB", GlobPatternType::GLOB)
.value("REGEX", GlobPatternType::REGEX)
.export_values();
// Non-mutable operations: // Non-mutable operations:
iFileTreeClass.def("exists", iFileTreeClass.def("exists",
py::overload_cast<QString, IFileTree::FileTypes>( py::overload_cast<QString, IFileTree::FileTypes>(
@@ -181,29 +175,10 @@ namespace mo2::python {
iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"), iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"),
py::arg("sep") = "\\"); py::arg("sep") = "\\");
iFileTreeClass.def( // Note: walk() would probably be better as a generator in python, but
"walk", // it is likely impossible to construct from the C++ walk() method.
py::overload_cast< iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"),
std::function<IFileTree::WalkReturn( py::arg("sep") = "\\");
QString const&, std::shared_ptr<const FileTreeEntry>)>,
QString>(&IFileTree::walk, py::const_),
py::arg("callback"), py::arg("sep") = "\\");
// the walk() and glob() generator version are free functions in C++ due to the
// conditional nature, but in Python, it makes more sense to have them as method
// of IFileTree directly
iFileTreeClass.def("walk", [](std::shared_ptr<const IFileTree> tree) {
return make_generator(walk(tree));
});
iFileTreeClass.def(
"glob",
[](std::shared_ptr<const IFileTree> tree, QString pattern,
GlobPatternType patternType) {
return make_generator(glob(tree, pattern, patternType));
},
py::arg("pattern"), py::arg("type") = GlobPatternType::GLOB);
// Kind-of-static operations: // Kind-of-static operations:
iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree, iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree,
+4 -10
View File
@@ -55,7 +55,6 @@ namespace mo2::python {
.def("gameIcon", &IPluginGame::gameIcon) .def("gameIcon", &IPluginGame::gameIcon)
.def("gameDirectory", &IPluginGame::gameDirectory) .def("gameDirectory", &IPluginGame::gameDirectory)
.def("dataDirectory", &IPluginGame::dataDirectory) .def("dataDirectory", &IPluginGame::dataDirectory)
.def("modDataDirectory", &IPluginGame::modDataDirectory)
.def("secondaryDataDirectories", &IPluginGame::secondaryDataDirectories) .def("secondaryDataDirectories", &IPluginGame::secondaryDataDirectories)
.def("setGamePath", &IPluginGame::setGamePath, "path"_a) .def("setGamePath", &IPluginGame::setGamePath, "path"_a)
.def("documentsDirectory", &IPluginGame::documentsDirectory) .def("documentsDirectory", &IPluginGame::documentsDirectory)
@@ -76,7 +75,6 @@ namespace mo2::python {
.def("iniFiles", &IPluginGame::iniFiles) .def("iniFiles", &IPluginGame::iniFiles)
.def("DLCPlugins", &IPluginGame::DLCPlugins) .def("DLCPlugins", &IPluginGame::DLCPlugins)
.def("CCPlugins", &IPluginGame::CCPlugins) .def("CCPlugins", &IPluginGame::CCPlugins)
.def("blueprintPrefix", &IPluginGame::blueprintPrefix)
.def("loadOrderMechanism", &IPluginGame::loadOrderMechanism) .def("loadOrderMechanism", &IPluginGame::loadOrderMechanism)
.def("sortMechanism", &IPluginGame::sortMechanism) .def("sortMechanism", &IPluginGame::sortMechanism)
.def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID) .def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID)
@@ -84,8 +82,7 @@ namespace mo2::python {
.def("looksValid", &IPluginGame::looksValid, "directory"_a) .def("looksValid", &IPluginGame::looksValid, "directory"_a)
.def("gameVersion", &IPluginGame::gameVersion) .def("gameVersion", &IPluginGame::gameVersion)
.def("getLauncherName", &IPluginGame::getLauncherName) .def("getLauncherName", &IPluginGame::getLauncherName)
.def("getSupportURL", &IPluginGame::getSupportURL) .def("getSupportURL", &IPluginGame::getSupportURL);
.def("getModMappings", &IPluginGame::getModMappings);
} }
// multiple installers // multiple installers
@@ -112,7 +109,7 @@ namespace mo2::python {
py::return_value_policy::reference); py::return_value_policy::reference);
py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller, py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller,
IPlugin, std::unique_ptr<IPluginInstallerSimple, py::nodelete>>( std::unique_ptr<IPluginInstallerSimple, py::nodelete>>(
m, "IPluginInstallerSimple", py::multiple_inheritance()) m, "IPluginInstallerSimple", py::multiple_inheritance())
.def(py::init<>()) .def(py::init<>())
@@ -129,7 +126,7 @@ namespace mo2::python {
"name"_a, "tree"_a, "version"_a, "nexus_id"_a); "name"_a, "tree"_a, "version"_a, "nexus_id"_a);
py::class_<IPluginInstallerCustom, PyPluginInstallerCustom, IPluginInstaller, py::class_<IPluginInstallerCustom, PyPluginInstallerCustom, IPluginInstaller,
IPlugin, std::unique_ptr<IPluginInstallerCustom, py::nodelete>>( std::unique_ptr<IPluginInstallerCustom, py::nodelete>>(
m, "IPluginInstallerCustom", py::multiple_inheritance()) m, "IPluginInstallerCustom", py::multiple_inheritance())
.def(py::init<>()) .def(py::init<>())
.def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported, .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported,
@@ -147,12 +144,9 @@ namespace mo2::python {
.def("init", &IPlugin::init, "organizer"_a) .def("init", &IPlugin::init, "organizer"_a)
.def("name", &IPlugin::name) .def("name", &IPlugin::name)
.def("localizedName", &IPlugin::localizedName) .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("requirements", &IPlugin::requirements)
.def("settings", &IPlugin::settings) .def("settings", &IPlugin::settings)
.def("settingGroups", &IPlugin::settingGroups)
.def("enabledByDefault", &IPlugin::enabledByDefault); .def("enabledByDefault", &IPlugin::enabledByDefault);
py::class_<IPyPlugin, PyPlugin, IPlugin, py::class_<IPyPlugin, PyPlugin, IPlugin,
+8 -27
View File
@@ -45,25 +45,19 @@ namespace mo2::python {
{ {
PYBIND11_OVERRIDE(QString, PluginBase, localizedName, ); PYBIND11_OVERRIDE(QString, PluginBase, localizedName, );
} }
QString master() const override std::vector<std::shared_ptr<const IPluginRequirement>>
requirements() const override
{ {
PYBIND11_OVERRIDE(QString, PluginBase, master, ); PYBIND11_OVERRIDE(std::vector<std::shared_ptr<const IPluginRequirement>>,
PluginBase, requirements, );
} }
QString author() const override QList<Setting> settings() const override
{ {
PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, ); PYBIND11_OVERRIDE_PURE(QList<Setting>, PluginBase, settings, );
} }
QString description() const override QList<SettingGroup> settingGroups() const override
{ {
PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, ); PYBIND11_OVERRIDE(QList<SettingGroup>, PluginBase, settingGroups, );
}
VersionInfo version() const override
{
PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, );
}
QList<PluginSetting> settings() const override
{
PYBIND11_OVERRIDE_PURE(QList<PluginSetting>, PluginBase, settings, );
} }
}; };
@@ -397,10 +391,6 @@ namespace mo2::python {
{ {
PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, ); PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, );
} }
QString modDataDirectory() const override
{
PYBIND11_OVERRIDE(QString, IPluginGame, modDataDirectory, );
}
QMap<QString, QDir> secondaryDataDirectories() const override QMap<QString, QDir> secondaryDataDirectories() const override
{ {
using string_dir_map = QMap<QString, QDir>; using string_dir_map = QMap<QString, QDir>;
@@ -483,10 +473,6 @@ namespace mo2::python {
{ {
PYBIND11_OVERRIDE(QStringList, IPluginGame, CCPlugins, ); PYBIND11_OVERRIDE(QStringList, IPluginGame, CCPlugins, );
} }
QString blueprintPrefix() const override
{
PYBIND11_OVERRIDE(QString, IPluginGame, blueprintPrefix, );
}
LoadOrderMechanism loadOrderMechanism() const override LoadOrderMechanism loadOrderMechanism() const override
{ {
PYBIND11_OVERRIDE(LoadOrderMechanism, IPluginGame, loadOrderMechanism, ); PYBIND11_OVERRIDE(LoadOrderMechanism, IPluginGame, loadOrderMechanism, );
@@ -519,11 +505,6 @@ namespace mo2::python {
{ {
PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, ); PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, );
} }
QMap<QString, QStringList> getModMappings() const override
{
using vfs_map = QMap<QString, QStringList>;
PYBIND11_OVERRIDE(vfs_map, IPluginGame, getModMappings, );
}
}; };
} // namespace mo2::python } // namespace mo2::python
+9 -52
View File
@@ -4,73 +4,30 @@
<context> <context>
<name>ProxyPython</name> <name>ProxyPython</name>
<message> <message>
<location filename="proxy/proxypython.cpp" line="88"/> <location filename="proxy/proxypython.cpp" line="170"/>
<source>Python Initialization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="89"/>
<source>On a previous start the Python Plugin failed to initialize.
Do you want to try initializing python again (at the risk of another crash)?
Suggestion: Select &quot;no&quot;, and click the warning sign for further help.Afterwards you have to re-enable the python plugin.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="162"/>
<source>Python Proxy</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="172"/>
<source>Proxy Plugin to allow plugins written in python to be loaded</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="244"/>
<source>ModOrganizer path contains a semicolon</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="246"/>
<source>Python DLL not found</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="248"/>
<source>Invalid Python DLL</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="250"/>
<source>Initializing Python failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="252"/>
<location filename="proxy/proxypython.cpp" line="281"/>
<source>invalid problem key %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="proxy/proxypython.cpp" line="260"/>
<source>The path to Mod Organizer (%1) contains a semicolon.&lt;br&gt;While this is legal on NTFS drives, many applications do not handle it correctly.&lt;br&gt;Unfortunately MO depends on libraries that seem to fall into that group.&lt;br&gt;As a result the python plugin cannot be loaded, and the only solution we can offer is to remove the semicolon or move MO to a path without a semicolon.</source> <source>The path to Mod Organizer (%1) contains a semicolon.&lt;br&gt;While this is legal on NTFS drives, many applications do not handle it correctly.&lt;br&gt;Unfortunately MO depends on libraries that seem to fall into that group.&lt;br&gt;As a result the python plugin cannot be loaded, and the only solution we can offer is to remove the semicolon or move MO to a path without a semicolon.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="proxy/proxypython.cpp" line="270"/> <location filename="proxy/proxypython.cpp" line="180"/>
<source>The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem.</source> <source>The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="proxy/proxypython.cpp" line="273"/> <location filename="proxy/proxypython.cpp" line="183"/>
<source>The Python plugin DLL is invalid, maybe your antivirus is blocking it. Re-installing MO2 and adding exclusions for it to your AV might fix the problem.</source> <source>The Python plugin DLL is invalid, maybe your antivirus is blocking it. Re-installing MO2 and adding exclusions for it to your AV might fix the problem.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="proxy/proxypython.cpp" line="278"/> <location filename="proxy/proxypython.cpp" line="188"/>
<source>The initialization of the Python plugin DLL failed, unfortunately without any details.</source> <source>The initialization of the Python plugin DLL failed, unfortunately without any details.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<location filename="proxy/proxypython.cpp" line="191"/>
<source>no failure</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QObject</name> <name>QObject</name>
+15 -13
View File
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16)
find_package(mo2-uibase CONFIG REQUIRED) find_package(mo2-uibase CONFIG REQUIRED)
set(PLUGIN_NAME "plugin_python") set(PROXY_NAME "python")
add_library(proxy SHARED proxypython.cpp proxypython.h) add_library(proxy SHARED proxypython.cpp proxypython.h)
mo2_configure_plugin(proxy mo2_configure_plugin(proxy
@@ -16,18 +16,23 @@ mo2_configure_plugin(proxy
${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt)
mo2_default_source_group() mo2_default_source_group()
target_link_libraries(proxy PRIVATE runner mo2::uibase) target_link_libraries(proxy PRIVATE runner mo2::uibase)
set_target_properties(proxy PROPERTIES OUTPUT_NAME ${PLUGIN_NAME}) set_target_properties(proxy PROPERTIES OUTPUT_NAME "python_proxy")
mo2_install_plugin(proxy FOLDER)
set(PLUGIN_PYTHON_DIR bin/plugins/${PLUGIN_NAME}) set(PROXY_PYTHON_DIR ${MO2_INSTALL_BIN}/proxies/python)
# install runner # install runner and proxy
install(FILES $<TARGET_FILE:proxy> DESTINATION ${PROXY_PYTHON_DIR})
install(FILES $<TARGET_FILE:runner> DESTINATION ${PROXY_PYTHON_DIR}/dlls)
# install PDB
install(FILES $<TARGET_PDB_FILE:proxy> DESTINATION pdb)
# delay loading since the dll is not in the standard folder
target_link_options(proxy PRIVATE "/DELAYLOAD:runner.dll") target_link_options(proxy PRIVATE "/DELAYLOAD:runner.dll")
install(FILES $<TARGET_FILE:runner> DESTINATION ${PLUGIN_PYTHON_DIR}/dlls)
# translations (custom location) # translations (custom location)
mo2_add_translations(proxy mo2_add_translations(proxy
TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../${PLUGIN_NAME}_en.ts TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../plugin_python_en.ts
SOURCES SOURCES
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../runner ${CMAKE_CURRENT_SOURCE_DIR}/../runner
@@ -35,17 +40,14 @@ mo2_add_translations(proxy
${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt)
# install DLLs files needed # install DLLs files needed
set(DLL_DIRS ${PLUGIN_PYTHON_DIR}/dlls) set(DLL_DIRS ${PROXY_PYTHON_DIR}/dlls)
file(GLOB dlls_to_install file(GLOB dlls_to_install
${Python_HOME}/dlls/libffi*.dll # ${PYTHON_BUILD_PATH}/libffi*.dll
${Python_HOME}/dlls/sqlite*.dll
${Python_HOME}/dlls/libssl*.dll
${Python_HOME}/dlls/libcrypto*.dll
${Python_HOME}/python${Python_VERSION_MAJOR}*.dll) ${Python_HOME}/python${Python_VERSION_MAJOR}*.dll)
install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS}) install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS})
# install Python .pyd files # install Python .pyd files
set(PYLIB_DIR ${PLUGIN_PYTHON_DIR}/libs) set(PYLIB_DIR ${PROXY_PYTHON_DIR}/libs)
file(GLOB libs_to_install ${Python_DLL_DIR}/*.pyd) file(GLOB libs_to_install ${Python_DLL_DIR}/*.pyd)
install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR}) install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR})
+49 -145
View File
@@ -53,58 +53,26 @@ fs::path getPluginFolder()
return fs::path(path).parent_path(); return fs::path(path).parent_path();
} }
ProxyPython::ProxyPython() ProxyPython::ProxyPython() : m_RunnerLib{nullptr}, m_Runner{nullptr} {}
: m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr},
m_LoadFailure(FailureType::NONE)
{
}
bool ProxyPython::init(IOrganizer* moInfo) bool ProxyPython::initialize(QString& errorMessage)
{ {
m_MOInfo = moInfo; errorMessage = "";
if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) {
return false;
}
if (QCoreApplication::applicationDirPath().contains(';')) { if (QCoreApplication::applicationDirPath().contains(';')) {
m_LoadFailure = FailureType::SEMICOLON; errorMessage = failureMessage(FailureType::SEMICOLON);
return true; return true;
} }
const auto pluginFolder = getPluginFolder(); const auto pluginFolder = getPluginFolder();
if (pluginFolder.empty()) { if (pluginFolder.empty()) {
DWORD error = ::GetLastError(); DWORD error = ::GetLastError();
m_LoadFailure = FailureType::DLL_NOT_FOUND; errorMessage = failureMessage(FailureType::DLL_NOT_FOUND);
log::error("failed to resolve Python proxy directory ({}): {}", error, log::error("failed to resolve Python proxy directory ({}): {}", error,
qUtf8Printable(windowsErrorString(::GetLastError()))); qUtf8Printable(windowsErrorString(::GetLastError())));
return false; return false;
} }
if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) {
m_LoadFailure = FailureType::INITIALIZATION;
if (QMessageBox::question(
parentWidget(), tr("Python Initialization failed"),
tr("On a previous start, the Python Plugin failed to initialize.\n"
"Do you want to try initializing Python again (at the risk of "
"another crash)?\n "
"Suggestion: Select \"no\", and click the warning sign for further "
"help. Afterwards you have to re-enable the Python plugin."),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::No) {
// we force enabled here (note: this is a persistent settings since MO2 2.4
// or something), plugin usually should not handle enabled/disabled
// themselves but this is a base plugin so...
m_MOInfo->setPersistent(name(), "enabled", false, true);
return true;
}
}
if (m_MOInfo) {
m_MOInfo->setPersistent(name(), "tryInit", true);
}
// load the pythonrunner library, this is done in multiple steps: // load the pythonrunner library, this is done in multiple steps:
// //
// 1. we set the dlls/ subfolder (from the plugin) as the DLL directory so Windows // 1. we set the dlls/ subfolder (from the plugin) as the DLL directory so Windows
@@ -114,7 +82,7 @@ bool ProxyPython::init(IOrganizer* moInfo)
const auto dllPaths = pluginFolder / "dlls"; const auto dllPaths = pluginFolder / "dlls";
if (SetDllDirectoryW(dllPaths.c_str()) == 0) { if (SetDllDirectoryW(dllPaths.c_str()) == 0) {
DWORD error = ::GetLastError(); DWORD error = ::GetLastError();
m_LoadFailure = FailureType::DLL_NOT_FOUND; errorMessage = failureMessage(FailureType::DLL_NOT_FOUND);
log::error("failed to add python DLL directory ({}): {}", error, log::error("failed to add python DLL directory ({}): {}", error,
qUtf8Printable(windowsErrorString(::GetLastError()))); qUtf8Printable(windowsErrorString(::GetLastError())));
return false; return false;
@@ -128,21 +96,16 @@ bool ProxyPython::init(IOrganizer* moInfo)
if (m_Runner) { if (m_Runner) {
const auto libpath = pluginFolder / "libs"; const auto libpath = pluginFolder / "libs";
const std::vector<fs::path> paths{ const std::vector<fs::path> paths{libpath / "pythoncore.zip", libpath};
libpath / "pythoncore.zip", libpath,
std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}};
m_Runner->initialize(paths); m_Runner->initialize(paths);
} }
if (m_MOInfo) {
m_MOInfo->setPersistent(name(), "tryInit", false);
}
// reset DLL directory // reset DLL directory
SetDllDirectoryW(NULL); SetDllDirectoryW(NULL);
if (!m_Runner || !m_Runner->isInitialized()) { if (!m_Runner || !m_Runner->isInitialized()) {
m_LoadFailure = FailureType::INITIALIZATION; errorMessage = failureMessage(FailureType::INITIALIZATION);
return false;
} }
else { else {
m_Runner->addDllSearchPath(pluginFolder / "dlls"); m_Runner->addDllSearchPath(pluginFolder / "dlls");
@@ -151,110 +114,58 @@ bool ProxyPython::init(IOrganizer* moInfo)
return true; return true;
} }
QString ProxyPython::name() const QList<QList<QObject*>> ProxyPython::load(const PluginExtension& extension)
{
return "Python Proxy";
}
QString ProxyPython::localizedName() const
{
return tr("Python Proxy");
}
QString ProxyPython::author() const
{
return "AnyOldName3, Holt59, Silarn, Tannin";
}
QString ProxyPython::description() const
{
return tr("Proxy Plugin to allow plugins written in python to be loaded");
}
VersionInfo ProxyPython::version() const
{
return VersionInfo(3, 0, 0, VersionInfo::RELEASE_FINAL);
}
QList<PluginSetting> ProxyPython::settings() const
{
return {};
}
QStringList ProxyPython::pluginList(const QDir& pluginPath) const
{
QDir dir(pluginPath);
dir.setFilter(dir.filter() | QDir::NoDotAndDotDot);
QDirIterator iter(dir);
// Note: We put python script (.py) and directory names, not the __init__.py
// files in those since it is easier for the runner to import them.
QStringList result;
while (iter.hasNext()) {
QString name = iter.next();
QFileInfo info = iter.fileInfo();
if (info.isFile() && name.endsWith(".py")) {
result.append(name);
}
else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) {
result.append(name);
}
}
return result;
}
QList<QObject*> ProxyPython::load(const QString& identifier)
{ {
if (!m_Runner) { if (!m_Runner) {
return {}; return {};
} }
return m_Runner->load(identifier);
}
void ProxyPython::unload(const QString& identifier)
{
if (m_Runner) {
return m_Runner->unload(identifier);
}
}
std::vector<unsigned int> ProxyPython::activeProblems() const
{
auto failure = m_LoadFailure;
// don't know how this could happen but wth
if (m_Runner && !m_Runner->isInitialized()) {
failure = FailureType::INITIALIZATION;
}
if (failure != FailureType::NONE) {
return {static_cast<std::underlying_type_t<FailureType>>(failure)};
}
if (extension.autodetect()) {
log::debug("{}: automatic plugin detection is not supported for Python plugins",
extension.metadata().name());
return {}; return {};
} }
QString ProxyPython::shortDescription(unsigned int key) const m_ExtensionModules[&extension] = {};
QList<QList<QObject*>> plugins;
for (auto& [moduleName, modulePath] : extension.plugins()) {
m_ExtensionModules[&extension].push_back({moduleName, modulePath});
plugins.append(m_Runner->load(moduleName, modulePath));
}
return plugins;
}
void ProxyPython::unload(const PluginExtension& extension)
{ {
switch (static_cast<FailureType>(key)) { if (!m_Runner) {
case FailureType::SEMICOLON: return;
return tr("ModOrganizer path contains a semicolon"); }
case FailureType::DLL_NOT_FOUND:
return tr("Python DLL not found"); if (auto it = m_ExtensionModules.find(&extension); it != m_ExtensionModules.end()) {
case FailureType::INVALID_DLL: for (auto& [moduleName, modulePath] : it->second) {
return tr("Invalid Python DLL"); m_Runner->unload(moduleName, modulePath);
case FailureType::INITIALIZATION: }
return tr("Initializing Python failed"); m_ExtensionModules.erase(it);
default:
return tr("invalid problem key %1").arg(key);
} }
} }
QString ProxyPython::fullDescription(unsigned int key) const void ProxyPython::unloadAll()
{ {
switch (static_cast<FailureType>(key)) { if (m_Runner) {
for (auto& [ext, modules] : m_ExtensionModules) {
for (auto& [moduleName, modulePath] : modules) {
m_Runner->unload(moduleName, modulePath);
}
}
}
m_ExtensionModules.clear();
}
QString ProxyPython::failureMessage(FailureType key)
{
switch (key) {
case FailureType::SEMICOLON: case FailureType::SEMICOLON:
return tr("The path to Mod Organizer (%1) contains a semicolon.<br>" return tr("The path to Mod Organizer (%1) contains a semicolon.<br>"
"While this is legal on NTFS drives, many applications do not " "While this is legal on NTFS drives, many applications do not "
@@ -277,13 +188,6 @@ QString ProxyPython::fullDescription(unsigned int key) const
return tr("The initialization of the Python plugin DLL failed, unfortunately " return tr("The initialization of the Python plugin DLL failed, unfortunately "
"without any details."); "without any details.");
default: default:
return tr("invalid problem key %1").arg(key); return tr("no failure");
} }
} }
bool ProxyPython::hasGuidedFix(unsigned int) const
{
return false;
}
void ProxyPython::startGuidedFix(unsigned int) const {}
+17 -28
View File
@@ -23,45 +23,27 @@ along with python proxy plugin. If not, see <http://www.gnu.org/licenses/>.
#include <map> #include <map>
#include <memory> #include <memory>
#include <uibase/extensions/ipluginloader.h>
#include <uibase/iplugindiagnose.h> #include <uibase/iplugindiagnose.h>
#include <uibase/ipluginproxy.h>
#include <Windows.h>
#include <pythonrunner.h> #include <pythonrunner.h>
class ProxyPython : public QObject, class ProxyPython : public MOBase::IPluginLoader {
public MOBase::IPluginProxy,
public MOBase::IPluginDiagnose {
Q_OBJECT Q_OBJECT
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) Q_INTERFACES(MOBase::IPluginLoader)
Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython") Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython")
public: public:
ProxyPython(); ProxyPython();
virtual bool init(MOBase::IOrganizer* moInfo); bool initialize(QString& errorMessage) override;
virtual QString name() const override; QList<QList<QObject*>> load(const MOBase::PluginExtension& extension) override;
virtual QString localizedName() const override; void unload(const MOBase::PluginExtension& extension) override;
virtual QString author() const override; void unloadAll() override;
virtual QString description() const override;
virtual MOBase::VersionInfo version() const override;
virtual QList<MOBase::PluginSetting> settings() const override;
QStringList pluginList(const QDir& pluginPath) const override;
QList<QObject*> load(const QString& identifier) override;
void unload(const QString& identifier) override;
public: // IPluginDiagnose
virtual std::vector<unsigned int> activeProblems() const override;
virtual QString shortDescription(unsigned int key) const override;
virtual QString fullDescription(unsigned int key) const override;
virtual bool hasGuidedFix(unsigned int key) const override;
virtual void startGuidedFix(unsigned int key) const override;
private: private:
MOBase::IOrganizer* m_MOInfo;
HMODULE m_RunnerLib;
std::unique_ptr<mo2::python::IPythonRunner> m_Runner;
enum class FailureType : unsigned int { enum class FailureType : unsigned int {
NONE = 0, NONE = 0,
SEMICOLON = 1, SEMICOLON = 1,
@@ -70,7 +52,14 @@ private:
INITIALIZATION = 4 INITIALIZATION = 4
}; };
FailureType m_LoadFailure; static QString failureMessage(FailureType failureType);
private:
HMODULE m_RunnerLib;
std::unique_ptr<mo2::python::IPythonRunner> m_Runner;
std::unordered_map<const MOBase::PluginExtension*,
std::vector<std::pair<std::string, std::filesystem::path>>>
m_ExtensionModules;
}; };
#endif // PROXYPYTHON_H #endif // PROXYPYTHON_H
+48 -1
View File
@@ -2,12 +2,17 @@
#include <type_traits> #include <type_traits>
#include <QColor>
#include <QIcon>
#include <QPixmap>
#include <pybind11/stl/filesystem.h> #include <pybind11/stl/filesystem.h>
#include "pybind11_qt/details/pybind11_qt_utils.h" #include "pybind11_qt/details/pybind11_qt_utils.h"
// need to import containers to get QVariantList and QVariantMap // need to import containers to get QVariantList and QVariantMap
#include "pybind11_qt/pybind11_qt_containers.h" #include "pybind11_qt/pybind11_qt_containers.h"
#include "pybind11_qt/pybind11_qt_objects.h"
namespace pybind11::detail { namespace pybind11::detail {
@@ -81,7 +86,18 @@ namespace pybind11::detail {
2 * src.length(), nullptr, 0); 2 * src.length(), nullptr, 0);
} }
bool type_caster<QVariant>::load(handle src, bool) template <class T>
bool tryCast(QVariant& value, handle src, bool implicit)
{
type_caster<T> caster;
if (caster.load(src, implicit)) {
value = caster.value;
return true;
}
return false;
}
bool type_caster<QVariant>::load(handle src, bool implicit)
{ {
// test for string first otherwise PyList_Check also works // test for string first otherwise PyList_Check also works
if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) {
@@ -119,6 +135,15 @@ namespace pybind11::detail {
value = src.cast<int>(); value = src.cast<int>();
return true; return true;
} }
else if (PyFloat_Check(src.ptr())) {
value = src.cast<double>();
return true;
}
else if (tryCast<QColor>(value, src, implicit) ||
tryCast<QIcon>(value, src, implicit) ||
tryCast<QPixmap>(value, src, implicit)) {
return true;
}
else { else {
return false; return false;
} }
@@ -131,13 +156,25 @@ namespace pybind11::detail {
case QMetaType::UnknownType: case QMetaType::UnknownType:
return Py_None; return Py_None;
case QMetaType::Int: case QMetaType::Int:
case QMetaType::Long:
return PyLong_FromLong(var.toInt()); return PyLong_FromLong(var.toInt());
case QMetaType::LongLong:
return PyLong_FromLongLong(var.toLongLong());
case QMetaType::UInt: case QMetaType::UInt:
case QMetaType::ULong:
return PyLong_FromUnsignedLong(var.toUInt()); return PyLong_FromUnsignedLong(var.toUInt());
case QMetaType::ULongLong:
return PyLong_FromUnsignedLongLong(var.toULongLong());
case QMetaType::Float:
return PyFloat_FromDouble(var.toFloat());
case QMetaType::Double:
return PyFloat_FromDouble(var.toDouble());
case QMetaType::Bool: case QMetaType::Bool:
return PyBool_FromLong(var.toBool()); return PyBool_FromLong(var.toBool());
case QMetaType::QString: case QMetaType::QString:
return type_caster<QString>::cast(var.toString(), policy, parent); return type_caster<QString>::cast(var.toString(), policy, parent);
// We need to check for StringList here because these are not considered // We need to check for StringList here because these are not considered
// List since List is QList<QVariant> will StringList is QList<QString>: // List since List is QList<QVariant> will StringList is QList<QString>:
case QMetaType::QStringList: case QMetaType::QStringList:
@@ -146,6 +183,16 @@ namespace pybind11::detail {
return type_caster<QVariantList>::cast(var.toList(), policy, parent); return type_caster<QVariantList>::cast(var.toList(), policy, parent);
case QMetaType::QVariantMap: case QMetaType::QVariantMap:
return type_caster<QVariantMap>::cast(var.toMap(), policy, parent); return type_caster<QVariantMap>::cast(var.toMap(), policy, parent);
case QMetaType::QColor:
return type_caster<QColor>::cast(var.value<QColor>(), policy, parent);
case QMetaType::QIcon:
return type_caster<QIcon>::cast(var.value<QIcon>(), policy, parent);
case QMetaType::QPixmap:
return type_caster<QPixmap>::cast(var.value<QPixmap>(), policy, parent);
default: { default: {
PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.userType()); PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.userType());
throw pybind11::error_already_set(); throw pybind11::error_already_set();
-1
View File
@@ -2,7 +2,6 @@ cmake_minimum_required(VERSION 3.16)
add_library(pybind11-utils STATIC add_library(pybind11-utils STATIC
./include/pybind11_utils/functional.h ./include/pybind11_utils/functional.h
./include/pybind11_utils/generator.h
./include/pybind11_utils/shared_cpp_owner.h ./include/pybind11_utils/shared_cpp_owner.h
./include/pybind11_utils/smart_variant_wrapper.h ./include/pybind11_utils/smart_variant_wrapper.h
./include/pybind11_utils/smart_variant.h ./include/pybind11_utils/smart_variant.h
@@ -1,57 +0,0 @@
#ifndef PYTHON_PYBIND11_GENERATOR_H
#define PYTHON_PYBIND11_GENERATOR_H
#include <generator>
#include <pybind11/pybind11.h>
namespace mo2::python {
// the code here is mostly taken from pybind11 itself, and relies on some pybind11
// internals so might be subject to change when upgrading pybind11 versions
namespace detail {
template <typename T>
struct generator_state {
std::generator<T> g;
decltype(g.begin()) it;
generator_state(std::generator<T> gen) : g(std::move(gen)), it(g.begin()) {}
};
} // namespace detail
// create a Python generator from a C++ generator
//
template <typename T, typename... Args>
auto make_generator(std::generator<T> g, Args&&... args)
{
using state = detail::generator_state<T>;
namespace py = pybind11;
if (!py::detail::get_type_info(typeid(state), false)) {
py::class_<state>(py::handle(), "iterator", pybind11::module_local())
.def("__iter__",
[](state& s) -> state& {
return s;
})
.def(
"__next__",
[](state& s) -> T {
if (s.it != s.g.end()) {
T v = *s.it;
s.it++;
return v;
}
else {
throw py::stop_iteration();
}
},
std::forward<Args>(args)...);
}
return py::cast(state{std::move(g)});
}
} // namespace mo2::python
#endif
+2 -1
View File
@@ -21,7 +21,8 @@ target_link_libraries(runner PUBLIC mo2::uibase PRIVATE pybind11::embed pybind11
target_include_directories(runner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(runner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(runner PRIVATE RUNNER_BUILD) target_compile_definitions(runner PRIVATE RUNNER_BUILD)
# proxy will install runner # proxy will install runner but we install the PDB
install(FILES $<TARGET_PDB_FILE:runner> DESTINATION pdb)
# force runner to build mobase # force runner to build mobase
add_dependencies(runner mobase) add_dependencies(runner mobase)
+58 -110
View File
@@ -36,24 +36,14 @@ namespace mo2::python {
PythonRunner() = default; PythonRunner() = default;
~PythonRunner() = default; ~PythonRunner() = default;
QList<QObject*> load(const QString& identifier) override; QList<QList<QObject*>> load(std::string_view moduleName,
void unload(const QString& identifier) override; std::filesystem::path const& modulePath) override;
void unload(std::string_view moduleName,
std::filesystem::path const& modulePath) override;
bool initialize(std::vector<std::filesystem::path> const& pythonPaths) override; bool initialize(std::vector<std::filesystem::path> const& pythonPaths) override;
void addDllSearchPath(std::filesystem::path const& dllPath) override; void addDllSearchPath(std::filesystem::path const& dllPath) override;
bool isInitialized() const override; bool isInitialized() const override;
private:
/**
* @brief Ensure that the given folder is in sys.path.
*/
void ensureFolderInPath(QString folder);
private:
// for each "identifier" (python file or python module folder), contains the
// list of python objects - this does not keep the objects alive, it simply used
// to unload plugins
std::unordered_map<QString, std::vector<py::handle>> m_PythonObjects;
}; };
std::unique_ptr<IPythonRunner> createPythonRunner() std::unique_ptr<IPythonRunner> createPythonRunner()
@@ -162,72 +152,44 @@ namespace mo2::python {
py::module_::import("os").attr("add_dll_directory")(absolute(dllPath)); py::module_::import("os").attr("add_dll_directory")(absolute(dllPath));
} }
void PythonRunner::ensureFolderInPath(QString folder) QList<QList<QObject*>> PythonRunner::load(std::string_view name,
{ const std::filesystem::path& pythonModule)
py::module_ sys = py::module_::import("sys");
py::list sysPath = sys.attr("path");
// Converting to QStringList for Qt::CaseInsensitive and because .index()
// raise an exception:
const QStringList currentPath = sysPath.cast<QStringList>();
if (!currentPath.contains(folder, Qt::CaseInsensitive)) {
sysPath.insert(0, folder);
}
}
QList<QObject*> PythonRunner::load(const QString& identifier)
{ {
py::gil_scoped_acquire lock; py::gil_scoped_acquire lock;
// `pluginName` can either be a python file (single-file plugin or a folder
// (whole module).
//
// For whole module, we simply add the parent folder to path, then we load
// the module with a simple py::import, and we retrieve the associated
// __dict__ from which we extract either createPlugin or createPlugins.
//
// For single file, we need to use py::eval_file, and we will use the
// context (global variables) from __main__ (already contains mobase, and
// other required module). Since the context is shared between called of
// `instantiate`, we need to make sure to remove createPlugin(s) from
// previous call.
try { try {
// dictionary that will contain createPlugin() or createPlugins(). // some needed import
py::dict moduleDict; auto sys = py::module_::import("sys");
auto importlib_util = py::module_::import("importlib.util");
if (identifier.endsWith(".py")) {
py::object mainModule = py::module_::import("__main__");
// make a copy, otherwise we might end up calling the createPlugin() or
// createPlugins() function multiple time
py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")();
std::string temp = ToString(identifier);
py::eval_file(temp, moduleNamespace).is_none();
moduleDict = moduleNamespace;
}
else {
// Retrieve the module name:
QStringList parts = identifier.split("/");
std::string moduleName = ToString(parts.takeLast());
ensureFolderInPath(parts.join("/"));
// check if the module is already loaded // check if the module is already loaded
py::dict modules = py::module_::import("sys").attr("modules"); py::dict modules = sys.attr("modules");
if (modules.contains(moduleName)) { py::module_ pymodule;
py::module_ prev = modules[py::str(moduleName)]; if (modules.contains(name)) {
py::module_(prev).reload(); pymodule = modules[py::str(name)];
moduleDict = prev.attr("__dict__"); pymodule.reload();
} }
else { else {
moduleDict = // load the module
py::module_::import(moduleName.c_str()).attr("__dict__"); auto spec =
} importlib_util.attr("spec_from_file_location")(name, pythonModule);
if (Py_IsNone(spec.ptr())) {
MOBase::log::error("failed to load Python plugin '{}' from '{}'",
name, pythonModule);
return {};
} }
pymodule = importlib_util.attr("module_from_spec")(spec);
sys.attr("modules")[py::str(name)] = pymodule;
spec.attr("loader").attr("exec_module")(pymodule);
}
py::dict moduleDict = pymodule.attr("__dict__");
if (py::len(moduleDict) == 0) { if (py::len(moduleDict) == 0) {
MOBase::log::error("No plugins found in {}.", identifier); MOBase::log::error("no plugins found in {}", pythonModule);
return {}; return {};
} }
@@ -240,8 +202,8 @@ namespace mo2::python {
else if (moduleDict.contains("createPlugins")) { else if (moduleDict.contains("createPlugins")) {
py::object pyPlugins = moduleDict["createPlugins"](); py::object pyPlugins = moduleDict["createPlugins"]();
if (!py::isinstance<py::sequence>(pyPlugins)) { if (!py::isinstance<py::sequence>(pyPlugins)) {
MOBase::log::error( MOBase::log::error("{}: createPlugins must return a sequence",
"Plugin {}: createPlugins must return a sequence.", identifier); pythonModule);
} }
else { else {
py::sequence pyList(pyPlugins); py::sequence pyList(pyPlugins);
@@ -252,30 +214,26 @@ namespace mo2::python {
} }
} }
else { else {
MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", MOBase::log::error("{}: missing createPlugin(s) function",
identifier); pythonModule);
} }
// If we have no plugins, there was an issue, and we already logged the // if we have no plugins, there was an issue, and we already logged the
// problem: // problem
if (plugins.empty()) { if (plugins.empty()) {
return QList<QObject*>(); return {};
} }
QList<QObject*> allInterfaceList; QList<QList<QObject*>> allInterfaceList;
for (py::object pluginObj : plugins) { for (py::object pluginObj : plugins) {
// save to be able to unload it
m_PythonObjects[identifier].push_back(pluginObj);
QList<QObject*> interfaceList = py::module_::import("mobase.private") QList<QObject*> interfaceList = py::module_::import("mobase.private")
.attr("extract_plugins")(pluginObj) .attr("extract_plugins")(pluginObj)
.cast<QList<QObject*>>(); .cast<QList<QObject*>>();
if (interfaceList.isEmpty()) { if (interfaceList.isEmpty()) {
MOBase::log::error("Plugin {}: no plugin interface implemented.", MOBase::log::error("{}: no plugin interface implemented.",
identifier); pythonModule);
} }
// Append the plugins to the main list: // Append the plugins to the main list:
@@ -285,57 +243,47 @@ namespace mo2::python {
return allInterfaceList; return allInterfaceList;
} }
catch (const py::error_already_set& ex) { catch (const py::error_already_set& ex) {
MOBase::log::error("Failed to import plugin from {}.", identifier); MOBase::log::error("failed to import plugin from {}", pythonModule);
throw pyexcept::PythonError(ex); throw pyexcept::PythonError(ex);
} }
} }
void PythonRunner::unload(const QString& identifier) void PythonRunner::unload(std::string_view moduleName,
std::filesystem::path const& modulePath)
{ {
auto it = m_PythonObjects.find(identifier);
if (it != m_PythonObjects.end()) {
py::gil_scoped_acquire lock; py::gil_scoped_acquire lock;
if (!identifier.endsWith(".py")) { // at this point, the identifier is the full path to the module.
QDir folder(modulePath);
// At this point, the identifier is the full path to the module. // we want to "unload" (remove from sys.modules) modules that come
QDir folder(identifier);
// We want to "unload" (remove from sys.modules) modules that come
// from this plugin (whose __path__ points under this module, // from this plugin (whose __path__ points under this module,
// including the module of the plugin itself). // including the module of the plugin itself)
//
py::object sys = py::module_::import("sys"); py::object sys = py::module_::import("sys");
py::dict modules = sys.attr("modules"); py::dict modules = sys.attr("modules");
py::list keys = modules.attr("keys")(); py::list keys = modules.attr("keys")();
for (std::size_t i = 0; i < py::len(keys); ++i) { for (std::size_t i = 0; i < py::len(keys); ++i) {
py::object mod = modules[keys[i]]; py::object mod = modules[keys[i]];
if (PyObject_HasAttrString(mod.ptr(), "__path__")) { if (PyObject_HasAttrString(mod.ptr(), "__path__")) {
QString mpath = QString mpath = mod.attr("__path__")[py::int_(0)].cast<QString>();
mod.attr("__path__")[py::int_(0)].cast<QString>();
if (!folder.relativeFilePath(mpath).startsWith("..")) { if (!folder.relativeFilePath(mpath).startsWith("..")) {
// If the path is under identifier, we need to unload // if the path is under identifier, we need to unload it
// it. log::debug("unloading module {} from {}",
log::debug("Unloading module {} from {} for {}.", keys[i].cast<std::string>(), mpath);
keys[i].cast<std::string>(), mpath, identifier);
PyDict_DelItem(modules.ptr(), keys[i].ptr()); PyDict_DelItem(modules.ptr(), keys[i].ptr());
} }
} }
} }
}
// Boost.Python does not handle cyclic garbace collection, so we need to // for simple Python file - not really used anymore, but actually used in
// release everything hold by the objects before deleting the objects // testing - we need to remove using the module name
// themselves (done when erasing from m_PythonObjects). //
for (auto& obj : it->second) { py::str pyModuleName(moduleName);
obj.attr("__dict__").attr("clear")(); if (modules.contains(pyModuleName)) {
} PyDict_DelItem(modules.ptr(), pyModuleName.ptr());
log::debug("Deleting {} python objects for {}.", it->second.size(),
identifier);
m_PythonObjects.erase(it);
} }
} }
+6 -2
View File
@@ -9,6 +9,8 @@
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <uibase/extensions/extension.h>
#ifdef RUNNER_BUILD #ifdef RUNNER_BUILD
#define RUNNER_DLL_EXPORT Q_DECL_EXPORT #define RUNNER_DLL_EXPORT Q_DECL_EXPORT
#else #else
@@ -21,8 +23,10 @@ namespace mo2::python {
// //
class IPythonRunner { class IPythonRunner {
public: public:
virtual QList<QObject*> load(const QString& identifier) = 0; virtual QList<QList<QObject*>>
virtual void unload(const QString& identifier) = 0; load(std::string_view moduleName, std::filesystem::path const& modulePath) = 0;
virtual void unload(std::string_view moduleName,
std::filesystem::path const& modulePath) = 0;
// initialize Python // initialize Python
// //

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