diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4940fcc..095a2e4 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -6,20 +6,50 @@ on: pull_request: types: [opened, synchronize, reopened] +env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + jobs: build: runs-on: windows-2022 steps: - - name: Build Plugin Python - id: build-plugin-python - uses: ModOrganizer2/build-with-mob-action@master + # 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 + with: + python-version: "3.12" + + - name: Install Qt + uses: jurplel/install-qt-action@v3 with: - mo2-third-parties: gtest python spdlog boost sip pyqt pybind11 - mo2-dependencies: cmake_common uibase - mo2-cmake-command: -DPLUGIN_PYTHON_TESTS=1 .. - - name: Build Plugin Python Tests - run: cmake --build vsbuild --config RelWithDebInfo -j4 --target python-tests --target runner-tests - working-directory: ${{ steps.build-plugin-python.outputs.working-directory }} + setup-python: false + 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 + run: cmake --build vsbuild --config RelWithDebInfo --verbose ` + --target python-tests --target runner-tests --target proxy + - name: Test Plugin Python run: ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure - working-directory: ${{ steps.build-plugin-python.outputs.working-directory }} diff --git a/CMakeLists.txt b/CMakeLists.txt index e92c331..5d3acf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,44 +2,32 @@ cmake_minimum_required(VERSION 3.16) cmake_policy(SET CMP0144 NEW) -if(DEFINED DEPENDENCIES_DIR) - include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/mo2.cmake) -else() - include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) -endif() - project(plugin_python CXX) -set(PYTHON_BUILD_PATH ${PYTHON_ROOT}/PCBuild/amd64) +set(Python_FIND_VIRTUALENV STANDARD) -# find Python - lots of "Hints" since we have a weird setup -set(Python_USE_STATIC_LIBS False) -set(Python_INCLUDE_DIR ${PYTHON_ROOT}/Include) -set(Python_EXECUTABLE ${PYTHON_BUILD_PATH}/python.exe) -if (EXISTS "${PYTHON_BUILD_PATH}/python_d.exe") - set(Python_EXECUTABLE ${PYTHON_BUILD_PATH}/python_d.exe) -endif() -file(GLOB Python_LIBRARY ${PYTHON_BUILD_PATH}/python[0-9][0-9]*.lib) -find_package(Python COMPONENTS Interpreter Development REQUIRED) +# 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 +find_package(Python ${MO2_PYTHON_VERSION} COMPONENTS Interpreter Development REQUIRED) +find_package(pybind11 CONFIG REQUIRED) -# pybind11 needs uppercase (at least EXECUTABLE and LIBRARY) -set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) -set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIR}) -set(PYTHON_LIBRARY ${Python_LIBRARY}) +find_package(mo2-cmake CONFIG REQUIRED) + +get_filename_component(Python_HOME ${Python_EXECUTABLE} PATH) +set(Python_DLL_DIR "${Python_HOME}/DLLs") +set(Python_LIB_DIR "${Python_HOME}/Lib") + +mo2_python_install_pyqt() # useful for naming DLL, zip, etc. (3.10 -> 310) set(Python_VERSION_SHORT ${Python_VERSION_MAJOR}${Python_VERSION_MINOR}) -# pybind11 -add_subdirectory(${MO2_BUILD_PATH}/pybind11 ${CMAKE_CURRENT_BINARY_DIR}/pybind11) - # projects add_subdirectory(src) -set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT proxy) # tests (if requested) -set(PLUGIN_PYTHON_TESTS ${PLUGIN_PYTHON_TESTS} CACHE BOOL "build tests for plugin_python") -if (PLUGIN_PYTHON_TESTS) +set(PLUGIN_PYTHON_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python") +if (PLUGIN_PYTHON_TESTING) enable_testing() add_subdirectory(tests) endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..6293933 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,73 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone;testing" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/src/mobase/CMakeLists.txt b/src/mobase/CMakeLists.txt index 87fd525..72eff01 100644 --- a/src/mobase/CMakeLists.txt +++ b/src/mobase/CMakeLists.txt @@ -1,12 +1,38 @@ cmake_minimum_required(VERSION 3.16) +find_package(Qt6 COMPONENTS Core) +find_package(mo2-uibase CONFIG REQUIRED) + pybind11_add_module(mobase MODULE) -mo2_configure_library(mobase - SOURCE_TREE +mo2_default_source_group() +mo2_configure_target(mobase + NO_SOURCES WARNINGS 4 EXTERNAL_WARNINGS 4 AUTOMOC ON TRANSLATIONS OFF - PRIVATE_DEPENDS uibase Qt::Core ) -target_link_libraries(mobase PRIVATE pybind11::qt pybind11::utils) +mo2_target_sources(mobase + FOLDER src + PRIVATE + deprecation.cpp + deprecation.h + mobase.cpp + pybind11_all.h +) +mo2_target_sources(mobase + FOLDER src/wrappers + PRIVATE + ./wrappers/basic_classes.cpp + ./wrappers/game_features.cpp + ./wrappers/known_folders.h + ./wrappers/pyfiletree.cpp + ./wrappers/pyfiletree.h + ./wrappers/pyplugins.cpp + ./wrappers/pyplugins.h + ./wrappers/utils.cpp + ./wrappers/widgets.cpp + ./wrappers/wrappers.cpp + ./wrappers/wrappers.h +) +target_link_libraries(mobase PRIVATE pybind11::qt pybind11::utils mo2::uibase Qt6::Core) diff --git a/src/mobase/deprecation.cpp b/src/mobase/deprecation.cpp index 2b86eee..958712d 100644 --- a/src/mobase/deprecation.cpp +++ b/src/mobase/deprecation.cpp @@ -7,7 +7,7 @@ #include -#include "log.h" +#include namespace py = pybind11; diff --git a/src/mobase/mobase.cpp b/src/mobase/mobase.cpp index 575e716..3f80de5 100644 --- a/src/mobase/mobase.cpp +++ b/src/mobase/mobase.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include #include "pybind11_all.h" @@ -12,23 +15,6 @@ #include "wrappers/pyfiletree.h" #include "wrappers/wrappers.h" -// TODO: remove these include (only for testing) -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - using namespace MOBase; namespace py = pybind11; @@ -41,6 +27,13 @@ PYBIND11_MODULE(mobase, m) m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui")); m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets")); + // exceptions + // + py::register_exception(m, "MO2Exception"); + py::register_exception(m, "InvalidNXMLinkException"); + py::register_exception(m, "IncompatibilityException"); + py::register_exception(m, "InvalidVersionException"); + // bindings // mo2::python::add_basic_bindings(m); @@ -86,7 +79,8 @@ PYBIND11_MODULE(mobase, m) // m.add_object( "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 // diff --git a/src/mobase/pybind11_all.h b/src/mobase/pybind11_all.h index 019e98d..9a76ad2 100644 --- a/src/mobase/pybind11_all.h +++ b/src/mobase/pybind11_all.h @@ -16,9 +16,9 @@ #include "pybind11_utils/shared_cpp_owner.h" #include "pybind11_utils/smart_variant_wrapper.h" -#include -#include -#include +#include +#include +#include namespace mo2::python { diff --git a/src/mobase/wrappers/basic_classes.cpp b/src/mobase/wrappers/basic_classes.cpp index c594362..9dec4eb 100644 --- a/src/mobase/wrappers/basic_classes.cpp +++ b/src/mobase/wrappers/basic_classes.cpp @@ -4,21 +4,23 @@ #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "../deprecation.h" #include "pyfiletree.h" @@ -33,6 +35,69 @@ namespace mo2::python { void add_versioninfo_classes(py::module_ m) { + // Version + py::class_ pyVersion(m, "Version"); + + py::enum_(pyVersion, "ReleaseType") + .value("DEVELOPMENT", Version::Development) + .value("ALPHA", Version::Alpha) + .value("BETA", Version::Beta) + .value("RELEASE_CANDIDATE", Version::ReleaseCandidate) + .export_values(); + + py::enum_(pyVersion, "ParseMode") + .value("SEMVER", Version::ParseMode::SemVer) + .value("MO2", Version::ParseMode::MO2); + + py::enum_(pyVersion, "FormatMode", py::arithmetic{}) + .value("FORCE_SUBPATCH", Version::FormatMode::ForceSubPatch) + .value("NO_SEPARATOR", Version::FormatMode::NoSeparator) + .value("SHORT_ALPHA_BETA", Version::FormatMode::ShortAlphaBeta) + .value("NO_METADATA", Version::FormatMode::NoMetadata) + .value("CONDENSED", + static_cast(Version::FormatCondensed.toInt())) + .export_values(); + + pyVersion + .def_static("parse", &Version::parse, "value"_a, + "mode"_a = Version::ParseMode::SemVer) + .def(py::init(), "major"_a, "minor"_a, "patch"_a, + "metadata"_a = "") + .def(py::init(), "major"_a, "minor"_a, + "patch"_a, "subpatch"_a, "metadata"_a = "") + .def(py::init(), "major"_a, + "minor"_a, "patch"_a, "type"_a, "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a, + "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "type"_a, "prerelease"_a, + "metadata"_a = "") + .def(py::init(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a, + "prerelease"_a, "metadata"_a = "") + .def(py::init>, + QString>(), + "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "prereleases"_a, + "metadata"_a = "") + .def("isPreRelease", &Version::isPreRelease) + .def_property_readonly("major", &Version::major) + .def_property_readonly("minor", &Version::minor) + .def_property_readonly("patch", &Version::patch) + .def_property_readonly("subpatch", &Version::subpatch) + .def_property_readonly("prereleases", &Version::preReleases) + .def_property_readonly("build_metadata", &Version::buildMetadata) + .def("string", &Version::string, "mode"_a = Version::FormatCondensed) + .def("__str__", &Version::string) + .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); + + // VersionInfo py::enum_(m, "ReleaseType") .value("final", MOBase::VersionInfo::RELEASE_FINAL) .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE) @@ -295,6 +360,38 @@ namespace mo2::python { py::implicitly_convertible>(); } + void add_iextensionlist_classes(py::module_ m) + { + // TODO: add all bindings here + + py::class_(m, "IExtension"); + + py::class_(m, "IExtensionList") + .def("installed", &IExtensionList::installed, "identifier"_a) + .def( + "enabled", + py::overload_cast(&IExtensionList::enabled, py::const_), + "identifier"_a) + .def( + "__getitem__", + [](IExtensionList const& self, + std::variant const& index) { + return std::visit( + [&self](auto&& value) { + if constexpr (std::is_same_v, + 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) { py::enum_(m, "PluginState", py::arithmetic()) @@ -447,7 +544,17 @@ namespace mo2::python { .def("overwritePath", &IOrganizer::overwritePath) .def("basePath", &IOrganizer::basePath) .def("modsPath", &IOrganizer::modsPath) - .def("appVersion", &IOrganizer::appVersion) + .def("appVersion", + [](IOrganizer& o) { + mo2::python::show_deprecation_warning( + "appVersion", "IOrganizer::appVersion() is deprecated, use " + "IOrganizer::version() instead."); +#pragma warning(push) +#pragma warning(disable : 4996) + return o.appVersion(); +#pragma warning(pop) + }) + .def("version", &IOrganizer::version) .def("createMod", &IOrganizer::createMod, py::return_value_policy::reference, "name"_a) .def("getGame", &IOrganizer::getGame, py::return_value_policy::reference, @@ -517,6 +624,8 @@ namespace mo2::python { py::return_value_policy::reference) .def("pluginList", &IOrganizer::pluginList, py::return_value_policy::reference) + .def("extensionList", &IOrganizer::extensionList, + py::return_value_policy::reference) .def("modList", &IOrganizer::modList, py::return_value_policy::reference) .def("gameFeatures", &IOrganizer::gameFeatures, py::return_value_policy::reference) @@ -721,12 +830,37 @@ namespace mo2::python { add_modinterface_classes(m); add_modrepository_classes(m); - py::class_(m, "PluginSetting") - .def(py::init(), "key"_a, - "description"_a, "default_value"_a) - .def_readwrite("key", &PluginSetting::key) - .def_readwrite("description", &PluginSetting::description) - .def_readwrite("default_value", &PluginSetting::defaultValue); + py::class_(m, "Setting") + .def(py::init([](const QString& name, const QString& description, + const QVariant& defaultValue) { + mo2::python::show_deprecation_warning( + "Setting(key, description, default)", + "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(), + "name"_a, "title"_a, "description"_a, "default_value"_a) + .def(py::init(), + "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_(m, "SettingGroup") + .def(py::init(), "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_(m, "PluginRequirementFactory") // pluginDependency @@ -785,6 +919,7 @@ namespace mo2::python { }) .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a); + add_iextensionlist_classes(m); add_ipluginlist_classes(m); add_imodlist_classes(m); add_idownload_manager_classes(m); diff --git a/src/mobase/wrappers/game_features.cpp b/src/mobase/wrappers/game_features.cpp index fce2bd2..70b82ee 100644 --- a/src/mobase/wrappers/game_features.cpp +++ b/src/mobase/wrappers/game_features.cpp @@ -4,19 +4,19 @@ #include "../pybind11_all.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "pyfiletree.h" diff --git a/src/mobase/wrappers/pyfiletree.cpp b/src/mobase/wrappers/pyfiletree.cpp index 4fb1560..4657b8a 100644 --- a/src/mobase/wrappers/pyfiletree.cpp +++ b/src/mobase/wrappers/pyfiletree.cpp @@ -5,8 +5,8 @@ #include "../pybind11_all.h" -#include -#include +#include +#include namespace py = pybind11; using namespace MOBase; diff --git a/src/mobase/wrappers/pyfiletree.h b/src/mobase/wrappers/pyfiletree.h index 25655d2..0e94665 100644 --- a/src/mobase/wrappers/pyfiletree.h +++ b/src/mobase/wrappers/pyfiletree.h @@ -3,7 +3,7 @@ #include "../pybind11_all.h" -#include +#include namespace pybind11 { template <> diff --git a/src/mobase/wrappers/pyplugins.cpp b/src/mobase/wrappers/pyplugins.cpp index 49fca7e..92f11bc 100644 --- a/src/mobase/wrappers/pyplugins.cpp +++ b/src/mobase/wrappers/pyplugins.cpp @@ -144,12 +144,9 @@ namespace mo2::python { .def("init", &IPlugin::init, "organizer"_a) .def("name", &IPlugin::name) .def("localizedName", &IPlugin::localizedName) - .def("master", &IPlugin::master) - .def("author", &IPlugin::author) - .def("description", &IPlugin::description) - .def("version", &IPlugin::version) .def("requirements", &IPlugin::requirements) .def("settings", &IPlugin::settings) + .def("settingGroups", &IPlugin::settingGroups) .def("enabledByDefault", &IPlugin::enabledByDefault); py::class_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // these needs to be defined in a header file for automoc - this file is included only // in pyplugins.cpp @@ -45,25 +45,19 @@ namespace mo2::python { { PYBIND11_OVERRIDE(QString, PluginBase, localizedName, ); } - QString master() const override + std::vector> + requirements() const override { - PYBIND11_OVERRIDE(QString, PluginBase, master, ); - } - QString author() const override - { - PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, ); - } - QString description() const override - { - PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, ); + PYBIND11_OVERRIDE(std::vector>, + PluginBase, requirements, ); } - VersionInfo version() const override + QList settings() const override { - PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, ); + PYBIND11_OVERRIDE_PURE(QList, PluginBase, settings, ); } - QList settings() const override + QList settingGroups() const override { - PYBIND11_OVERRIDE_PURE(QList, PluginBase, settings, ); + PYBIND11_OVERRIDE(QList, PluginBase, settingGroups, ); } }; diff --git a/src/mobase/wrappers/utils.cpp b/src/mobase/wrappers/utils.cpp index 95cd637..eb3e9e2 100644 --- a/src/mobase/wrappers/utils.cpp +++ b/src/mobase/wrappers/utils.cpp @@ -2,8 +2,8 @@ #include "../pybind11_all.h" -#include -#include +#include +#include #include "known_folders.h" diff --git a/src/mobase/wrappers/widgets.cpp b/src/mobase/wrappers/widgets.cpp index 0abdbe5..e626540 100644 --- a/src/mobase/wrappers/widgets.cpp +++ b/src/mobase/wrappers/widgets.cpp @@ -2,7 +2,7 @@ #include "../pybind11_all.h" -#include +#include namespace py = pybind11; using namespace MOBase; diff --git a/src/mobase/wrappers/wrappers.cpp b/src/mobase/wrappers/wrappers.cpp index 33d5804..10f043d 100644 --- a/src/mobase/wrappers/wrappers.cpp +++ b/src/mobase/wrappers/wrappers.cpp @@ -10,10 +10,10 @@ // IOrganizer must be bring here to properly compile the Python bindings of // plugin requirements -#include -#include -#include -#include +#include +#include +#include +#include using namespace pybind11::literals; namespace py = pybind11; diff --git a/src/mobase/wrappers/wrappers.h b/src/mobase/wrappers/wrappers.h index d4af629..43dd574 100644 --- a/src/mobase/wrappers/wrappers.h +++ b/src/mobase/wrappers/wrappers.h @@ -10,7 +10,7 @@ #include #include -#include +#include namespace mo2::python { diff --git a/src/plugin_python_en.ts b/src/plugin_python_en.ts index 9ef7836..b51ccf5 100644 --- a/src/plugin_python_en.ts +++ b/src/plugin_python_en.ts @@ -4,73 +4,30 @@ ProxyPython - - Python Initialization failed - - - - - 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. - - - - - Python Proxy - - - - - Proxy Plugin to allow plugins written in python to be loaded - - - - - ModOrganizer path contains a semicolon - - - - - Python DLL not found - - - - - Invalid Python DLL - - - - - Initializing Python failed - - - - - - invalid problem key %1 - - - - + 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. - + The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem. - + 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. - + The initialization of the Python plugin DLL failed, unfortunately without any details. + + + no failure + + QObject diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index bee4952..a383753 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -1,9 +1,12 @@ cmake_minimum_required(VERSION 3.16) -set(PLUGIN_NAME "plugin_python") +find_package(mo2-uibase CONFIG REQUIRED) -add_library(proxy SHARED) +set(PROXY_NAME "python") + +add_library(proxy SHARED proxypython.cpp proxypython.h) mo2_configure_plugin(proxy + NO_SOURCES WARNINGS 4 EXTERNAL_WARNINGS 4 TRANSLATIONS OFF @@ -11,19 +14,25 @@ mo2_configure_plugin(proxy ${CMAKE_CURRENT_SOURCE_DIR}/../runner ${CMAKE_CURRENT_SOURCE_DIR}/../mobase ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) -target_link_libraries(proxy PRIVATE runner) -set_target_properties(proxy PROPERTIES OUTPUT_NAME ${PLUGIN_NAME}) -mo2_install_target(proxy FOLDER) +mo2_default_source_group() +target_link_libraries(proxy PRIVATE runner mo2::uibase) +set_target_properties(proxy PROPERTIES OUTPUT_NAME "python_proxy") + +set(PROXY_PYTHON_DIR ${MO2_INSTALL_BIN}/proxies/python) -set(PLUGIN_PYTHON_DIR ${MO2_INSTALL_PATH}/bin/plugins/${PLUGIN_NAME}) +# install runner and proxy +install(FILES $ DESTINATION ${PROXY_PYTHON_DIR}) +install(FILES $ DESTINATION ${PROXY_PYTHON_DIR}/dlls) -# install runner +# install PDB +install(FILES $ DESTINATION pdb) + +# delay loading since the dll is not in the standard folder target_link_options(proxy PRIVATE "/DELAYLOAD:runner.dll") -mo2_install_target(runner INSTALLDIR ${PLUGIN_PYTHON_DIR}/dlls) # translations (custom location) 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 ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../runner @@ -31,25 +40,39 @@ mo2_add_translations(proxy ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) # install DLLs files needed -set(DLL_DIRS ${PLUGIN_PYTHON_DIR}/dlls) +set(DLL_DIRS ${PROXY_PYTHON_DIR}/dlls) file(GLOB dlls_to_install - ${PYTHON_BUILD_PATH}/libffi*.dll - ${PYTHON_BUILD_PATH}/python${Python_VERSION_SHORT}*.dll) + # ${PYTHON_BUILD_PATH}/libffi*.dll + ${Python_HOME}/python${Python_VERSION_MAJOR}*.dll) install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS}) -# install Python files -set(PYLIB_DIR ${PLUGIN_PYTHON_DIR}/libs) -file(GLOB libs_to_install ${PYTHON_BUILD_PATH}/pythoncore/*.pyd) +# install Python .pyd files +set(PYLIB_DIR ${PROXY_PYTHON_DIR}/libs) +file(GLOB libs_to_install ${Python_DLL_DIR}/*.pyd) install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR}) -install(FILES ${PYTHON_BUILD_PATH}/pythoncore/python${Python_VERSION_SHORT}.zip - DESTINATION ${PYLIB_DIR} RENAME pythoncore.zip) + +# generate + install standard library +set(pythoncore_zip "${CMAKE_CURRENT_BINARY_DIR}/pythoncore.zip") +add_custom_command( + TARGET proxy POST_BUILD + COMMAND ${Python_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}\\build_pythoncore.py" + ${pythoncore_zip} + ) +install(FILES ${pythoncore_zip} DESTINATION ${PYLIB_DIR}) # install mobase install(TARGETS mobase DESTINATION ${PYLIB_DIR}) # install PyQt6 -#file(GLOB PYQT_DIR ${MO2_BUILD_PATH}/PyQt${QT_MAJOR_VERSION}*) -set(PYQT_LIB_DIR ${PYTHON_ROOT}/Lib/site-packages/PyQt${QT_MAJOR_VERSION}) -set(PYQT_TARGET_DIR ${PYLIB_DIR}/PyQt${QT_MAJOR_VERSION}) -file(GLOB pyqt_files ${PYQT_LIB_DIR}/*.py ${PYQT_LIB_DIR}/*.pyd ${PYQT_LIB_DIR}/*.pyi) -install(FILES ${pyqt_files} DESTINATION ${PYQT_TARGET_DIR}) +install( + DIRECTORY ${CMAKE_BINARY_DIR}/pylibs/PyQt${MO2_QT_VERSION_MAJOR} + DESTINATION ${PYLIB_DIR} + PATTERN "*.pyd" + PATTERN "*.pyi" + PATTERN "__pycache__" EXCLUDE + PATTERN "bindings" EXCLUDE + PATTERN "lupdate" EXCLUDE + PATTERN "Qt6" EXCLUDE + PATTERN "uic" EXCLUDE +) diff --git a/src/proxy/build_pythoncore.py b/src/proxy/build_pythoncore.py new file mode 100644 index 0000000..481dec5 --- /dev/null +++ b/src/proxy/build_pythoncore.py @@ -0,0 +1,14 @@ +import sys +import zipfile +from pathlib import Path + +_EXCLUDE_MODULES = ["ensurepip", "idlelib", "test", "tkinter", "turtle_demo", "venv"] + +libdir = Path(sys.executable).parent.joinpath("Lib") +assert libdir.exists() + +with zipfile.PyZipFile(sys.argv[1], optimize=2, mode="w") as fp: + fp.writepy(libdir) # pyright: ignore[reportArgumentType] + for path in libdir.iterdir(): + if path.is_dir() and path.name not in _EXCLUDE_MODULES: + fp.writepy(path) # pyright: ignore[reportArgumentType] diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index b38dd0c..3e4ade4 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -28,9 +28,9 @@ along with python proxy plugin. If not, see . #include #include -#include -#include -#include +#include +#include +#include namespace fs = std::filesystem; using namespace MOBase; @@ -53,59 +53,26 @@ fs::path getPluginFolder() return fs::path(path).parent_path(); } -ProxyPython::ProxyPython() - : m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr}, - m_LoadFailure(FailureType::NONE) -{ -} +ProxyPython::ProxyPython() : m_RunnerLib{nullptr}, m_Runner{nullptr} {} -bool ProxyPython::init(IOrganizer* moInfo) +bool ProxyPython::initialize(QString& errorMessage) { - m_MOInfo = moInfo; - - if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { - return false; - } + errorMessage = ""; if (QCoreApplication::applicationDirPath().contains(';')) { - m_LoadFailure = FailureType::SEMICOLON; + errorMessage = failureMessage(FailureType::SEMICOLON); return true; } const auto pluginFolder = getPluginFolder(); - if (pluginFolder.empty()) { - DWORD error = ::GetLastError(); - m_LoadFailure = FailureType::DLL_NOT_FOUND; + DWORD error = ::GetLastError(); + errorMessage = failureMessage(FailureType::DLL_NOT_FOUND); log::error("failed to resolve Python proxy directory ({}): {}", error, qUtf8Printable(windowsErrorString(::GetLastError()))); 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: // // 1. we set the dlls/ subfolder (from the plugin) as the DLL directory so Windows @@ -114,8 +81,8 @@ bool ProxyPython::init(IOrganizer* moInfo) // const auto dllPaths = pluginFolder / "dlls"; if (SetDllDirectoryW(dllPaths.c_str()) == 0) { - DWORD error = ::GetLastError(); - m_LoadFailure = FailureType::DLL_NOT_FOUND; + DWORD error = ::GetLastError(); + errorMessage = failureMessage(FailureType::DLL_NOT_FOUND); log::error("failed to add python DLL directory ({}): {}", error, qUtf8Printable(windowsErrorString(::GetLastError()))); return false; @@ -129,21 +96,16 @@ bool ProxyPython::init(IOrganizer* moInfo) if (m_Runner) { const auto libpath = pluginFolder / "libs"; - const std::vector paths{ - libpath / "pythoncore.zip", libpath, - std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; + const std::vector paths{libpath / "pythoncore.zip", libpath}; m_Runner->initialize(paths); } - if (m_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", false); - } - // reset DLL directory SetDllDirectoryW(NULL); if (!m_Runner || !m_Runner->isInitialized()) { - m_LoadFailure = FailureType::INITIALIZATION; + errorMessage = failureMessage(FailureType::INITIALIZATION); + return false; } else { m_Runner->addDllSearchPath(pluginFolder / "dlls"); @@ -152,110 +114,58 @@ bool ProxyPython::init(IOrganizer* moInfo) return true; } -QString ProxyPython::name() const -{ - 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 ProxyPython::settings() const -{ - return {}; -} - -QStringList ProxyPython::pluginList(const QDir& pluginPath) const +QList> ProxyPython::load(const PluginExtension& extension) { - 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); - } + if (!m_Runner) { + return {}; } - return result; -} - -QList ProxyPython::load(const QString& identifier) -{ - if (!m_Runner) { + if (extension.autodetect()) { + log::debug("{}: automatic plugin detection is not supported for Python plugins", + extension.metadata().name()); return {}; } - return m_Runner->load(identifier); -} -void ProxyPython::unload(const QString& identifier) -{ - if (m_Runner) { - return m_Runner->unload(identifier); + m_ExtensionModules[&extension] = {}; + + QList> plugins; + for (auto& [moduleName, modulePath] : extension.plugins()) { + m_ExtensionModules[&extension].push_back({moduleName, modulePath}); + plugins.append(m_Runner->load(moduleName, modulePath)); } + + return plugins; } -std::vector ProxyPython::activeProblems() const +void ProxyPython::unload(const PluginExtension& extension) { - auto failure = m_LoadFailure; - - // don't know how this could happen but wth - if (m_Runner && !m_Runner->isInitialized()) { - failure = FailureType::INITIALIZATION; + if (!m_Runner) { + return; } - if (failure != FailureType::NONE) { - return {static_cast>(failure)}; + if (auto it = m_ExtensionModules.find(&extension); it != m_ExtensionModules.end()) { + for (auto& [moduleName, modulePath] : it->second) { + m_Runner->unload(moduleName, modulePath); + } + m_ExtensionModules.erase(it); } - - return {}; } -QString ProxyPython::shortDescription(unsigned int key) const +void ProxyPython::unloadAll() { - switch (static_cast(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); + if (m_Runner) { + for (auto& [ext, modules] : m_ExtensionModules) { + for (auto& [moduleName, modulePath] : modules) { + m_Runner->unload(moduleName, modulePath); + } + } } + m_ExtensionModules.clear(); } -QString ProxyPython::fullDescription(unsigned int key) const +QString ProxyPython::failureMessage(FailureType key) { - switch (static_cast(key)) { + switch (key) { case FailureType::SEMICOLON: return tr("The path to Mod Organizer (%1) contains a semicolon.
" "While this is legal on NTFS drives, many applications do not " @@ -278,13 +188,6 @@ QString ProxyPython::fullDescription(unsigned int key) const return tr("The initialization of the Python plugin DLL failed, unfortunately " "without any details."); 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 {} diff --git a/src/proxy/proxypython.h b/src/proxy/proxypython.h index 5165fae..ffb088c 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -23,45 +23,27 @@ along with python proxy plugin. If not, see . #include #include -#include -#include +#include +#include + +#include #include -class ProxyPython : public QObject, - public MOBase::IPluginProxy, - public MOBase::IPluginDiagnose { +class ProxyPython : public MOBase::IPluginLoader { Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) + Q_INTERFACES(MOBase::IPluginLoader) Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython") public: ProxyPython(); - virtual bool init(MOBase::IOrganizer* moInfo); - virtual QString name() const override; - virtual QString localizedName() const override; - virtual QString author() const override; - virtual QString description() const override; - virtual MOBase::VersionInfo version() const override; - virtual QList settings() const override; - - QStringList pluginList(const QDir& pluginPath) const override; - QList load(const QString& identifier) override; - void unload(const QString& identifier) override; - -public: // IPluginDiagnose - virtual std::vector 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; + bool initialize(QString& errorMessage) override; + QList> load(const MOBase::PluginExtension& extension) override; + void unload(const MOBase::PluginExtension& extension) override; + void unloadAll() override; private: - MOBase::IOrganizer* m_MOInfo; - HMODULE m_RunnerLib; - std::unique_ptr m_Runner; - enum class FailureType : unsigned int { NONE = 0, SEMICOLON = 1, @@ -70,7 +52,14 @@ class ProxyPython : public QObject, INITIALIZATION = 4 }; - FailureType m_LoadFailure; + static QString failureMessage(FailureType failureType); + +private: + HMODULE m_RunnerLib; + std::unique_ptr m_Runner; + std::unordered_map>> + m_ExtensionModules; }; #endif // PROXYPYTHON_H diff --git a/src/proxy/resource.h b/src/proxy/resource.h deleted file mode 100644 index 1f33c5c..0000000 --- a/src/proxy/resource.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef RESOURCE_H -#define RESOURCE_H - -#define IDR_LOADER_DLL 100 - -#endif // RESOURCE_H diff --git a/src/pybind11-qt/CMakeLists.txt b/src/pybind11-qt/CMakeLists.txt index b75fbc7..dd9d217 100644 --- a/src/pybind11-qt/CMakeLists.txt +++ b/src/pybind11-qt/CMakeLists.txt @@ -1,18 +1,62 @@ cmake_minimum_required(VERSION 3.16) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets) + +mo2_find_python_executable(PYTHON_EXE) + add_library(pybind11-qt STATIC) -mo2_configure_library(pybind11-qt - SOURCE_TREE +mo2_configure_target(pybind11-qt + NO_SOURCES WARNINGS 4 EXTERNAL_WARNINGS 4 AUTOMOC OFF TRANSLATIONS OFF - PRIVATE_DEPENDS Qt::Core Qt::Widgets ) -target_link_libraries(pybind11-qt PUBLIC pybind11::pybind11) +mo2_default_source_group() +target_sources(pybind11-qt + PRIVATE + ./include/pybind11_qt/pybind11_qt_basic.h + ./include/pybind11_qt/pybind11_qt_containers.h + ./include/pybind11_qt/pybind11_qt_enums.h + ./include/pybind11_qt/pybind11_qt_holder.h + ./include/pybind11_qt/pybind11_qt_objects.h + ./include/pybind11_qt/pybind11_qt_qflags.h + ./include/pybind11_qt/pybind11_qt.h + + pybind11_qt_basic.cpp + pybind11_qt_sip.cpp + pybind11_qt_utils.cpp + +) +mo2_target_sources(pybind11-qt + FOLDER src/details + PRIVATE + ./include/pybind11_qt/details/pybind11_qt_enum.h + ./include/pybind11_qt/details/pybind11_qt_qlist.h + ./include/pybind11_qt/details/pybind11_qt_qmap.h + ./include/pybind11_qt/details/pybind11_qt_sip.h + ./include/pybind11_qt/details/pybind11_qt_utils.h +) +target_link_libraries(pybind11-qt PUBLIC pybind11::pybind11 PRIVATE Qt6::Core Qt6::Widgets) target_include_directories(pybind11-qt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # this is kind of broken but it only works with this... target_compile_definitions(pybind11-qt PUBLIC QT_NO_KEYWORDS) +# we need sip.h for pybind11-qt +add_custom_target(PyQt6-siph DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/sip.h") +set_target_properties(PyQt6-siph PROPERTIES FOLDER autogen) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/sip.h" + COMMAND + ${CMAKE_COMMAND} -E env PYTHONPATH=${MO2_PYLIBS_DIR} + ${MO2_PYLIBS_DIR}/bin/sip-module.exe + --sip-h PyQt${MO2_QT_VERSION_MAJOR}.sip + --target-dir ${CMAKE_CURRENT_BINARY_DIR} +) +add_dependencies(PyQt6-siph PyQt6) +add_dependencies(pybind11-qt PyQt6-siph) + +target_include_directories(pybind11-qt PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + add_library(pybind11::qt ALIAS pybind11-qt) diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h index 2996c4b..556a980 100644 --- a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -4,21 +4,37 @@ #include #include -#include #include #include #include "../pybind11_qt_holder.h" +struct _sipTypeDef; +typedef struct _sipTypeDef sipTypeDef; + +struct _sipSimpleWrapper; +typedef struct _sipSimpleWrapper sipSimpleWrapper; + +struct _sipWrapper; +typedef struct _sipWrapper sipWrapper; + namespace pybind11::detail::qt { - /** - * @brief Retrieve the SIP api. - * - * @return const sipAPIDef* - */ - const sipAPIDef* sipAPI(); + // helper functions to avoid bringing in this header + namespace sip { + + // extract the underlying data if present from the equivalent PyQt object + void* extract_data(PyObject*); + + const sipTypeDef* api_find_type(const char* type); + int api_can_convert_to_type(PyObject* pyObj, const sipTypeDef* td, int flags); + + void api_transfer_to(PyObject* self, PyObject* owner); + void api_transfer_back(PyObject* self); + PyObject* api_convert_from_type(void* cpp, const sipTypeDef* td, + PyObject* transferObj); + } // namespace sip template struct MetaData; @@ -91,12 +107,11 @@ namespace pybind11::detail::qt { } } - const sipTypeDef* type = - qt::sipAPI()->api_find_type(MetaData::class_name); + const auto* type = sip::api_find_type(MetaData::class_name); if (type == nullptr) { return false; } - if (!qt::sipAPI()->api_can_convert_to_type(src.ptr(), type, 0)) { + if (!sip::api_can_convert_to_type(src.ptr(), type, 0)) { return false; } @@ -107,20 +122,14 @@ namespace pybind11::detail::qt { // // sipAPI()->api_transfer_to(objPtr, Py_None); // - void* data = nullptr; - if (PyObject_TypeCheck(src.ptr(), qt::sipAPI()->api_simplewrapper_type)) { - data = reinterpret_cast(src.ptr())->data; - } - else if (PyObject_TypeCheck(src.ptr(), qt::sipAPI()->api_wrapper_type)) { - data = reinterpret_cast(src.ptr())->super.data; - } + void* const data = sip::extract_data(src.ptr()); if (data) { if constexpr (is_pointer) { value = reinterpret_cast(data); // transfer ownership - sipAPI()->api_transfer_to(src.ptr(), Py_None); + sip::api_transfer_to(src.ptr(), Py_None); // tie the py::object to the C++ one new pybind11::detail::qt::qobject_holder_impl(value); @@ -164,8 +173,7 @@ namespace pybind11::detail::qt { } } - const sipTypeDef* type = - qt::sipAPI()->api_find_type(MetaData::class_name); + const sipTypeDef* type = sip::api_find_type(MetaData::class_name); if (type == nullptr) { return Py_None; } @@ -188,7 +196,7 @@ namespace pybind11::detail::qt { sipData = &src; } - sipObj = qt::sipAPI()->api_convert_from_type(sipData, type, 0); + sipObj = sip::api_convert_from_type(sipData, type, 0); if (sipObj == nullptr) { return Py_None; @@ -196,11 +204,11 @@ namespace pybind11::detail::qt { // ensure Python deletes the C++ component if constexpr (!is_pointer) { - qt::sipAPI()->api_transfer_back(sipObj); + sip::api_transfer_back(sipObj); } else { if (policy == return_value_policy::take_ownership) { - qt::sipAPI()->api_transfer_back(sipObj); + sip::api_transfer_back(sipObj); } } diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h index 3edba59..e803e9f 100644 --- a/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h @@ -59,7 +59,7 @@ namespace pybind11::qt { * simply return the PyQtX object as a QClass* object, while __getattr__ * will delegate to the underlying QClass object when required. * - * This allow access to Qt interface for object exposed using boost::python + * This allow access to Qt interface for object exposed using pybind11 * (e.g., signals, methods from QObject or QWidget, etc.). * * @param pyclass Python class to define the methods on. diff --git a/src/pybind11-qt/pybind11_qt_basic.cpp b/src/pybind11-qt/pybind11_qt_basic.cpp index d82ec2b..6a0f117 100644 --- a/src/pybind11-qt/pybind11_qt_basic.cpp +++ b/src/pybind11-qt/pybind11_qt_basic.cpp @@ -2,12 +2,17 @@ #include +#include +#include +#include + #include #include "pybind11_qt/details/pybind11_qt_utils.h" // need to import containers to get QVariantList and QVariantMap #include "pybind11_qt/pybind11_qt_containers.h" +#include "pybind11_qt/pybind11_qt_objects.h" namespace pybind11::detail { @@ -81,7 +86,18 @@ namespace pybind11::detail { 2 * src.length(), nullptr, 0); } - bool type_caster::load(handle src, bool) + template + bool tryCast(QVariant& value, handle src, bool implicit) + { + type_caster caster; + if (caster.load(src, implicit)) { + value = caster.value; + return true; + } + return false; + } + + bool type_caster::load(handle src, bool implicit) { // test for string first otherwise PyList_Check also works if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { @@ -119,6 +135,15 @@ namespace pybind11::detail { value = src.cast(); return true; } + else if (PyFloat_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (tryCast(value, src, implicit) || + tryCast(value, src, implicit) || + tryCast(value, src, implicit)) { + return true; + } else { return false; } @@ -131,13 +156,25 @@ namespace pybind11::detail { case QMetaType::UnknownType: return Py_None; case QMetaType::Int: + case QMetaType::Long: return PyLong_FromLong(var.toInt()); + case QMetaType::LongLong: + return PyLong_FromLongLong(var.toLongLong()); case QMetaType::UInt: + case QMetaType::ULong: 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: return PyBool_FromLong(var.toBool()); + case QMetaType::QString: return type_caster::cast(var.toString(), policy, parent); + // We need to check for StringList here because these are not considered // List since List is QList will StringList is QList: case QMetaType::QStringList: @@ -146,6 +183,16 @@ namespace pybind11::detail { return type_caster::cast(var.toList(), policy, parent); case QMetaType::QVariantMap: return type_caster::cast(var.toMap(), policy, parent); + + case QMetaType::QColor: + return type_caster::cast(var.value(), policy, parent); + + case QMetaType::QIcon: + return type_caster::cast(var.value(), policy, parent); + + case QMetaType::QPixmap: + return type_caster::cast(var.value(), policy, parent); + default: { PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.userType()); throw pybind11::error_already_set(); diff --git a/src/pybind11-qt/pybind11_qt_sip.cpp b/src/pybind11-qt/pybind11_qt_sip.cpp index e3e0a1f..69ad4b8 100644 --- a/src/pybind11-qt/pybind11_qt_sip.cpp +++ b/src/pybind11-qt/pybind11_qt_sip.cpp @@ -4,6 +4,8 @@ #include +#include + namespace py = pybind11; namespace pybind11::detail::qt { @@ -61,4 +63,43 @@ namespace pybind11::detail::qt { return sipApi; } + namespace sip { + const sipTypeDef* api_find_type(const char* type) + { + return sipAPI()->api_find_type(type); + } + + int api_can_convert_to_type(PyObject* pyObj, const sipTypeDef* td, int flags) + { + return sipAPI()->api_can_convert_to_type(pyObj, td, flags); + } + + void api_transfer_to(PyObject* self, PyObject* owner) + { + sipAPI()->api_transfer_to(self, owner); + } + + void api_transfer_back(PyObject* self) + { + sipAPI()->api_transfer_back(self); + } + + PyObject* api_convert_from_type(void* cpp, const sipTypeDef* td, PyObject*) + { + return sipAPI()->api_convert_from_type(cpp, td, 0); + } + + void* extract_data(PyObject* ptr) + { + if (PyObject_TypeCheck(ptr, sipAPI()->api_simplewrapper_type)) { + return reinterpret_cast(ptr)->data; + } + else if (PyObject_TypeCheck(ptr, sipAPI()->api_wrapper_type)) { + return reinterpret_cast(ptr)->super.data; + } + return nullptr; + } + + } // namespace sip + } // namespace pybind11::detail::qt diff --git a/src/pybind11-utils/CMakeLists.txt b/src/pybind11-utils/CMakeLists.txt index 4b2c170..372a9b7 100644 --- a/src/pybind11-utils/CMakeLists.txt +++ b/src/pybind11-utils/CMakeLists.txt @@ -1,13 +1,21 @@ cmake_minimum_required(VERSION 3.16) -add_library(pybind11-utils STATIC) -mo2_configure_library(pybind11-utils - SOURCE_TREE +add_library(pybind11-utils STATIC + ./include/pybind11_utils/functional.h + ./include/pybind11_utils/shared_cpp_owner.h + ./include/pybind11_utils/smart_variant_wrapper.h + ./include/pybind11_utils/smart_variant.h + + functional.cpp +) +mo2_configure_target(pybind11-utils + NO_SOURCES WARNINGS 4 EXTERNAL_WARNINGS 4 AUTOMOC OFF TRANSLATIONS OFF ) +mo2_default_source_group() target_link_libraries(pybind11-utils PUBLIC pybind11::pybind11) target_include_directories(pybind11-utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/src/runner/CMakeLists.txt b/src/runner/CMakeLists.txt index 9457ab6..cd16884 100644 --- a/src/runner/CMakeLists.txt +++ b/src/runner/CMakeLists.txt @@ -1,19 +1,28 @@ cmake_minimum_required(VERSION 3.16) -add_library(runner SHARED) -mo2_configure_library(runner - SOURCE_TREE +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(runner SHARED + error.h + pythonrunner.cpp + pythonrunner.h + pythonutils.h + pythonutils.cpp +) +mo2_configure_target(runner + NO_SOURCES WARNINGS 4 EXTERNAL_WARNINGS 4 AUTOMOC ON TRANSLATIONS OFF - PUBLIC_DEPENDS uibase Qt::Core ) -target_link_libraries(runner PRIVATE pybind11::embed pybind11::qt) +mo2_default_source_group() +target_link_libraries(runner PUBLIC mo2::uibase PRIVATE pybind11::embed pybind11::qt) target_include_directories(runner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(runner PRIVATE RUNNER_BUILD) -# proxy will install runner +# proxy will install runner but we install the PDB +install(FILES $ DESTINATION pdb) # force runner to build mobase add_dependencies(runner mobase) diff --git a/src/runner/error.h b/src/runner/error.h index d38c8af..341c7ea 100644 --- a/src/runner/error.h +++ b/src/runner/error.h @@ -7,7 +7,7 @@ #include -#include +#include namespace pyexcept { diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 35b2216..006214d 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -16,8 +16,8 @@ #include #include -#include -#include +#include +#include #include "error.h" #include "pythonutils.h" @@ -36,24 +36,14 @@ namespace mo2::python { PythonRunner() = default; ~PythonRunner() = default; - QList load(const QString& identifier) override; - void unload(const QString& identifier) override; + QList> load(std::string_view moduleName, + std::filesystem::path const& modulePath) override; + void unload(std::string_view moduleName, + std::filesystem::path const& modulePath) override; bool initialize(std::vector const& pythonPaths) override; void addDllSearchPath(std::filesystem::path const& dllPath) 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> m_PythonObjects; }; std::unique_ptr createPythonRunner() @@ -162,72 +152,44 @@ namespace mo2::python { py::module_::import("os").attr("add_dll_directory")(absolute(dllPath)); } - void PythonRunner::ensureFolderInPath(QString folder) - { - 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(); - if (!currentPath.contains(folder, Qt::CaseInsensitive)) { - sysPath.insert(0, folder); - } - } - - QList PythonRunner::load(const QString& identifier) + QList> PythonRunner::load(std::string_view name, + const std::filesystem::path& pythonModule) { 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 { - // dictionary that will contain createPlugin() or createPlugins(). - py::dict moduleDict; + // some needed import + 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; + // check if the module is already loaded + py::dict modules = sys.attr("modules"); + py::module_ pymodule; + if (modules.contains(name)) { + pymodule = modules[py::str(name)]; + pymodule.reload(); } 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 - py::dict modules = py::module_::import("sys").attr("modules"); - if (modules.contains(moduleName)) { - 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__"); + // load the module + 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) { - MOBase::log::error("No plugins found in {}.", identifier); + MOBase::log::error("no plugins found in {}", pythonModule); return {}; } @@ -240,8 +202,8 @@ namespace mo2::python { else if (moduleDict.contains("createPlugins")) { py::object pyPlugins = moduleDict["createPlugins"](); if (!py::isinstance(pyPlugins)) { - MOBase::log::error( - "Plugin {}: createPlugins must return a sequence.", identifier); + MOBase::log::error("{}: createPlugins must return a sequence", + pythonModule); } else { py::sequence pyList(pyPlugins); @@ -252,30 +214,26 @@ namespace mo2::python { } } else { - MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", - identifier); + MOBase::log::error("{}: missing createPlugin(s) function", + pythonModule); } - // If we have no plugins, there was an issue, and we already logged the - // problem: + // if we have no plugins, there was an issue, and we already logged the + // problem if (plugins.empty()) { - return QList(); + return {}; } - QList allInterfaceList; + QList> allInterfaceList; for (py::object pluginObj : plugins) { - - // save to be able to unload it - m_PythonObjects[identifier].push_back(pluginObj); - QList interfaceList = py::module_::import("mobase.private") .attr("extract_plugins")(pluginObj) .cast>(); if (interfaceList.isEmpty()) { - MOBase::log::error("Plugin {}: no plugin interface implemented.", - identifier); + MOBase::log::error("{}: no plugin interface implemented.", + pythonModule); } // Append the plugins to the main list: @@ -285,57 +243,47 @@ namespace mo2::python { return allInterfaceList; } 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); } } - 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; - - if (!identifier.endsWith(".py")) { - - // At this point, the identifier is the full path to the module. - QDir folder(identifier); - - // 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(); - - 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(), mpath, identifier); - - PyDict_DelItem(modules.ptr(), keys[i].ptr()); - } - } - } - } + py::gil_scoped_acquire lock; + + // at this point, the identifier is the full path to the module. + QDir folder(modulePath); - // Boost.Python does not handle cyclic garbace collection, so we need to - // release everything hold by the objects before deleting the objects - // themselves (done when erasing from m_PythonObjects). - for (auto& obj : it->second) { - obj.attr("__dict__").attr("clear")(); + // 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(); + + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // if the path is under identifier, we need to unload it + log::debug("unloading module {} from {}", + keys[i].cast(), mpath); + + PyDict_DelItem(modules.ptr(), keys[i].ptr()); + } } + } - log::debug("Deleting {} python objects for {}.", it->second.size(), - identifier); - m_PythonObjects.erase(it); + // for simple Python file - not really used anymore, but actually used in + // testing - we need to remove using the module name + // + py::str pyModuleName(moduleName); + if (modules.contains(pyModuleName)) { + PyDict_DelItem(modules.ptr(), pyModuleName.ptr()); } } diff --git a/src/runner/pythonrunner.h b/src/runner/pythonrunner.h index 5f9751b..ad3a595 100644 --- a/src/runner/pythonrunner.h +++ b/src/runner/pythonrunner.h @@ -9,6 +9,8 @@ #include #include +#include + #ifdef RUNNER_BUILD #define RUNNER_DLL_EXPORT Q_DECL_EXPORT #else @@ -21,8 +23,10 @@ namespace mo2::python { // class IPythonRunner { public: - virtual QList load(const QString& identifier) = 0; - virtual void unload(const QString& identifier) = 0; + virtual QList> + 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 // diff --git a/src/runner/pythonutils.cpp b/src/runner/pythonutils.cpp index 9cc3a75..c94a50b 100644 --- a/src/runner/pythonutils.cpp +++ b/src/runner/pythonutils.cpp @@ -7,7 +7,7 @@ #include #include -#include "log.h" +#include namespace py = pybind11; @@ -123,7 +123,7 @@ namespace mo2::python { void configure_python_logging(py::module_ mobase) { // most of this is dealing with actual Python objects since it is not - // possible to derive from logging.Handler in C++ using Boost.Python, + // possible to derive from logging.Handler in C++ using pybind11, // and since a lot of this would require extra register only for this. // see also diff --git a/tests/mocks/DummyFileTree.h b/tests/mocks/DummyFileTree.h index 3f1cb01..cf81691 100644 --- a/tests/mocks/DummyFileTree.h +++ b/tests/mocks/DummyFileTree.h @@ -1,7 +1,7 @@ #ifndef DUMMY_TREE_H #define DUMMY_TREE_H -#include +#include // filetree implementation for testing purpose // @@ -19,8 +19,8 @@ class DummyFileTree : public MOBase::IFileTree { return std::make_shared(parent, name); } - bool doPopulate(std::shared_ptr parent, - std::vector>& entries) const override + bool doPopulate(std::shared_ptr, + std::vector>&) const override { return true; } diff --git a/tests/mocks/MockOrganizer.h b/tests/mocks/MockOrganizer.h index 5ef98e8..8f9997e 100644 --- a/tests/mocks/MockOrganizer.h +++ b/tests/mocks/MockOrganizer.h @@ -1,5 +1,5 @@ -#include "imoinfo.h" #include +#include using namespace MOBase; @@ -14,6 +14,7 @@ class MockOrganizer : public IOrganizer { MOCK_METHOD(QString, basePath, (), (const, override)); MOCK_METHOD(QString, modsPath, (), (const, override)); MOCK_METHOD(VersionInfo, appVersion, (), (const, override)); + MOCK_METHOD(Version, version, (), (const, override)); MOCK_METHOD(IModInterface*, createMod, (GuessedValue &name), (override)); MOCK_METHOD(IPluginGame*, getGame, (const QString &gameName), (const, override)); MOCK_METHOD(void, modDataChanged, (IModInterface *mod), (override)); @@ -34,6 +35,7 @@ class MockOrganizer : public IOrganizer { MOCK_METHOD(std::shared_ptr, virtualFileTree, (), (const, override)); MOCK_METHOD(MOBase::IDownloadManager*, downloadManager, (), (const, override)); MOCK_METHOD(MOBase::IPluginList*, pluginList, (), (const, override)); + MOCK_METHOD(MOBase::IExtensionList&, extensionList, (), (const, override)); MOCK_METHOD(MOBase::IModList*, modList, (), (const, override)); MOCK_METHOD(MOBase::IProfile*, profile, (), (const, override)); MOCK_METHOD(MOBase::IGameFeatures*, gameFeatures, (), (const, override)); diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 3e80a54..f91af55 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -2,17 +2,28 @@ cmake_minimum_required(VERSION 3.16) # pytest cmake_policy(SET CMP0144 NEW) + +find_package(mo2-uibase CONFIG REQUIRED) find_package(GTest REQUIRED) set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) -if (TARGET uibase) - set(UIBASE_PATH $) -else() - set(UIBASE_PATH "${MO2_INSTALL_PATH}/bin") -endif() +set(UIBASE_PATH $) add_custom_target(python-tests) +target_sources(python-tests + PRIVATE + conftest.py + test_argument_wrapper.py + test_filetree.py + test_functional.py + test_guessed_string.py + test_organizer.py + test_path_wrappers.py + test_qt_widgets.py + test_qt.py + test_shared_cpp_owner.py +) add_test(NAME pytest COMMAND ${CMAKE_CURRENT_BINARY_DIR}/pylibs/bin/pytest.exe ${CMAKE_CURRENT_SOURCE_DIR} -s @@ -21,18 +32,16 @@ add_test(NAME pytest set_tests_properties(pytest PROPERTIES DEPENDS python-tests - WORKING_DIRECTORY ${MO2_INSTALL_PATH}/bin ENVIRONMENT_MODIFICATION "PYTHONPATH=set:${PYLIB_DIR}\\;$;\ -UIBASE_PATH=set:${UIBASE_PATH};\ -QT_ROOT=set:${QT_ROOT}" +UIBASE_PATH=set:${UIBASE_PATH}" ) mo2_python_pip_install(python-tests DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs PACKAGES pytest - PyQt${QT_MAJOR_VERSION}==${QT_VERSION} - PyQt${QT_MAJOR_VERSION}-Qt${QT_MAJOR_VERSION}==${QT_VERSION}) + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION} + PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION}) add_dependencies(python-tests mobase) set_target_properties(python-tests PROPERTIES FOLDER tests/python) @@ -44,6 +53,7 @@ foreach (test_file ${test_files}) pybind11_add_module(${target} EXCLUDE_FROM_ALL THIN_LTO ${test_file}) set_target_properties(${target} PROPERTIES + CXX_STANDARD 20 OUTPUT_NAME ${pymodule} FOLDER tests/python LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests") @@ -56,8 +66,8 @@ foreach (test_file ${test_files}) endforeach() endif() - mo2_add_dependencies(${target} PRIVATE uibase Qt::Core Qt::Widgets) - target_link_libraries(${target} PRIVATE pybind11::qt pybind11::utils GTest::gmock) + target_link_libraries(${target} PRIVATE + mo2::uibase Qt6::Core Qt6::Widgets pybind11::qt pybind11::utils GTest::gmock) target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) diff --git a/tests/python/conftest.py b/tests/python/conftest.py index 34ca988..99e7800 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -1,15 +1,12 @@ import os import sys -from pathlib import Path -from typing import cast + +from PyQt6.QtWidgets import QApplication def pytest_configure(): global app - os.add_dll_directory(str(Path(cast(str, os.getenv("QT_ROOT"))).joinpath("bin"))) os.add_dll_directory(str(os.getenv("UIBASE_PATH"))) - from PyQt6.QtWidgets import QApplication - app = QApplication(sys.argv) diff --git a/tests/python/test_filetree.cpp b/tests/python/test_filetree.cpp index 882a0b4..cce43cf 100644 --- a/tests/python/test_filetree.cpp +++ b/tests/python/test_filetree.cpp @@ -3,7 +3,7 @@ #include #include -#include "ifiletree.h" +#include using namespace MOBase; diff --git a/tests/python/test_guessed_string.cpp b/tests/python/test_guessed_string.cpp index 4256989..5e76773 100644 --- a/tests/python/test_guessed_string.cpp +++ b/tests/python/test_guessed_string.cpp @@ -3,7 +3,7 @@ #include #include -#include "guessedvalue.h" +#include using namespace MOBase; diff --git a/tests/python/test_organizer.cpp b/tests/python/test_organizer.cpp index 3f1878d..ea54c0d 100644 --- a/tests/python/test_organizer.cpp +++ b/tests/python/test_organizer.cpp @@ -17,32 +17,30 @@ PYBIND11_MODULE(organizer, m) using ::testing::Eq; using ::testing::Return; - m.def( - "organizer", - []() -> IOrganizer* { - MockOrganizer* mock = new NiceMock(); - ON_CALL(*mock, profileName).WillByDefault([&mock]() { - return "profile"; + m.def("organizer", []() -> IOrganizer* { + MockOrganizer* mock = new NiceMock(); + ON_CALL(*mock, profileName).WillByDefault([&mock]() { + return "profile"; + }); + + const auto handle = (HANDLE)std::uintptr_t{4654}; + ON_CALL(*mock, startApplication) + .WillByDefault([handle](const auto& name, auto&&... args) { + return name == "valid.exe" ? handle : INVALID_HANDLE_VALUE; + }); + ON_CALL(*mock, waitForApplication) + .WillByDefault([&mock, original_handle = handle](HANDLE handle, bool, + LPDWORD exitCode) { + if (handle == original_handle) { + *exitCode = 0; + return true; + } + else { + *exitCode = static_cast(-1); + return false; + } }); - const auto handle = (HANDLE)std::uintptr_t{4654}; - EXPECT_CALL(*mock, startApplication(Eq("valid.exe"), _, _, _, _, _)) - .WillRepeatedly(Return(handle)); - EXPECT_CALL(*mock, startApplication(Eq("invalid.exe"), _, _, _, _, _)) - .WillRepeatedly(Return(INVALID_HANDLE_VALUE)); - ON_CALL(*mock, waitForApplication) - .WillByDefault([&mock, original_handle = handle]( - HANDLE handle, bool refresh, LPDWORD exitCode) { - if (handle == original_handle) { - *exitCode = 0; - return true; - } - else { - *exitCode = -1; - return false; - } - }); - - return mock; - }, - py::return_value_policy::take_ownership); + + return mock; + }); } diff --git a/tests/python/test_qt.cpp b/tests/python/test_qt.cpp index 35b1785..b2f9064 100644 --- a/tests/python/test_qt.cpp +++ b/tests/python/test_qt.cpp @@ -33,6 +33,14 @@ PYBIND11_MODULE(qt, m) return QString::number(value); }); + // QByteArray + m.def("create_qbytearray_from_raw", [](const char* raw) { + return QByteArray(raw); + }); + m.def("get_qbytearray_length", [](const QByteArray& data) { + return data.size(); + }); + // QStringList m.def("qstringlist_join", [](QStringList const& values, QString const& sep) { @@ -72,24 +80,26 @@ PYBIND11_MODULE(qt, m) // QVariant m.def("qvariant_from_none", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::Invalid, + return std::make_tuple(variant.userType() == QMetaType::UnknownType, variant.isValid()); }); m.def("qvariant_from_int", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::Int, variant.toInt()); + return std::make_tuple(variant.userType() == QMetaType::Int, variant.toInt()); }); m.def("qvariant_from_bool", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::Bool, variant.toBool()); + return std::make_tuple(variant.userType() == QMetaType::Bool, variant.toBool()); }); m.def("qvariant_from_str", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::String, + return std::make_tuple(variant.userType() == QMetaType::QString, variant.toString()); }); m.def("qvariant_from_list", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::List, variant.toList()); + return std::make_tuple(variant.userType() == QMetaType::QVariantList, + variant.toList()); }); m.def("qvariant_from_map", [](QVariant const& variant) { - return std::make_tuple(variant.userType() == QVariant::Map, variant.toMap()); + return std::make_tuple(variant.userType() == QMetaType::QVariantMap, + variant.toMap()); }); m.def("qvariant_none", []() { diff --git a/tests/python/test_qt.py b/tests/python/test_qt.py index 991e33c..fd5dca4 100644 --- a/tests/python/test_qt.py +++ b/tests/python/test_qt.py @@ -1,4 +1,4 @@ -from PyQt6.QtCore import QDateTime, Qt +from PyQt6.QtCore import QByteArray, QDateTime, Qt import mobase_tests.qt as m @@ -23,6 +23,12 @@ def test_qstring(): assert m.consume_qstring_with_emoji("🌎") == 2 +def test_qbytearray(): + arr = m.create_qbytearray_from_raw("hello world!") + assert isinstance(arr, QByteArray) + assert m.get_qbytearray_length(arr) == 12 + + def test_qstringlist(): assert m.qstringlist_join([""], "--") == "" assert m.qstringlist_join(["a", "b"], "") == "ab" diff --git a/tests/runner/CMakeLists.txt b/tests/runner/CMakeLists.txt index 800b593..fb424c7 100644 --- a/tests/runner/CMakeLists.txt +++ b/tests/runner/CMakeLists.txt @@ -5,8 +5,33 @@ cmake_minimum_required(VERSION 3.22) # first we configure the tests as with other tests add_executable(runner-tests EXCLUDE_FROM_ALL) - -mo2_configure_tests(runner-tests WARNINGS OFF) +mo2_default_source_group() +mo2_target_sources(runner-tests + FOLDER src + PRIVATE + test_diagnose.cpp + test_filemapper.cpp + test_game.cpp + test_installer.cpp + test_iplugin.cpp + test_lifetime.cpp +) +mo2_target_sources(runner-tests + FOLDER src/mocks + PRIVATE + ../mocks/DummyFileTree.h + ../mocks/MockOrganizer.h +) +mo2_target_sources(runner-tests + FOLDER src/plugins + PRIVATE + plugins/dummy-diagnose.py + plugins/dummy-filemapper.py + plugins/dummy-game.py + plugins/dummy-installer.py + plugins/dummy-iplugin.py +) +mo2_configure_tests(runner-tests NO_SOURCES WARNINGS 4) set_target_properties(runner-tests PROPERTIES FOLDER tests/runner) @@ -25,16 +50,14 @@ target_include_directories(runner-tests set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) mo2_python_pip_install(runner-tests DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs - PACKAGES - pytest - PyQt${QT_MAJOR_VERSION}==${QT_VERSION} - PyQt${QT_MAJOR_VERSION}-Qt${QT_MAJOR_VERSION}==${QT_VERSION}) + PACKAGES + pytest + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION} + PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION}) add_dependencies(runner-tests mobase) -set(pythoncore "${PYTHON_ROOT}/PCbuild/amd64/pythoncore") -set(pythoncorezip "${pythoncore}/python${Python_VERSION_SHORT}.zip") -set(PYTHONPATH "${PYLIB_DIR}\\;$\\;${pythoncore}\\;${pythoncorezip}") +set(PYTHONPATH "${PYLIB_DIR}\\;$\\;${Python_DLL_DIR}\\;${Python_LIB_DIR}") set_tests_properties(${runner-tests_gtests} PROPERTIES diff --git a/tests/runner/plugins/dummy-iplugin.py b/tests/runner/plugins/dummy-iplugin.py index eaaf6c0..693b15c 100644 --- a/tests/runner/plugins/dummy-iplugin.py +++ b/tests/runner/plugins/dummy-iplugin.py @@ -20,7 +20,7 @@ def version(self) -> mobase.VersionInfo: def settings(self) -> list[mobase.PluginSetting]: return [ mobase.PluginSetting( - "a setting", "the setting description", default_value=12 + "a_setting", "A Setting", "the setting description", default_value=12 ) ] diff --git a/tests/runner/test_diagnose.cpp b/tests/runner/test_diagnose.cpp index b0a6152..a1c8872 100644 --- a/tests/runner/test_diagnose.cpp +++ b/tests/runner/test_diagnose.cpp @@ -5,9 +5,10 @@ #include +#include +#include + #include "MockOrganizer.h" -#include "iplugindiagnose.h" -#include "iplugingame.h" using namespace MOBase; @@ -15,19 +16,20 @@ using ::testing::ElementsAre; TEST(IPluginDiagnose, Simple) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); // load objects - const auto objects = runner->load(plugins_folder + "/dummy-diagnose.py"); - EXPECT_EQ(objects.size(), 3); + const auto objects = + runner->load("dummy_diagnose", plugins_folder / "dummy-diagnose.py"); + ASSERT_EQ(objects.size(), 2); // load the first IPluginDiagnose { - IPluginDiagnose* plugin = qobject_cast(objects[0]); - EXPECT_NE(plugin, nullptr); + IPluginDiagnose* plugin = qobject_cast(objects[0][0]); + ASSERT_NE(plugin, nullptr); ASSERT_THAT(plugin->activeProblems(), ElementsAre(1, 2)); EXPECT_EQ(plugin->shortDescription(1), "short-1"); @@ -40,8 +42,8 @@ TEST(IPluginDiagnose, Simple) // load the second one (this is cast before IPluginGame so should be before) { - IPluginDiagnose* plugin = qobject_cast(objects[1]); - EXPECT_NE(plugin, nullptr); + IPluginDiagnose* plugin = qobject_cast(objects[1][0]); + ASSERT_NE(plugin, nullptr); ASSERT_THAT(plugin->activeProblems(), ElementsAre(5, 7)); EXPECT_EQ(plugin->shortDescription(5), "short-5"); @@ -54,7 +56,7 @@ TEST(IPluginDiagnose, Simple) // load the game plugin { - IPluginGame* plugin = qobject_cast(objects[2]); - EXPECT_NE(plugin, nullptr); + IPluginGame* plugin = qobject_cast(objects[1][1]); + ASSERT_NE(plugin, nullptr); } } diff --git a/tests/runner/test_filemapper.cpp b/tests/runner/test_filemapper.cpp index 7591177..29e6c03 100644 --- a/tests/runner/test_filemapper.cpp +++ b/tests/runner/test_filemapper.cpp @@ -5,30 +5,33 @@ #include +#include +#include + #include "MockOrganizer.h" -#include "ipluginfilemapper.h" -#include "iplugingame.h" using namespace MOBase; TEST(IPluginFileMapper, Simple) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); // load objects - const auto objects = runner->load(plugins_folder + "/dummy-filemapper.py"); - EXPECT_EQ(objects.size(), 3); + const auto objects = + runner->load("dummy_filemapper", plugins_folder / "dummy-filemapper.py"); + EXPECT_EQ(objects.size(), 2); // load the first IPluginFileMapper { - IPluginFileMapper* plugin = qobject_cast(objects[0]); - EXPECT_NE(plugin, nullptr); + ASSERT_EQ(objects[0].size(), 1); + IPluginFileMapper* plugin = qobject_cast(objects[0][0]); + ASSERT_NE(plugin, nullptr); const auto m = plugin->mappings(); - EXPECT_EQ(m.size(), 2); + ASSERT_EQ(m.size(), 2); EXPECT_EQ(m[0].source, "the source"); EXPECT_EQ(m[0].destination, "the destination"); @@ -43,11 +46,12 @@ TEST(IPluginFileMapper, Simple) // load the second one (this is cast before IPluginGame so should be before) { - IPluginFileMapper* plugin = qobject_cast(objects[1]); - EXPECT_NE(plugin, nullptr); + ASSERT_EQ(objects[1].size(), 2); + IPluginFileMapper* plugin = qobject_cast(objects[1][0]); + ASSERT_NE(plugin, nullptr); const auto m = plugin->mappings(); - EXPECT_EQ(m.size(), 1); + ASSERT_EQ(m.size(), 1); EXPECT_EQ(m[0].source, "the source"); EXPECT_EQ(m[0].destination, "the destination"); @@ -57,7 +61,7 @@ TEST(IPluginFileMapper, Simple) // load the game plugin { - IPluginGame* plugin = qobject_cast(objects[2]); - EXPECT_NE(plugin, nullptr); + IPluginGame* plugin = qobject_cast(objects[1][1]); + ASSERT_NE(plugin, nullptr); } } diff --git a/tests/runner/test_game.cpp b/tests/runner/test_game.cpp index 7e96d42..d651a57 100644 --- a/tests/runner/test_game.cpp +++ b/tests/runner/test_game.cpp @@ -5,23 +5,24 @@ #include +#include + #include "MockOrganizer.h" -#include "iplugingame.h" using namespace MOBase; TEST(IPluginGame, Simple) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); // load objects - const auto objects = runner->load(plugins_folder + "/dummy-game.py"); + const auto objects = runner->load("dummy_game", plugins_folder / "dummy-game.py"); EXPECT_EQ(objects.size(), 1); // load the IPlugin - IPluginGame* plugin = qobject_cast(objects[0]); + IPluginGame* plugin = qobject_cast(objects[0][0]); EXPECT_NE(plugin, nullptr); } diff --git a/tests/runner/test_installer.cpp b/tests/runner/test_installer.cpp index a96ddf6..ba0a2d8 100644 --- a/tests/runner/test_installer.cpp +++ b/tests/runner/test_installer.cpp @@ -1,8 +1,9 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include + #include "MockOrganizer.h" -#include "iplugininstallersimple.h" #include "pythonrunner.h" #include @@ -13,17 +14,19 @@ using namespace MOBase; TEST(IPluginInstaller, Simple) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); // load objects - const auto objects = runner->load(plugins_folder + "/dummy-installer.py"); + const auto objects = + runner->load("dummy_installer", plugins_folder / "dummy-installer.py"); EXPECT_EQ(objects.size(), 1); // load the IPlugin - IPluginInstallerSimple* plugin = qobject_cast(objects[0]); + IPluginInstallerSimple* plugin = + qobject_cast(objects[0][0]); EXPECT_NE(plugin, nullptr); // basic tests diff --git a/tests/runner/test_iplugin.cpp b/tests/runner/test_iplugin.cpp index cadfa11..dfcfab1 100644 --- a/tests/runner/test_iplugin.cpp +++ b/tests/runner/test_iplugin.cpp @@ -1,8 +1,9 @@ -#include "gmock/gmock.h" -#include "gtest/gtest.h" +#include +#include + +#include #include "MockOrganizer.h" -#include "iplugin.h" #include "pythonrunner.h" #include @@ -11,31 +12,30 @@ using namespace MOBase; TEST(IPlugin, Basic) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); // load objects - const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + const auto objects = + runner->load("dummy_iplugin", plugins_folder / "dummy-iplugin.py"); EXPECT_EQ(objects.size(), 1); // load the IPlugin - const IPlugin* plugin = qobject_cast(objects[0]); + const IPlugin* plugin = qobject_cast(objects[0][0]); EXPECT_NE(plugin, nullptr); - EXPECT_EQ(plugin->author(), "The Author"); EXPECT_EQ(plugin->name(), "The Name"); - EXPECT_EQ(plugin->version(), VersionInfo(1, 3, 0)); - EXPECT_EQ(plugin->description(), "The Description"); // settings const auto settings = plugin->settings(); EXPECT_EQ(settings.size(), 1); - EXPECT_EQ(settings[0].key, "a setting"); - EXPECT_EQ(settings[0].description, "the setting description"); - EXPECT_EQ(settings[0].defaultValue.userType(), QVariant::Type::Int); - EXPECT_EQ(settings[0].defaultValue.toInt(), 12); + EXPECT_EQ(settings[0].name(), "a_setting"); + EXPECT_EQ(settings[0].title(), "A Setting"); + EXPECT_EQ(settings[0].description(), "the setting description"); + EXPECT_EQ(settings[0].defaultValue().userType(), QMetaType::Type::Int); + EXPECT_EQ(settings[0].defaultValue().toInt(), 12); // no translation, no custom implementation -> name() EXPECT_EQ(plugin->localizedName(), "The Name"); diff --git a/tests/runner/test_lifetime.cpp b/tests/runner/test_lifetime.cpp index bf057d3..06bdeef 100644 --- a/tests/runner/test_lifetime.cpp +++ b/tests/runner/test_lifetime.cpp @@ -7,38 +7,42 @@ TEST(Lifetime, Plugins) { - const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + const auto plugins_folder = std::filesystem::path(std::getenv("PLUGIN_DIR")); auto runner = mo2::python::createPythonRunner(); runner->initialize(); { - const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + const auto objects = + runner->load("dummy_iplugin", plugins_folder / "dummy-iplugin.py"); // we found one plugin - EXPECT_EQ(objects.size(), 1); + ASSERT_EQ(objects.size(), 1); // check that deleting the object actually destroys it bool destroyed = false; - QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + QObject::connect(objects[0][0], &QObject::destroyed, [&destroyed]() { destroyed = true; }); - delete objects[0]; + delete objects[0][0]; EXPECT_EQ(destroyed, true); + + runner->unload("dummy_iplugin", plugins_folder / "dummy-iplugin.py"); } // same things but with a parent { QObject* dummy_parent = new QObject(); - const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + const auto objects = + runner->load("dummy_iplugin", plugins_folder / "dummy-iplugin.py"); // we found one plugin - EXPECT_EQ(objects.size(), 1); - objects[0]->setParent(dummy_parent); + ASSERT_EQ(objects.size(), 1); + objects[0][0]->setParent(dummy_parent); // check that deleting the object actually destroys it bool destroyed = false; - QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + QObject::connect(objects[0][0], &QObject::destroyed, [&destroyed]() { destroyed = true; }); delete dummy_parent; diff --git a/typings/mobase_tests/qt.pyi b/typings/mobase_tests/qt.pyi index 088ef2a..a10d574 100644 --- a/typings/mobase_tests/qt.pyi +++ b/typings/mobase_tests/qt.pyi @@ -10,6 +10,9 @@ __all__ = [ "SimpleEnum", "consume_qstring_with_emoji", "create_qstring_with_emoji", + "create_qbytearray_from_raw", + "get_qbytearray_length", + "qbytearray_to_longlong", "datetime_from_string", "datetime_to_string", "int_to_qstring", @@ -92,6 +95,9 @@ def qflags_explode(arg0: int) -> tuple[int, bool, bool, bool]: ... def qmap_to_length(arg0: dict[str, str]) -> dict[str, int]: ... def qstring_to_int(arg0: str) -> int: ... def qstring_to_stdstring(arg0: str) -> str: ... +def create_qbytearray_from_raw(arg0: str | bytes) -> PyQt6.QtCore.QByteArray: ... +def get_qbytearray_length(arg0: PyQt6.QtCore.QByteArray) -> int: ... +def qbytearray_to_longlong(arg0: PyQt6.QtCore.QByteArray) -> int: ... def qstringlist_at(arg0: typing.Sequence[str], arg1: int) -> str: ... def qstringlist_join(arg0: typing.Sequence[str], arg1: str) -> str: ... def qvariant_bool() -> MoVariant: ... diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..98c2075 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,34 @@ +{ + "dependencies": ["pybind11"], + "features": { + "testing": { + "description": "Build Plugin Python tests.", + "dependencies": ["gtest"] + }, + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake", "mo2-uibase"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "dc9d84b9442c5ca4b6b8f23380e42f777f2857e5", + "packages": ["mo2-*", "pybind11", "spdlog"] + } + ] + } +}