mirror of
https://github.com/ModOrganizer2/modorganizer-plugin_python.git
synced 2026-07-27 14:03:33 -07:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d55a50a7 | ||
|
|
17840a0d97 | ||
|
|
0b0f34b2da | ||
|
|
1720aeedf8 | ||
|
|
1c8ab3f1c1 | ||
|
|
5003bf75a7 | ||
|
|
b1ecd89c50 | ||
|
|
076244faab | ||
|
|
b81646562c | ||
|
|
648db4db99 | ||
|
|
15d74e5e76 | ||
|
|
4f3a9891fa | ||
|
|
6bed428d04 | ||
|
|
f16e51867a | ||
|
|
c05027b653 | ||
|
|
5726e65504 | ||
|
|
b0d2b48524 | ||
|
|
00a6d5969b | ||
|
|
1010bfda55 | ||
|
|
9a788c9f3f | ||
|
|
84bf90d575 | ||
|
|
8c47cea270 | ||
|
|
5cdf0f6f19 | ||
|
|
4409375fd8 |
@@ -2,54 +2,37 @@ 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: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }}
|
||||||
|
|
||||||
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: Install Qt
|
- name: Configure Plugin Python
|
||||||
uses: jurplel/install-qt-action@v3
|
id: configure-plugin-python
|
||||||
|
uses: ModOrganizer2/build-with-mob-action@master
|
||||||
with:
|
with:
|
||||||
setup-python: false
|
mo2-dependencies: uibase
|
||||||
version: 6.7.1
|
mo2-skip-build: true
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
+18
-5
@@ -4,14 +4,27 @@ cmake_policy(SET CMP0144 NEW)
|
|||||||
|
|
||||||
project(plugin_python CXX)
|
project(plugin_python CXX)
|
||||||
|
|
||||||
set(Python_FIND_VIRTUALENV STANDARD)
|
# we need mo2-cmake to obtain the Python version, but mo2-cmake will set
|
||||||
|
# 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} COMPONENTS Interpreter Development REQUIRED)
|
find_package(Python ${MO2_PYTHON_VERSION} EXACT COMPONENTS Interpreter Development REQUIRED)
|
||||||
find_package(pybind11 CONFIG REQUIRED)
|
find_package(pybind11 CONFIG REQUIRED)
|
||||||
|
|
||||||
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})
|
||||||
|
|
||||||
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")
|
||||||
@@ -26,8 +39,8 @@ set(Python_VERSION_SHORT ${Python_VERSION_MAJOR}${Python_VERSION_MINOR})
|
|||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
# tests (if requested)
|
# tests (if requested)
|
||||||
set(PLUGIN_PYTHON_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python")
|
set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python")
|
||||||
if (PLUGIN_PYTHON_TESTING)
|
if (BUILD_TESTING)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
+1
-12
@@ -5,9 +5,6 @@
|
|||||||
#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"
|
||||||
@@ -27,13 +24,6 @@ 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);
|
||||||
@@ -79,8 +69,7 @@ PYBIND11_MODULE(mobase, m)
|
|||||||
//
|
//
|
||||||
m.add_object(
|
m.add_object(
|
||||||
"MoVariant",
|
"MoVariant",
|
||||||
py::eval(
|
py::eval("None | bool | int | str | list[object] | dict[str, object]"));
|
||||||
"None | bool | int | str | float | list[object] | dict[str, object]"));
|
|
||||||
|
|
||||||
// same thing for GameFeatureType
|
// same thing for GameFeatureType
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#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"
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
#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>
|
||||||
@@ -56,7 +60,24 @@ 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,
|
||||||
@@ -88,8 +109,11 @@ 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::FormatCondensed)
|
.def("string", &Version::string, "mode"_a = Version::FormatModes{})
|
||||||
.def("__str__", &Version::string)
|
.def("__str__",
|
||||||
|
[](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)
|
||||||
@@ -179,6 +203,27 @@ 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)
|
||||||
@@ -213,6 +258,9 @@ 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)
|
||||||
@@ -280,7 +328,10 @@ 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)
|
||||||
@@ -360,38 +411,6 @@ 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())
|
||||||
@@ -417,9 +436,15 @@ 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",
|
||||||
@@ -538,6 +563,7 @@ 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)
|
||||||
@@ -620,16 +646,20 @@ 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("extensionList", &IOrganizer::extensionList,
|
|
||||||
py::return_value_policy::reference)
|
|
||||||
.def("modList", &IOrganizer::modList, py::return_value_policy::reference)
|
.def("modList", &IOrganizer::modList, py::return_value_policy::reference)
|
||||||
|
.def("executablesList", &IOrganizer::executablesList,
|
||||||
|
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, py::return_value_policy::reference)
|
.def("profile", &IOrganizer::profile)
|
||||||
|
.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
|
||||||
@@ -773,12 +803,29 @@ 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)
|
||||||
@@ -830,37 +877,12 @@ namespace mo2::python {
|
|||||||
add_modinterface_classes(m);
|
add_modinterface_classes(m);
|
||||||
add_modrepository_classes(m);
|
add_modrepository_classes(m);
|
||||||
|
|
||||||
py::class_<Setting>(m, "Setting")
|
py::class_<PluginSetting>(m, "PluginSetting")
|
||||||
.def(py::init([](const QString& name, const QString& description,
|
.def(py::init<const QString&, const QString&, const QVariant&>(), "key"_a,
|
||||||
const QVariant& defaultValue) {
|
"description"_a, "default_value"_a)
|
||||||
mo2::python::show_deprecation_warning(
|
.def_readwrite("key", &PluginSetting::key)
|
||||||
"Setting(key, description, default)",
|
.def_readwrite("description", &PluginSetting::description)
|
||||||
"Setting(key, description, default) is deprecated, use "
|
.def_readwrite("default_value", &PluginSetting::defaultValue);
|
||||||
"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
|
||||||
@@ -906,7 +928,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>(m, "IProfile")
|
py::class_<IProfile, std::shared_ptr<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)
|
||||||
@@ -919,9 +941,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);
|
||||||
|
|||||||
@@ -89,11 +89,15 @@ namespace mo2::python {
|
|||||||
}
|
}
|
||||||
bool lightPluginsAreSupported() override
|
bool lightPluginsAreSupported() override
|
||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE_PURE(bool, GamePlugins, lightPluginsAreSupported, );
|
PYBIND11_OVERRIDE(bool, GamePlugins, lightPluginsAreSupported, );
|
||||||
}
|
}
|
||||||
bool mediumPluginsAreSupported() override
|
bool mediumPluginsAreSupported() override
|
||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE_PURE(bool, GamePlugins, mediumPluginsAreSupported, );
|
PYBIND11_OVERRIDE(bool, GamePlugins, mediumPluginsAreSupported, );
|
||||||
|
}
|
||||||
|
bool blueprintPluginsAreSupported() override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE(bool, GamePlugins, blueprintPluginsAreSupported, );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +263,9 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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(std::shared_ptr<const IFileTree> parent,
|
bool doPopulate([[maybe_unused]] 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,7 +83,6 @@ 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,
|
||||||
@@ -164,6 +163,13 @@ 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>(
|
||||||
@@ -175,10 +181,29 @@ 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") = "\\");
|
||||||
|
|
||||||
// Note: walk() would probably be better as a generator in python, but
|
iFileTreeClass.def(
|
||||||
// it is likely impossible to construct from the C++ walk() method.
|
"walk",
|
||||||
iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"),
|
py::overload_cast<
|
||||||
py::arg("sep") = "\\");
|
std::function<IFileTree::WalkReturn(
|
||||||
|
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,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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)
|
||||||
@@ -75,6 +76,7 @@ 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)
|
||||||
@@ -82,7 +84,8 @@ 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
|
||||||
@@ -109,7 +112,7 @@ namespace mo2::python {
|
|||||||
py::return_value_policy::reference);
|
py::return_value_policy::reference);
|
||||||
|
|
||||||
py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller,
|
py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller,
|
||||||
std::unique_ptr<IPluginInstallerSimple, py::nodelete>>(
|
IPlugin, std::unique_ptr<IPluginInstallerSimple, py::nodelete>>(
|
||||||
m, "IPluginInstallerSimple", py::multiple_inheritance())
|
m, "IPluginInstallerSimple", py::multiple_inheritance())
|
||||||
.def(py::init<>())
|
.def(py::init<>())
|
||||||
|
|
||||||
@@ -126,7 +129,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,
|
||||||
std::unique_ptr<IPluginInstallerCustom, py::nodelete>>(
|
IPlugin, 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,
|
||||||
@@ -144,9 +147,12 @@ 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,
|
||||||
|
|||||||
@@ -45,19 +45,25 @@ namespace mo2::python {
|
|||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE(QString, PluginBase, localizedName, );
|
PYBIND11_OVERRIDE(QString, PluginBase, localizedName, );
|
||||||
}
|
}
|
||||||
std::vector<std::shared_ptr<const IPluginRequirement>>
|
QString master() const override
|
||||||
requirements() const override
|
|
||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE(std::vector<std::shared_ptr<const IPluginRequirement>>,
|
PYBIND11_OVERRIDE(QString, PluginBase, master, );
|
||||||
PluginBase, requirements, );
|
|
||||||
}
|
}
|
||||||
QList<Setting> settings() const override
|
QString author() const override
|
||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE_PURE(QList<Setting>, PluginBase, settings, );
|
PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, );
|
||||||
}
|
}
|
||||||
QList<SettingGroup> settingGroups() const override
|
QString description() const override
|
||||||
{
|
{
|
||||||
PYBIND11_OVERRIDE(QList<SettingGroup>, PluginBase, settingGroups, );
|
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, );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,6 +397,10 @@ 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>;
|
||||||
@@ -473,6 +483,10 @@ 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, );
|
||||||
@@ -505,6 +519,11 @@ 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
|
||||||
|
|||||||
+52
-9
@@ -4,30 +4,73 @@
|
|||||||
<context>
|
<context>
|
||||||
<name>ProxyPython</name>
|
<name>ProxyPython</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="proxy/proxypython.cpp" line="170"/>
|
<location filename="proxy/proxypython.cpp" line="88"/>
|
||||||
|
<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 "no", 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.<br>While this is legal on NTFS drives, many applications do not handle it correctly.<br>Unfortunately MO depends on libraries that seem to fall into that group.<br>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.<br>While this is legal on NTFS drives, many applications do not handle it correctly.<br>Unfortunately MO depends on libraries that seem to fall into that group.<br>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="180"/>
|
<location filename="proxy/proxypython.cpp" line="270"/>
|
||||||
<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="183"/>
|
<location filename="proxy/proxypython.cpp" line="273"/>
|
||||||
<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="188"/>
|
<location filename="proxy/proxypython.cpp" line="278"/>
|
||||||
<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>
|
||||||
|
|||||||
+13
-15
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
|
|
||||||
find_package(mo2-uibase CONFIG REQUIRED)
|
find_package(mo2-uibase CONFIG REQUIRED)
|
||||||
|
|
||||||
set(PROXY_NAME "python")
|
set(PLUGIN_NAME "plugin_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,23 +16,18 @@ 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 "python_proxy")
|
set_target_properties(proxy PROPERTIES OUTPUT_NAME ${PLUGIN_NAME})
|
||||||
|
mo2_install_plugin(proxy FOLDER)
|
||||||
|
|
||||||
set(PROXY_PYTHON_DIR ${MO2_INSTALL_BIN}/proxies/python)
|
set(PLUGIN_PYTHON_DIR bin/plugins/${PLUGIN_NAME})
|
||||||
|
|
||||||
# install runner and proxy
|
# install runner
|
||||||
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_python_en.ts
|
TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../${PLUGIN_NAME}_en.ts
|
||||||
SOURCES
|
SOURCES
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../runner
|
${CMAKE_CURRENT_SOURCE_DIR}/../runner
|
||||||
@@ -40,14 +35,17 @@ 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 ${PROXY_PYTHON_DIR}/dlls)
|
set(DLL_DIRS ${PLUGIN_PYTHON_DIR}/dlls)
|
||||||
file(GLOB dlls_to_install
|
file(GLOB dlls_to_install
|
||||||
# ${PYTHON_BUILD_PATH}/libffi*.dll
|
${Python_HOME}/dlls/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 ${PROXY_PYTHON_DIR}/libs)
|
set(PYLIB_DIR ${PLUGIN_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})
|
||||||
|
|
||||||
|
|||||||
+146
-50
@@ -53,26 +53,58 @@ fs::path getPluginFolder()
|
|||||||
return fs::path(path).parent_path();
|
return fs::path(path).parent_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
ProxyPython::ProxyPython() : m_RunnerLib{nullptr}, m_Runner{nullptr} {}
|
ProxyPython::ProxyPython()
|
||||||
|
: m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr},
|
||||||
bool ProxyPython::initialize(QString& errorMessage)
|
m_LoadFailure(FailureType::NONE)
|
||||||
{
|
{
|
||||||
errorMessage = "";
|
}
|
||||||
|
|
||||||
|
bool ProxyPython::init(IOrganizer* moInfo)
|
||||||
|
{
|
||||||
|
m_MOInfo = moInfo;
|
||||||
|
|
||||||
|
if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (QCoreApplication::applicationDirPath().contains(';')) {
|
if (QCoreApplication::applicationDirPath().contains(';')) {
|
||||||
errorMessage = failureMessage(FailureType::SEMICOLON);
|
m_LoadFailure = 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();
|
||||||
errorMessage = failureMessage(FailureType::DLL_NOT_FOUND);
|
m_LoadFailure = 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
|
||||||
@@ -81,8 +113,8 @@ bool ProxyPython::initialize(QString& errorMessage)
|
|||||||
//
|
//
|
||||||
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();
|
||||||
errorMessage = failureMessage(FailureType::DLL_NOT_FOUND);
|
m_LoadFailure = 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;
|
||||||
@@ -96,16 +128,21 @@ bool ProxyPython::initialize(QString& errorMessage)
|
|||||||
|
|
||||||
if (m_Runner) {
|
if (m_Runner) {
|
||||||
const auto libpath = pluginFolder / "libs";
|
const auto libpath = pluginFolder / "libs";
|
||||||
const std::vector<fs::path> paths{libpath / "pythoncore.zip", libpath};
|
const std::vector<fs::path> paths{
|
||||||
|
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()) {
|
||||||
errorMessage = failureMessage(FailureType::INITIALIZATION);
|
m_LoadFailure = FailureType::INITIALIZATION;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_Runner->addDllSearchPath(pluginFolder / "dlls");
|
m_Runner->addDllSearchPath(pluginFolder / "dlls");
|
||||||
@@ -114,58 +151,110 @@ bool ProxyPython::initialize(QString& errorMessage)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QList<QObject*>> ProxyPython::load(const PluginExtension& extension)
|
QString ProxyPython::name() const
|
||||||
{
|
{
|
||||||
if (!m_Runner) {
|
return "Python Proxy";
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.autodetect()) {
|
|
||||||
log::debug("{}: automatic plugin detection is not supported for Python plugins",
|
|
||||||
extension.metadata().name());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
QString ProxyPython::localizedName() const
|
||||||
{
|
{
|
||||||
if (!m_Runner) {
|
return tr("Python Proxy");
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (auto it = m_ExtensionModules.find(&extension); it != m_ExtensionModules.end()) {
|
QString ProxyPython::author() const
|
||||||
for (auto& [moduleName, modulePath] : it->second) {
|
{
|
||||||
m_Runner->unload(moduleName, modulePath);
|
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);
|
||||||
}
|
}
|
||||||
m_ExtensionModules.erase(it);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProxyPython::unloadAll()
|
QList<QObject*> ProxyPython::load(const QString& identifier)
|
||||||
|
{
|
||||||
|
if (!m_Runner) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return m_Runner->load(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyPython::unload(const QString& identifier)
|
||||||
{
|
{
|
||||||
if (m_Runner) {
|
if (m_Runner) {
|
||||||
for (auto& [ext, modules] : m_ExtensionModules) {
|
return m_Runner->unload(identifier);
|
||||||
for (auto& [moduleName, modulePath] : modules) {
|
|
||||||
m_Runner->unload(moduleName, modulePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m_ExtensionModules.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ProxyPython::failureMessage(FailureType key)
|
std::vector<unsigned int> ProxyPython::activeProblems() const
|
||||||
{
|
{
|
||||||
switch (key) {
|
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)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ProxyPython::shortDescription(unsigned int key) const
|
||||||
|
{
|
||||||
|
switch (static_cast<FailureType>(key)) {
|
||||||
|
case FailureType::SEMICOLON:
|
||||||
|
return tr("ModOrganizer path contains a semicolon");
|
||||||
|
case FailureType::DLL_NOT_FOUND:
|
||||||
|
return tr("Python DLL not found");
|
||||||
|
case FailureType::INVALID_DLL:
|
||||||
|
return tr("Invalid Python DLL");
|
||||||
|
case FailureType::INITIALIZATION:
|
||||||
|
return tr("Initializing Python failed");
|
||||||
|
default:
|
||||||
|
return tr("invalid problem key %1").arg(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ProxyPython::fullDescription(unsigned int key) const
|
||||||
|
{
|
||||||
|
switch (static_cast<FailureType>(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 "
|
||||||
@@ -188,6 +277,13 @@ QString ProxyPython::failureMessage(FailureType key)
|
|||||||
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("no failure");
|
return tr("invalid problem key %1").arg(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ProxyPython::hasGuidedFix(unsigned int) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyPython::startGuidedFix(unsigned int) const {}
|
||||||
|
|||||||
+28
-17
@@ -23,27 +23,45 @@ 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 MOBase::IPluginLoader {
|
class ProxyPython : public QObject,
|
||||||
|
public MOBase::IPluginProxy,
|
||||||
|
public MOBase::IPluginDiagnose {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(MOBase::IPluginLoader)
|
Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose)
|
||||||
Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython")
|
Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython")
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ProxyPython();
|
ProxyPython();
|
||||||
|
|
||||||
bool initialize(QString& errorMessage) override;
|
virtual bool init(MOBase::IOrganizer* moInfo);
|
||||||
QList<QList<QObject*>> load(const MOBase::PluginExtension& extension) override;
|
virtual QString name() const override;
|
||||||
void unload(const MOBase::PluginExtension& extension) override;
|
virtual QString localizedName() const override;
|
||||||
void unloadAll() override;
|
virtual QString author() const 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,
|
||||||
@@ -52,14 +70,7 @@ private:
|
|||||||
INITIALIZATION = 4
|
INITIALIZATION = 4
|
||||||
};
|
};
|
||||||
|
|
||||||
static QString failureMessage(FailureType failureType);
|
FailureType m_LoadFailure;
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -2,17 +2,12 @@
|
|||||||
|
|
||||||
#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 {
|
||||||
|
|
||||||
@@ -86,18 +81,7 @@ namespace pybind11::detail {
|
|||||||
2 * src.length(), nullptr, 0);
|
2 * src.length(), nullptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class T>
|
bool type_caster<QVariant>::load(handle src, bool)
|
||||||
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())) {
|
||||||
@@ -135,15 +119,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -156,25 +131,13 @@ 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:
|
||||||
@@ -183,16 +146,6 @@ 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();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#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
|
||||||
@@ -91,7 +91,7 @@ namespace mo2::python {
|
|||||||
};
|
};
|
||||||
|
|
||||||
template <class R, class C, class... Args>
|
template <class R, class C, class... Args>
|
||||||
struct function_signature<R (C::*)(Args...)&> {
|
struct function_signature<R (C::*)(Args...) &> {
|
||||||
using type = R(C*, Args...);
|
using type = R(C*, Args...);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ 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 but we install the PDB
|
# proxy will install runner
|
||||||
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)
|
||||||
|
|||||||
+120
-68
@@ -36,14 +36,24 @@ namespace mo2::python {
|
|||||||
PythonRunner() = default;
|
PythonRunner() = default;
|
||||||
~PythonRunner() = default;
|
~PythonRunner() = default;
|
||||||
|
|
||||||
QList<QList<QObject*>> load(std::string_view moduleName,
|
QList<QObject*> load(const QString& identifier) override;
|
||||||
std::filesystem::path const& modulePath) override;
|
void unload(const QString& identifier) 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()
|
||||||
@@ -152,44 +162,72 @@ namespace mo2::python {
|
|||||||
py::module_::import("os").attr("add_dll_directory")(absolute(dllPath));
|
py::module_::import("os").attr("add_dll_directory")(absolute(dllPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QList<QObject*>> PythonRunner::load(std::string_view name,
|
void PythonRunner::ensureFolderInPath(QString folder)
|
||||||
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 {
|
||||||
|
|
||||||
// some needed import
|
// dictionary that will contain createPlugin() or createPlugins().
|
||||||
auto sys = py::module_::import("sys");
|
py::dict moduleDict;
|
||||||
auto importlib_util = py::module_::import("importlib.util");
|
|
||||||
|
|
||||||
// check if the module is already loaded
|
if (identifier.endsWith(".py")) {
|
||||||
py::dict modules = sys.attr("modules");
|
py::object mainModule = py::module_::import("__main__");
|
||||||
py::module_ pymodule;
|
|
||||||
if (modules.contains(name)) {
|
// make a copy, otherwise we might end up calling the createPlugin() or
|
||||||
pymodule = modules[py::str(name)];
|
// createPlugins() function multiple time
|
||||||
pymodule.reload();
|
py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")();
|
||||||
|
|
||||||
|
std::string temp = ToString(identifier);
|
||||||
|
py::eval_file(temp, moduleNamespace).is_none();
|
||||||
|
moduleDict = moduleNamespace;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// load the module
|
// Retrieve the module name:
|
||||||
auto spec =
|
QStringList parts = identifier.split("/");
|
||||||
importlib_util.attr("spec_from_file_location")(name, pythonModule);
|
std::string moduleName = ToString(parts.takeLast());
|
||||||
|
ensureFolderInPath(parts.join("/"));
|
||||||
|
|
||||||
if (Py_IsNone(spec.ptr())) {
|
// check if the module is already loaded
|
||||||
MOBase::log::error("failed to load Python plugin '{}' from '{}'",
|
py::dict modules = py::module_::import("sys").attr("modules");
|
||||||
name, pythonModule);
|
if (modules.contains(moduleName)) {
|
||||||
return {};
|
py::module_ prev = modules[py::str(moduleName)];
|
||||||
|
py::module_(prev).reload();
|
||||||
|
moduleDict = prev.attr("__dict__");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
moduleDict =
|
||||||
|
py::module_::import(moduleName.c_str()).attr("__dict__");
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {}", pythonModule);
|
MOBase::log::error("No plugins found in {}.", identifier);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +240,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("{}: createPlugins must return a sequence",
|
MOBase::log::error(
|
||||||
pythonModule);
|
"Plugin {}: createPlugins must return a sequence.", identifier);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
py::sequence pyList(pyPlugins);
|
py::sequence pyList(pyPlugins);
|
||||||
@@ -214,26 +252,30 @@ namespace mo2::python {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
MOBase::log::error("{}: missing createPlugin(s) function",
|
MOBase::log::error("Plugin {}: missing a createPlugin(s) function.",
|
||||||
pythonModule);
|
identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {};
|
return QList<QObject*>();
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QList<QObject*>> allInterfaceList;
|
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("{}: no plugin interface implemented.",
|
MOBase::log::error("Plugin {}: no plugin interface implemented.",
|
||||||
pythonModule);
|
identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the plugins to the main list:
|
// Append the plugins to the main list:
|
||||||
@@ -243,47 +285,57 @@ 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 {}", pythonModule);
|
MOBase::log::error("Failed to import plugin from {}.", identifier);
|
||||||
throw pyexcept::PythonError(ex);
|
throw pyexcept::PythonError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PythonRunner::unload(std::string_view moduleName,
|
void PythonRunner::unload(const QString& identifier)
|
||||||
std::filesystem::path const& modulePath)
|
|
||||||
{
|
{
|
||||||
py::gil_scoped_acquire lock;
|
auto it = m_PythonObjects.find(identifier);
|
||||||
|
if (it != m_PythonObjects.end()) {
|
||||||
|
|
||||||
// at this point, the identifier is the full path to the module.
|
py::gil_scoped_acquire lock;
|
||||||
QDir folder(modulePath);
|
|
||||||
|
|
||||||
// we want to "unload" (remove from sys.modules) modules that come
|
if (!identifier.endsWith(".py")) {
|
||||||
// from this plugin (whose __path__ points under this module,
|
|
||||||
// including the module of the plugin itself)
|
|
||||||
//
|
|
||||||
py::object sys = py::module_::import("sys");
|
|
||||||
py::dict modules = sys.attr("modules");
|
|
||||||
py::list keys = modules.attr("keys")();
|
|
||||||
for (std::size_t i = 0; i < py::len(keys); ++i) {
|
|
||||||
py::object mod = modules[keys[i]];
|
|
||||||
if (PyObject_HasAttrString(mod.ptr(), "__path__")) {
|
|
||||||
QString mpath = mod.attr("__path__")[py::int_(0)].cast<QString>();
|
|
||||||
|
|
||||||
if (!folder.relativeFilePath(mpath).startsWith("..")) {
|
// At this point, the identifier is the full path to the module.
|
||||||
// if the path is under identifier, we need to unload it
|
QDir folder(identifier);
|
||||||
log::debug("unloading module {} from {}",
|
|
||||||
keys[i].cast<std::string>(), mpath);
|
|
||||||
|
|
||||||
PyDict_DelItem(modules.ptr(), keys[i].ptr());
|
// We want to "unload" (remove from sys.modules) modules that come
|
||||||
|
// from this plugin (whose __path__ points under this module,
|
||||||
|
// including the module of the plugin itself).
|
||||||
|
py::object sys = py::module_::import("sys");
|
||||||
|
py::dict modules = sys.attr("modules");
|
||||||
|
py::list keys = modules.attr("keys")();
|
||||||
|
for (std::size_t i = 0; i < py::len(keys); ++i) {
|
||||||
|
py::object mod = modules[keys[i]];
|
||||||
|
if (PyObject_HasAttrString(mod.ptr(), "__path__")) {
|
||||||
|
QString mpath =
|
||||||
|
mod.attr("__path__")[py::int_(0)].cast<QString>();
|
||||||
|
|
||||||
|
if (!folder.relativeFilePath(mpath).startsWith("..")) {
|
||||||
|
// If the path is under identifier, we need to unload
|
||||||
|
// it.
|
||||||
|
log::debug("Unloading module {} from {} for {}.",
|
||||||
|
keys[i].cast<std::string>(), mpath, identifier);
|
||||||
|
|
||||||
|
PyDict_DelItem(modules.ptr(), keys[i].ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// for simple Python file - not really used anymore, but actually used in
|
// Boost.Python does not handle cyclic garbace collection, so we need to
|
||||||
// testing - we need to remove using the module name
|
// release everything hold by the objects before deleting the objects
|
||||||
//
|
// themselves (done when erasing from m_PythonObjects).
|
||||||
py::str pyModuleName(moduleName);
|
for (auto& obj : it->second) {
|
||||||
if (modules.contains(pyModuleName)) {
|
obj.attr("__dict__").attr("clear")();
|
||||||
PyDict_DelItem(modules.ptr(), pyModuleName.ptr());
|
}
|
||||||
|
|
||||||
|
log::debug("Deleting {} python objects for {}.", it->second.size(),
|
||||||
|
identifier);
|
||||||
|
m_PythonObjects.erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user