diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..c0403164 --- /dev/null +++ b/.clang-format @@ -0,0 +1,21 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 4 +--- +Language: Cpp +# Force pointers to the type for C++. +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -4 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: All +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Stroustrup +ColumnLimit: 88 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..6f8a3f91 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +1bffcfa8bfba93a9505692b11fdd3f0903983542 +7108d59267b0fe4e9442f9bb146ea5d70e7704a8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e613fd7a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf +*.py text eol=crlf diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..57e7475e --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,38 @@ +name: Build & Test Plugin Python + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened] + +env: + VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }} + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Configure Plugin Python + id: configure-plugin-python + uses: ModOrganizer2/build-with-mob-action@master + with: + mo2-dependencies: uibase + mo2-skip-build: true + + - name: Build Plugin Python + working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }} + run: cmake --build vsbuild --config RelWithDebInfo --verbose ` + --target python-tests --target runner-tests --target proxy + + - name: Test Plugin Python + working-directory: ${{ steps.configure-plugin-python.outputs.working-directory }} + 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 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000..70b5bcaa --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,24 @@ +name: Lint Plugin Python + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + - uses: abatilo/actions-poetry@v2 + - name: Check format Python tests + run: | + poetry install --no-root + poetry run poe lint diff --git a/.gitignore b/.gitignore index cf71be77..5fc3c138 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ +# build edit CMakeLists.txt.user /msbuild.log /*std*.log /*build + +# python +tests/**/__pycache__ + +# IDE +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..eb8969cd --- /dev/null +++ b/.pre-commit-config.yaml @@ -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: v21.1.8 + 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index b2e4c5d0..c55827ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,25 +1,46 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.16) -ADD_COMPILE_OPTIONS($<$:/MP> $<$:$<$:/O2>> $<$:$<$:/O2>>) +cmake_policy(SET CMP0144 NEW) -SET(PROJ_NAME plugin_python) +project(plugin_python CXX) -PROJECT(${PROJ_NAME}) +# 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(DEPENDENCIES_DIR CACHE PATH "") +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}) -# hint to find qt in dependencies path -LIST(APPEND CMAKE_PREFIX_PATH ${QT_ROOT}/lib/cmake) +set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL "") +set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO "") +set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE "") -# need to install python -IF(EXISTS "${PYTHON_ROOT}/PCbuild/python37.dll") - INSTALL(FILES ${PYTHON_ROOT}/PCbuild/python37.dll DESTINATION bin) - INSTALL(FILES ${PYTHON_ROOT}/PCbuild/python37.pdb DESTINATION pdb) -ELSEIF(EXISTS "${PYTHON_ROOT}/PCbuild/amd64/python27.dll") - INSTALL(FILES ${PYTHON_ROOT}/PCbuild/amd64/python37.dll DESTINATION bin) - INSTALL(FILES ${PYTHON_ROOT}/PCbuild/amd64/python37.pdb DESTINATION pdb) -ENDIF() +# 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} EXACT COMPONENTS Interpreter Development REQUIRED) +find_package(pybind11 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}) -ADD_SUBDIRECTORY(src/runner) -ADD_SUBDIRECTORY(src/proxy) +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}) + +# projects +add_subdirectory(src) + +# tests (if requested) +set(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for plugin_python") +if (BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..62939333 --- /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/README.md b/README.md new file mode 100644 index 00000000..90853609 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# ModOrganizer2 - Python Proxy + +This repository contains the Python proxy plugin for ModOrganizer2. +The proxy plugin allow developers to write Python plugins for ModOrganizer2. + +## Setup, build, tests + +This repository is part of MO2 main repositories and should usually be build using +[`mob`](https://github.com/ModOrganizer2/mob). + +## Organization + +This repositories contains 5 sub-projects in `src`. +The interface between Python and C++ is done using the +[`pybind11`](https://github.com/pybind/pybind11) library. +See the `README` in the subfolder (when there is one) for more details. + +- [`src/proxy`](src/proxy/) contains the actual proxy plugin. + This project is a simple interface between MO2 and the runner (see below). + The CMake code: + - generates the `plugin_python.dll` library, + - generates the translation file (under `src/`), + - installs necessary files for the plugin (Python DLL, Python libraries, etc), + including `mobase`. +- [`src/runner`](src/runner/) contains the Python runner. This is the project that + instantiates a Python interpreter and load/unload Python plugins. +- [`src/pybind11-qt`](src/pybind11-qt/) contains many utility stuff to interface + pybind11 with Qt and PyQt. +- [`src/pybind11-utils`](src/pybind11-utils/) contains some utility stuff pybind11. + This project is header-only. +- [`src/mobase`](src/mobase) contains the Python plugin interface. + - This projects generates the `mobase` Python library. + +Some (woefully incomplete) tests are available under `tests`, split in three +sub-directories: + +- [`tests/mocks`](tests/mocks/) simply contains mocks of `uibase` interfaces to be used + in the two other test projects. +- [`tests/python`](tests/python/) contains Python tests for `pytest`. + This project generates a bunch of Python test libraries that are then imported in + Python test files (`test_*.py`) and tested using `pytest`. + These tests mostly cover the pybind11 Qt and utility stuff, and some standalone + MO2 classes and functions (`IFileTree`, `GuessedString`, etc). +- [`tests/runner`](tests/runner/) contains C++ tests, using GTest + Tests in this project instantiate a Python runner and then use it to check that + plugins implemented in Python can be used properly on the C++ side. + +## Building & Running tests + +Tests are not built by default with `mob`, so you will need to run `cmake` manually +with the proper arguments. + +You need to define `PLUGIN_PYTHON_TESTS` with `-DPLUGIN_PYTHON_TESTS` when running +the configure step of cmake. + +You can then build the tests + +```bash +# replace vsbuild with your build folder +cmake --build vsbuild --config RelWithDebInfo --target "python-tests" "runner-tests" +``` + +To run the tests, use `ctest` + +```bash +# replace vsbuild with your build folder +ctest.exe --test-dir vsbuild -C RelWithDebInfo +``` diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 5abb1062..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: 1.0.{build} -skip_branch_with_pr: true -image: Visual Studio 2019 -environment: - WEBHOOK_URL: - secure: gOKbXaZM9ImtMD5XrYITvdyZUW/az082G9OIN1EC1Vbg57wBaeLhi49uGjxPw5GVujHku6kxN6ab89zhbS5GVeluR76GM83IbKV4Sh7udXzoYZZdg6YudtYHzdhCgUeiedpswbuczTq9ceIkkfSEWZuh/lMAAVVwvcGsJAnoPFw= -build: - parallel: true -build_script: -- cmd: >- - git clone --depth=1 --branch=%APPVEYOR_REPO_BRANCH% https://github.com/ModOrganizer2/modorganizer-umbrella.git c:\projects\modorganizer-umbrella 2> $null - - mkdir c:\projects\modorganizer-build -type directory - - cd c:\projects\modorganizer-umbrella - - C:\Python37-x64\python.exe unimake.py -d c:\projects\modorganizer-build -s Appveyor_Build=True %APPVEYOR_PROJECT_NAME% -artifacts: -- path: build\src\proxy\plugin_python.dll - name: plugin_python_dll -- path: build\src\proxy\plugin_python.pdb - name: plugin_python_pdb -- path: build\src\proxy\plugin_python.lib - name: plugin_python_lib -- path: build\src\runner\pythonrunner.dll - name: pythonrunner_dll -- path: build\src\runner\pythonrunner.pdb - name: pythonrunner_pdb -- path: build\src\runner\pythonrunner.lib - name: pythonrunner_lib -on_success: - - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER - - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - - ps: ./send.ps1 success $env:WEBHOOK_URL -on_failure: - - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER - - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stdout.log - - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stderr.log - - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - - ps: ./send.ps1 failure $env:WEBHOOK_URL \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..0ceb7f63 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,254 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mobase-stubs" +version = "2.5.1a0" +description = "PEP561 stub files for the mobase Python API." +optional = false +python-versions = "<4.0,>=3.12" +files = [ + {file = "mobase_stubs-2.5.1a0-py3-none-any.whl", hash = "sha256:bcaecfae038b890d82280fc518f7e44f38d22d35801a8ba7ffa480f7756d6823"}, + {file = "mobase_stubs-2.5.1a0.tar.gz", hash = "sha256:a8dc5574336ed3b1f673288447781f705a078472cf8808e05a36f129c81c8e20"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.23.0" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.23.0-py3-none-any.whl", hash = "sha256:d573ff31d7678e62b6f9bc9a1291ae2009ac14e0eead0a450598f9f05abb27a3"}, + {file = "poethepoet-0.23.0.tar.gz", hash = "sha256:62a0a6a518df5985c191aee0c1fcd2bb6a0a04eb102997786fcdf118e4147d22"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pybind11-stubgen" +version = "2.5.1" +description = "PEP 561 type stubs generator for pybind11 modules" +optional = false +python-versions = "~=3.7" +files = [ + {file = "pybind11-stubgen-2.5.1.tar.gz", hash = "sha256:4427a67038a00c5ac1637ffa6c65728c67c5b1251ecc23c7704152be0b14cc0b"}, + {file = "pybind11_stubgen-2.5.1-py3-none-any.whl", hash = "sha256:544d49df57da827c8761e7f6ef6bca996df80a33c9fd21c2521d694d4e72fe8d"}, +] + +[[package]] +name = "pyqt6" +version = "6.7.0" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyQt6-6.7.0-1-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:656734112853fde1be0963f0ad362e5efd87ba6c6ff234cb1f9fe8003ee254e6"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa2d27fc2f5340f3f1e145c815101ef4550771a9e4bfafd4c7c2479fe83d9488"}, + {file = "PyQt6-6.7.0-1-cp38-abi3-win_amd64.whl", hash = "sha256:6a1f6dfe03752f888b5e628c208f9fd1a03bda7ebda59ffed8c13580289a1892"}, + {file = "PyQt6-6.7.0-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:919ffb01020ece42209228bf94b4f2c156a6b77cc5a69a90a05e358b0333750b"}, + {file = "PyQt6-6.7.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e294f025f94493ee12b66efd6893fab309c9063172bb8a5b184f84dfc1ebcc49"}, + {file = "PyQt6-6.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:9d8865fb6357dba032002c4554a9648e88f2b4706c929cc51fba58edafad91fc"}, + {file = "PyQt6-6.7.0.tar.gz", hash = "sha256:3d31b2c59dc378ee26e16586d9469842483588142fc377280aad22aaf2fa6235"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.7.0,<6.8.0" +PyQt6-sip = ">=13.6,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.7.2" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +files = [ + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:065415589219a2f364aba29d6a98920bb32810286301acbfa157e522d30369e3"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f817efa86a0e8eda9152c85b73405463fbf3266299090f32bbb2266da540ead"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fc93945eaef4536d68bd53566535efcbe78a7c05c2a533790a8fd022bac8bfaa"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-win_amd64.whl", hash = "sha256:b2d7e5ddb1b9764cd60f1d730fa7bf7a1f0f61b2630967c81761d3d0a5a8a2e0"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.6.0" +description = "The sip module support for PyQt6" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyQt6_sip-13.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6b5f699aaed0ac1fcd23e8fbca70d8a77965831b7c1ce474b81b1678817a49d"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8c282062125eea5baf830c6998587d98c50be7c3a817a057fb95fef647184012"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-win32.whl", hash = "sha256:fa759b6339ff7e25f9afe2a6b651b775f0a36bcb3f5fa85e81a90d3b033c83f4"}, + {file = "PyQt6_sip-13.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f9df9f7ccd8a9f0f1d36948c686f03ce1a1281543a3e636b7b7d5e086e1a436"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b9c6b6f9cfccb48cbb78a59603145a698fb4ffd176764d7083e5bf47631d8df"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:86a7b67c64436e32bffa9c28c9f21bf14a9faa54991520b12c3f6f435f24df7f"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-win32.whl", hash = "sha256:58f68a48400e0b3d1ccb18090090299bad26e3aed7ccb7057c65887b79b8aeea"}, + {file = "PyQt6_sip-13.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:0dfd22cfedd87e96f9d51e0778ca2ba3dc0be83e424e9e0f98f6994d8d9c90f0"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3bf03e130fbfd75c9c06e687b86ba375410c7a9e835e4e03285889e61dd4b0c4"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:43fb8551796030aae3d66d6e35e277494071ec6172cd182c9569ab7db268a2f5"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-win32.whl", hash = "sha256:13885361ca2cb2f5085d50359ba61b3fabd41b139fb58f37332acbe631ef2357"}, + {file = "PyQt6_sip-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:24441032a29791e82beb7dfd76878339058def0e97fdb7c1cea517f3a0e6e96b"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3075d8b325382750829e6cde6971c943352309d35768a4d4da0587459606d562"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a6ce80bc24618d8a41be8ca51ad9f10e8bc4296dd90ab2809573df30a23ae0e5"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-win32.whl", hash = "sha256:fa7b10af7488efc5e53b41dd42c0f421bde6c2865a107af7ae259aff9d841da9"}, + {file = "PyQt6_sip-13.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:9adf672f9114687533a74d5c2d4c03a9a929ad5ad9c3e88098a7da1a440ab916"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98bf954103b087162fa63b3a78f30b0b63da22fd6450b610ec1b851dbb798228"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:39854dba35f8e5a4288da26ecb5f40b4c5ec1932efffb3f49d5ea435a7f37fb3"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-win32.whl", hash = "sha256:747f6ca44af81777a2c696bd501bc4815a53ec6fc94d4e25830e10bc1391f8ab"}, + {file = "PyQt6_sip-13.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:33ea771fe777eb0d1a2c3ef35bcc3f7a286eb3ff09cd5b2fdd3d87d1f392d7e8"}, + {file = "PyQt6_sip-13.6.0.tar.gz", hash = "sha256:2486e1588071943d4f6657ba09096dc9fffd2322ad2c30041e78ea3f037b5778"}, +] + +[[package]] +name = "pyright" +version = "1.1.369" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.369-py3-none-any.whl", hash = "sha256:06d5167a8d7be62523ced0265c5d2f1e022e110caf57a25d92f50fb2d07bcda0"}, + {file = "pyright-1.1.369.tar.gz", hash = "sha256:ad290710072d021e213b98cc7a2f90ae3a48609ef5b978f749346d1a47eb9af8"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.2.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "4a3746f1080ab40c4cfde6777517db502e124fe088e971895bd7f7c245c3fa3e" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..71bb9dff --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[tool.poetry] +name = "modorganizer-plugin_python" +version = "3.0.0" +description = "" +authors = ["Mikaël Capelle "] + +[tool.poetry.dependencies] +python = "^3.12" + +[tool.poetry.group.dev.dependencies] +pyright = "^1.1.369" +ruff = "^0.2.1" +poethepoet = "^0.23.0" +mobase-stubs = { version = "^2.5.1a0", allow-prereleases = true } +pyqt6 = "^6.7.0" +pytest = "^8.2.2" +pybind11-stubgen = "^2.5.1" + +[tool.poe.tasks] +format-imports = "ruff check --select I tests typings --fix" +format-ruff = "ruff format tests typings" +format.sequence = ["format-imports", "format-ruff"] +lint-ruff = "ruff check tests typings" +lint-ruff-format = "ruff format --check tests typings" +lint.sequence = ["lint-ruff", "lint-ruff-format"] +lint.ignore_fail = "return_non_zero" + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +extend-select = ["B", "Q", "I"] + +[tool.ruff.lint.isort.sections] +mobase = ["mobase"] +mobase_tests = ["mobase_tests"] + +[tool.ruff.lint.isort] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "mobase", + "mobase_tests", + "local-folder", +] + +[tool.pyright] +typeCheckingMode = "strict" +reportMissingTypeStubs = true +reportMissingModuleSource = false +pythonPlatform = "Windows" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..533a5ab5 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +# order matters +add_subdirectory(pybind11-qt) +add_subdirectory(pybind11-utils) +add_subdirectory(mobase) +add_subdirectory(runner) +add_subdirectory(proxy) diff --git a/src/mobase/CMakeLists.txt b/src/mobase/CMakeLists.txt new file mode 100644 index 00000000..72eff019 --- /dev/null +++ b/src/mobase/CMakeLists.txt @@ -0,0 +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_default_source_group() +mo2_configure_target(mobase + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC ON + TRANSLATIONS OFF +) +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/README.md b/src/mobase/README.md new file mode 100644 index 00000000..b4d184e2 --- /dev/null +++ b/src/mobase/README.md @@ -0,0 +1,110 @@ +# mobase + +`mobase` is a ModOrganizer2 Python API. +It provides access to the part of +[`uibase`](https://github.com/ModOrganizer2/modorganizer-uibase) C++ API from +Python through [`pybind11`](https://github.com/pybind/pybind11). + +## Organization + +**Important:** All (most) files should include `pybind11_all.h` (either directly +or through another header) to get proper `type_caster` available. + +- `mobase.cpp` contains the `PYBIND11_MODULE` definition of `mobase` but is otherwise + the entrypoint for other functions. +- `wrappers.h` contains the declaration of most functions implemented under + `wrappers/`. +- The other files under `wrappers/` contains bindings and trampoline classes (see + below) for `uibase` classes. + - `basic_classes.cpp` contains the bindings for most classes that cannot be extended + in Python (`IOrganizer`, `IModInterface`, etc.) + - `game_features.cpp` contains the bindings and trampoline classes for game features. + - `pyfiletree.h` and `pyfiletree.cpp` contains bindings for the `IFileTree`-related + classes. + - `pyplugins.h` contains the trampoline classes for the `IPluginXXX` classes and + `pyplugins.cpp` the bindings. + - `pyplugins.h` is required since the trampoline classes are tagged with `Q_OBJECT`, + and MOC does not work if the classes are declared in a C++ file. + - `pyplugins.cpp` also contains the `extract_plugins` function in `mobase.private` + that is used to extract plugins from Python object in the runner. + - `widgets.cpp` contains the bindings for the widget classes. + - `wrappers.cpp` contains the trampoline and bindings for non-plugin classes that can + be extended through Python. + +## Updating mobase + +### Classes that cannot be extended through Python + +Updating or adding classes that cannot be extended through Python is quite easily. +One simply needs to declare the appropriate `py::class_` or add new `.def()`. + +See below for things to remember when creating pybind11 bindings. + +### Free functions + +Similar to classes that cannot be extended through Python, see above. + +### Classes that can be extended through Python: Plugins + +To extend plugins, simply update the trampoline classes in `pyplugins.h` and the +bindings in `pyplugins.cpp`. + +**Note:** For new plugins, simply look at the existing one. + +### Classes that can be extended through Python: Game Features + +To extend or expose game features: + +- Create (if there is not already one) a trampoline class for the feature in + `game_features.cpp`. + - Add implementation of missing functions if required. +- Add the bindings in `add_game_feature_bindings` in `game_features.cpp`. +- For new feature, add the feature type to `GameFeaturesHelper::GameFeatures` in + `game_features.cpp`. + +### Classes that can be extended through Python: Others + +Non-plugin classes should be added to the `wrappers.cpp` file and should be exposed +with `std::shared_ptr<>` or `qobject_holder<>` holders. + +- If the classes extends `QObject`, use a `qobject_holder`. +- Otherwise use a `std::shared_ptr<>` holder and add a `MO2_PYBIND11_SHARED_CPP_HOLDER` + declaration in `pybind11_all.h`. + +Trampoline can be defined directly in `wrappers.cpp`, and bindings in the appropriate +function. +See the existing classes for example. + +**Important:** +You need to make sure that `uibase` manipulates such classes through +`std::shared_ptr<>` (unless those inherit `QObject`). +Using `std::unique_ptr<>` is not possible since `std::unique_ptr<>` cannot have custom +runtime-specified deleters. + +## Things to remember + +Here are a few things to remember when creating bindings: + +- If a function has multiple overloads that can conflict in Python, the more complex + one must be defined first as pybind11 will try calling them in order. +- If a C++ function expect a `QString`, `QFileInfo` or `QDir` that represents a file or + a directory, wrapping the function with `wrap_for_filepath` or `wrap_for_directory` is + a good idea. This allows Python to call the function with `pathlib.Path`. +- Most of the C++ function taking a reference to modify in C++ cannot be directly + exposed in Python since Python cannot modify reference to simple type (e.g. + `QString&` or `int&`). + The best way to expose such function is to bind a lambda that returns a variant from + Python, e.g. + +```cpp +// assume the C++ function is QString fn(QString& foo, QString const& bar, int& maz); + +m.def("function", [](QString& foo, QString const& bar, int& maz) { + // call the function + auto ret = function(foo, bar, maz); + + // make a tuple containing the return value (if there is one), and the modified + // values passed by reference + return std::make_tuple(ret, foo, maz); +}); +``` diff --git a/src/mobase/deprecation.cpp b/src/mobase/deprecation.cpp new file mode 100644 index 00000000..958712d8 --- /dev/null +++ b/src/mobase/deprecation.cpp @@ -0,0 +1,55 @@ +#include "deprecation.h" + +#include +#include + +#include + +#include + +#include + +namespace py = pybind11; + +namespace mo2::python { + + void show_deprecation_warning(std::string_view name, std::string_view message, + bool show_once) + { + + // Contains the list of filename / line number for which a deprecation + // warning has already been shown. + static std::set> DeprecatedLines; + + // Find the caller: + auto inspect = py::module_::import("inspect"); + auto current_frame = inspect.attr("currentframe")(); + py::sequence callable_frame = inspect.attr("getouterframes")(current_frame, 2); + auto last_frame = callable_frame[py::int_(-1)]; + auto filename = last_frame.attr("filename").cast(); + auto function = last_frame.attr("function").cast(); + auto lineno = last_frame.attr("lineno").cast(); + + // Only show once if requested: + if (show_once && DeprecatedLines.contains({filename, lineno})) { + return; + } + + // Register the deprecation: + DeprecatedLines.emplace(filename, lineno); + + auto path = relative(std::filesystem::path(filename), + QCoreApplication::applicationDirPath().toStdWString()); + + // Show the message: + if (message.empty()) { + MOBase::log::warn("[deprecated] {} in {} [{}:{}].", name, function, + path.native(), lineno); + } + else { + MOBase::log::warn("[deprecated] {} in {} [{}:{}]: {}", name, function, + path.native(), lineno, message); + } + } + +} // namespace mo2::python diff --git a/src/mobase/deprecation.h b/src/mobase/deprecation.h new file mode 100644 index 00000000..d9a445c7 --- /dev/null +++ b/src/mobase/deprecation.h @@ -0,0 +1,27 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include + +namespace mo2::python { + + /** + * @brief Show a deprecation warning. + * + * This methods will print a warning in MO2 log containing the location of + * the call to the deprecated function. If show_once is true, the + * deprecation warning will only be logged the first time the function is + * called at this location. + * + * @param name Name of the deprecated function. + * @param message Deprecation message. + * @param show_once Only show the message once per call location. + */ + void show_deprecation_warning(std::string_view name, std::string_view message = "", + bool show_once = true); + +} // namespace mo2::python + +#endif diff --git a/src/mobase/mobase.cpp b/src/mobase/mobase.cpp new file mode 100644 index 00000000..b1daa2bf --- /dev/null +++ b/src/mobase/mobase.cpp @@ -0,0 +1,86 @@ +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) + +#include +#include +#include + +#include + +#include "pybind11_all.h" + +#include "wrappers/pyfiletree.h" +#include "wrappers/wrappers.h" + +using namespace MOBase; +namespace py = pybind11; + +PYBIND11_MODULE(mobase, m) +{ + using namespace mo2::python; + + m.add_object("PyQt6", py::module_::import("PyQt6")); + m.add_object("PyQt6.QtCore", py::module_::import("PyQt6.QtCore")); + m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui")); + m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets")); + + // bindings + // + mo2::python::add_basic_bindings(m); + mo2::python::add_wrapper_bindings(m); + + // game features must be added before plugins + mo2::python::add_game_feature_bindings(m); + mo2::python::add_igamefeatures_classes(m); + + mo2::python::add_plugins_bindings(m); + + // widgets + // + py::module_ widgets = m.def_submodule("widgets"); + mo2::python::add_widget_bindings(widgets); + + // utils + // + py::module_ utils = m.def_submodule("utils"); + mo2::python::add_utils_bindings(utils); + + // functions + // + m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion), + py::arg("filepath")); + m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion), + py::arg("executable")); + m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable), + py::arg("executable")); + + // typing stuff to be consistent with stubs and allow plugin developers to properly + // type their code if they want + { + m.add_object("TypeVar", py::module_::import("typing").attr("TypeVar")); + + auto s = m.attr("__dict__"); + + // expose MoVariant + // + // this needs to be defined, otherwise MoVariant is not found when actually + // running plugins through MO2, making them crash (if plugins use MoVariant in + // their own code) + // + m.add_object( + "MoVariant", + py::eval("None | bool | int | str | list[object] | dict[str, object]")); + + // same thing for GameFeatureType + // + m.add_object("GameFeatureType", py::eval("TypeVar('GameFeatureType')", s)); + } + + // private stuff for debugging/test + py::module_ moprivate = m.def_submodule("private"); + + // expose a function to create a particular tree, only for debugging + // purpose, not in mobase. + mo2::python::add_make_tree_function(moprivate); + moprivate.def("extract_plugins", &mo2::python::extract_plugins); +} diff --git a/src/mobase/pybind11_all.h b/src/mobase/pybind11_all.h new file mode 100644 index 00000000..a293fb86 --- /dev/null +++ b/src/mobase/pybind11_all.h @@ -0,0 +1,146 @@ +#ifndef PYTHON_PYBIND11_ALL_H +#define PYTHON_PYBIND11_ALL_H + +#include + +#include + +#include +#include +#include +#include + +#include "pybind11_qt/pybind11_qt.h" + +#include "pybind11_utils/functional.h" +#include "pybind11_utils/generator.h" +#include "pybind11_utils/shared_cpp_owner.h" +#include "pybind11_utils/smart_variant_wrapper.h" + +#include +#include +#include + +namespace mo2::python { + + namespace detail { + + template <> + struct smart_variant_converter { + + static QString from(std::filesystem::path const& path) + { + return QString::fromStdWString(path.native()); + } + + static QString from(QFileInfo const& fileInfo) + { + return fileInfo.filePath(); + } + + static QString from(QDir const& dir) { return dir.path(); } + }; + + template <> + struct smart_variant_converter { + + static std::filesystem::path from(QString const& value) + { + return {value.toStdWString()}; + } + + static std::filesystem::path from(QFileInfo const& fileInfo) + { + return fileInfo.filesystemFilePath(); + } + + static std::filesystem::path from(QDir const& dir) + { + return dir.filesystemPath(); + } + }; + + // we do not need specialization for QFileInfo and QDir because both of them can + // be constructed from std::filesystem::path and QString already + + } // namespace detail + + using FileWrapper = smart_variant; + using DirectoryWrapper = smart_variant; + + // wrap the given function to accept FileWrapper (str | PathLike | QFileInfo) at the + // given argument positions (or any valid positions if Is... is empty) + // + template + auto wrap_for_filepath(Fn&& fn) + { + return mo2::python::wrap_arguments(std::forward(fn)); + } + + // wrap the given function to accept DirectoryWrapper (str | PathLike | QDir) + // at the given argument positions (or any valid positions if Is... is empty) + // + template + auto wrap_for_directory(Fn&& fn) + { + return mo2::python::wrap_arguments( + std::forward(fn)); + } + + // wrap a function-like object to return a FileWrapper instead of its return type, + // useful to generate proper typing in Python + // + // note that QFileInfo has a __fspath__ in Python, so it is quite easy to convert + // from "FileWrapper", a.k.a., str | os.PathLike | QFileInfo to Path by simply + // calling Path() on the return type if necessary + // + // this should be combined with custom return-value in PYBIND11_OVERRIDE(_PURE), see + // ISaveGame binding for an example + // + template + auto wrap_return_for_filepath(Fn&& fn) + { + return mo2::python::wrap_return(std::forward(fn)); + } + + // similar to wrap_return_for_filepath, except it returns a DirectoryWrapper instead + // of its return type + // + // this is much less practical than wrap_return_for_filepath since QDir does not + // expose __fspath__, so more complex things need to be done in Python, which is why + // this should be used carefully (e.g., should not be used if the return type is + // already QDir) + // + template + auto wrap_return_for_directory(Fn&& fn) + { + return mo2::python::wrap_return(std::forward(fn)); + } + + // convert a QList to QStringList - QString must be constructible from QString + // + template + QStringList toQStringList(QList const& list) + { + static_assert(std::is_constructible_v, + "QString must be constructible from T."); + return {list.begin(), list.end()}; + } + + // convert a QStringList to a QList - T must be constructible from QString + // + template + QList toQList(QStringList const& list) + { + static_assert(std::is_constructible_v, + "T must be constructible from QString."); + return {list.begin(), list.end()}; + } + +} // namespace mo2::python + +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement) +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame) +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::GameFeature) + +#endif diff --git a/src/mobase/wrappers/basic_classes.cpp b/src/mobase/wrappers/basic_classes.cpp new file mode 100644 index 00000000..ea69a467 --- /dev/null +++ b/src/mobase/wrappers/basic_classes.cpp @@ -0,0 +1,952 @@ +#include "wrappers.h" + +#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 "../deprecation.h" +#include "pyfiletree.h" + +using namespace MOBase; + +namespace mo2::python { + + namespace py = pybind11; + + using namespace pybind11::literals; + + 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() + .def("__xor__", + py::overload_cast( + &operator^)) + .def("__and__", + py::overload_cast( + &operator&)) + .def("__or__", py::overload_cast( + &operator|)) + .def("__rxor__", + py::overload_cast( + &operator^)) + .def("__rand__", + py::overload_cast( + &operator&)) + .def("__ror__", + py::overload_cast( + &operator|)); + + 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::FormatModes{}) + .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); + + // VersionInfo + py::enum_(m, "ReleaseType") + .value("final", MOBase::VersionInfo::RELEASE_FINAL) + .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE) + .value("beta", MOBase::VersionInfo::RELEASE_BETA) + .value("alpha", MOBase::VersionInfo::RELEASE_ALPHA) + .value("prealpha", MOBase::VersionInfo::RELEASE_PREALPHA) + + .value("FINAL", MOBase::VersionInfo::RELEASE_FINAL) + .value("CANDIDATE", MOBase::VersionInfo::RELEASE_CANDIDATE) + .value("BETA", MOBase::VersionInfo::RELEASE_BETA) + .value("ALPHA", MOBase::VersionInfo::RELEASE_ALPHA) + .value("PRE_ALPHA", MOBase::VersionInfo::RELEASE_PREALPHA); + + py::enum_(m, "VersionScheme") + .value("discover", MOBase::VersionInfo::SCHEME_DISCOVER) + .value("regular", MOBase::VersionInfo::SCHEME_REGULAR) + .value("decimalmark", MOBase::VersionInfo::SCHEME_DECIMALMARK) + .value("numbersandletters", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + .value("date", MOBase::VersionInfo::SCHEME_DATE) + .value("literal", MOBase::VersionInfo::SCHEME_LITERAL) + + .value("DISCOVER", MOBase::VersionInfo::SCHEME_DISCOVER) + .value("REGULAR", MOBase::VersionInfo::SCHEME_REGULAR) + .value("DECIMAL_MARK", MOBase::VersionInfo::SCHEME_DECIMALMARK) + .value("NUMBERS_AND_LETTERS", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + .value("DATE", MOBase::VersionInfo::SCHEME_DATE) + .value("LITERAL", MOBase::VersionInfo::SCHEME_LITERAL); + + py::class_(m, "VersionInfo") + .def(py::init<>()) + .def(py::init(), "value"_a, + "scheme"_a = VersionInfo::SCHEME_DISCOVER) + // note: order of the two init<> below is important because + // ReleaseType is a simple enum with an implicit int conversion. + .def(py::init(), "major"_a, + "minor"_a, "subminor"_a, "subsubminor"_a, + "release_type"_a = VersionInfo::RELEASE_FINAL) + .def(py::init(), "major"_a, + "minor"_a, "subminor"_a, "release_type"_a = VersionInfo::RELEASE_FINAL) + .def("clear", &VersionInfo::clear) + .def("parse", &VersionInfo::parse, "value"_a, + "scheme"_a = VersionInfo::SCHEME_DISCOVER, "is_manual"_a = false) + .def("canonicalString", &VersionInfo::canonicalString) + .def("displayString", &VersionInfo::displayString, "forced_segments"_a = 2) + .def("isValid", &VersionInfo::isValid) + .def("scheme", &VersionInfo::scheme) + .def("__str__", &VersionInfo::canonicalString) + .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); + } + + void add_executable_classes(py::module_ m) + { + py::class_(m, "ExecutableInfo") + .def(py::init(), "title"_a, "binary"_a) + .def("withArgument", &ExecutableInfo::withArgument, "argument"_a) + .def("withWorkingDirectory", + wrap_for_directory(&ExecutableInfo::withWorkingDirectory), + "directory"_a) + .def("withSteamAppId", &ExecutableInfo::withSteamAppId, "app_id"_a) + .def("asCustom", &ExecutableInfo::asCustom) + .def("isValid", &ExecutableInfo::isValid) + .def("title", &ExecutableInfo::title) + .def("binary", &ExecutableInfo::binary) + .def("arguments", &ExecutableInfo::arguments) + .def("workingDirectory", &ExecutableInfo::workingDirectory) + .def("steamAppID", &ExecutableInfo::steamAppID) + .def("isCustom", &ExecutableInfo::isCustom); + + py::class_(m, "ExecutableForcedLoadSetting") + .def(py::init(), "process"_a, "library"_a) + .def("withForced", &ExecutableForcedLoadSetting::withForced, "forced"_a) + .def("withEnabled", &ExecutableForcedLoadSetting::withEnabled, "enabled"_a) + .def("enabled", &ExecutableForcedLoadSetting::enabled) + .def("forced", &ExecutableForcedLoadSetting::forced) + .def("library", &ExecutableForcedLoadSetting::library) + .def("process", &ExecutableForcedLoadSetting::process); + + py::class_(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_(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("titleExists", &IExecutablesList::contains, "title"_a); + } + + void add_modinterface_classes(py::module_ m) + { + py::enum_(m, "EndorsedState", py::arithmetic()) + .value("ENDORSED_FALSE", EndorsedState::ENDORSED_FALSE) + .value("ENDORSED_TRUE", EndorsedState::ENDORSED_TRUE) + .value("ENDORSED_UNKNOWN", EndorsedState::ENDORSED_UNKNOWN) + .value("ENDORSED_NEVER", EndorsedState::ENDORSED_NEVER); + + py::enum_(m, "TrackedState", py::arithmetic()) + .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE) + .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE) + .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN); + + py::class_(m, "IModInterface") + .def("name", &IModInterface::name) + .def("absolutePath", &IModInterface::absolutePath) + + .def("comments", &IModInterface::comments) + .def("notes", &IModInterface::notes) + .def("gameName", &IModInterface::gameName) + .def("repository", &IModInterface::repository) + .def("nexusId", &IModInterface::nexusId) + .def("version", &IModInterface::version) + .def("newestVersion", &IModInterface::newestVersion) + .def("ignoredVersion", &IModInterface::ignoredVersion) + .def("installationFile", &IModInterface::installationFile) + .def("converted", &IModInterface::converted) + .def("validated", &IModInterface::validated) + .def("color", &IModInterface::color) + .def("url", &IModInterface::url) + .def("primaryCategory", &IModInterface::primaryCategory) + .def("categories", &IModInterface::categories) + .def("author", &IModInterface::author) + .def("uploader", &IModInterface::uploader) + .def("uploaderUrl", &IModInterface::uploaderUrl) + .def("trackedState", &IModInterface::trackedState) + .def("endorsedState", &IModInterface::endorsedState) + .def("fileTree", &IModInterface::fileTree) + .def("isOverwrite", &IModInterface::isOverwrite) + .def("isBackup", &IModInterface::isBackup) + .def("isSeparator", &IModInterface::isSeparator) + .def("isForeign", &IModInterface::isForeign) + + .def("setVersion", &IModInterface::setVersion, "version"_a) + .def("setNewestVersion", &IModInterface::setNewestVersion, "version"_a) + .def("setIsEndorsed", &IModInterface::setIsEndorsed, "endorsed"_a) + .def("setNexusID", &IModInterface::setNexusID, "nexus_id"_a) + .def("addNexusCategory", &IModInterface::addNexusCategory, "category_id"_a) + .def("addCategory", &IModInterface::addCategory, "name"_a) + .def("removeCategory", &IModInterface::removeCategory, "name"_a) + .def("setGameName", &IModInterface::setGameName, "name"_a) + .def("setUrl", &IModInterface::setUrl, "url"_a) + .def("pluginSetting", &IModInterface::pluginSetting, "plugin_name"_a, + "key"_a, "default"_a = QVariant()) + .def("pluginSettings", &IModInterface::pluginSettings, "plugin_name"_a) + .def("setPluginSetting", &IModInterface::setPluginSetting, "plugin_name"_a, + "key"_a, "value"_a) + .def("clearPluginSettings", &IModInterface::clearPluginSettings, + "plugin_name"_a); + } + + void add_modrepository_classes(py::module_ m) + { + py::class_ iModRepositoryBridge(m, + "IModRepositoryBridge"); + iModRepositoryBridge + .def("requestDescription", &IModRepositoryBridge::requestDescription, + "game_name"_a, "mod_id"_a, "user_data"_a) + .def("requestFiles", &IModRepositoryBridge::requestFiles, "game_name"_a, + "mod_id"_a, "user_data"_a) + .def("requestFileInfo", &IModRepositoryBridge::requestFileInfo, + "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a) + .def("requestDownloadURL", &IModRepositoryBridge::requestDownloadURL, + "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a) + .def("requestToggleEndorsement", + &IModRepositoryBridge::requestToggleEndorsement, "game_name"_a, + "mod_id"_a, "mod_version"_a, "endorse"_a, "user_data"_a); + + py::qt::add_qt_delegate(iModRepositoryBridge, "_object"); + + py::class_(m, "ModRepositoryFileInfo") + .def(py::init(), "other"_a) + .def(py::init(), "game_name"_a = "", "mod_id"_a = 0, + "file_id"_a = 0) + .def("__str__", &ModRepositoryFileInfo::toString) + .def_static("createFromJson", &ModRepositoryFileInfo::createFromJson, + "data"_a) + .def_readwrite("name", &ModRepositoryFileInfo::name) + .def_readwrite("uri", &ModRepositoryFileInfo::uri) + .def_readwrite("description", &ModRepositoryFileInfo::description) + .def_readwrite("version", &ModRepositoryFileInfo::version) + .def_readwrite("newestVersion", &ModRepositoryFileInfo::newestVersion) + .def_readwrite("categoryID", &ModRepositoryFileInfo::categoryID) + .def_readwrite("modName", &ModRepositoryFileInfo::modName) + .def_readwrite("gameName", &ModRepositoryFileInfo::gameName) + .def_readwrite("modID", &ModRepositoryFileInfo::modID) + .def_readwrite("fileID", &ModRepositoryFileInfo::fileID) + .def_readwrite("fileSize", &ModRepositoryFileInfo::fileSize) + .def_readwrite("fileName", &ModRepositoryFileInfo::fileName) + .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory) + .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime) + .def_readwrite("repository", &ModRepositoryFileInfo::repository) + .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) + { + py::enum_(m, "GuessQuality") + .value("INVALID", MOBase::GUESS_INVALID) + .value("FALLBACK", MOBase::GUESS_FALLBACK) + .value("GOOD", MOBase::GUESS_GOOD) + .value("META", MOBase::GUESS_META) + .value("PRESET", MOBase::GUESS_PRESET) + .value("USER", MOBase::GUESS_USER); + + py::class_>(m, "GuessedString") + .def(py::init<>()) + .def(py::init(), "value"_a, + "quality"_a = EGuessQuality::GUESS_USER) + .def("update", + py::overload_cast(&GuessedValue::update), + "value"_a) + .def("update", + py::overload_cast( + &GuessedValue::update), + "value"_a, "quality"_a) + + // Methods to simulate the assignment operator: + .def("reset", + [](GuessedValue* gv) { + *gv = GuessedValue(); + return gv; + }) + .def( + "reset", + [](GuessedValue* gv, const QString& value, EGuessQuality eq) { + *gv = GuessedValue(value, eq); + return gv; + }, + "value"_a, "quality"_a) + .def( + "reset", + [](GuessedValue* gv, const GuessedValue& other) { + *gv = other; + return gv; + }, + "other"_a) + + // use an intermediate lambda because we cannot have a function with a + // non-const reference in Python - in Python, the function should returned a + // bool or the modified value + .def( + "setFilter", + [](GuessedValue* gv, + std::function(QString const&)> fn) { + gv->setFilter([fn](QString& s) { + auto ret = fn(s); + return std::visit( + [&s](auto v) { + if constexpr (std::is_same_v) { + s = v; + return true; + } + else if constexpr (std::is_same_v) { + return v; + } + }, + ret); + }); + }, + "filter"_a) + + // this makes a copy in python but it more practical than + // exposing an iterator + .def("variants", &GuessedValue::variants) + .def("__str__", &MOBase::GuessedValue::operator const QString&); + + // implicit conversion from QString - this allows passing Python string to + // function expecting GuessedValue + py::implicitly_convertible>(); + } + + void add_ipluginlist_classes(py::module_ m) + { + py::enum_(m, "PluginState", py::arithmetic()) + .value("missing", IPluginList::STATE_MISSING) + .value("inactive", IPluginList::STATE_INACTIVE) + .value("active", IPluginList::STATE_ACTIVE) + + .value("MISSING", IPluginList::STATE_MISSING) + .value("INACTIVE", IPluginList::STATE_INACTIVE) + .value("ACTIVE", IPluginList::STATE_ACTIVE); + + py::class_(m, "IPluginList") + .def("state", &IPluginList::state, "name"_a) + .def("priority", &IPluginList::priority, "name"_a) + .def("setPriority", &IPluginList::setPriority, "name"_a, "priority"_a) + .def("loadOrder", &IPluginList::loadOrder, "name"_a) + .def("masters", &IPluginList::masters, "name"_a) + .def("origin", &IPluginList::origin, "name"_a) + .def("onRefreshed", &IPluginList::onRefreshed, "callback"_a) + .def("onPluginMoved", &IPluginList::onPluginMoved, "callback"_a) + + .def("isMasterFlagged", &IPluginList::isMasterFlagged, "name"_a) + .def("hasMasterExtension", &IPluginList::hasMasterExtension, "name"_a) + .def("isMediumFlagged", &IPluginList::isMediumFlagged, "name"_a) + .def("isLightFlagged", &IPluginList::isLightFlagged, "name"_a) + .def("isBlueprintFlagged", &IPluginList::isBlueprintFlagged, "name"_a) + .def("hasLightExtension", &IPluginList::hasLightExtension, "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: + .def( + "onPluginStateChanged", + [](IPluginList* modList, + const std::function& + fn) { + mo2::python::show_deprecation_warning( + "onPluginStateChanged", + "onPluginStateChanged(Callable[[str, " + "IPluginList.PluginStates], None]) is deprecated, " + "use onPluginStateChanged(Callable[[Dict[str, " + "IPluginList.PluginStates], None]) instead."); + return modList->onPluginStateChanged([fn](auto const& map) { + for (const auto& entry : map) { + fn(entry.first, entry.second); + } + }); + }, + "callback"_a) + .def( + "isMaster", + [](IPluginList* modList, QString const& name) { + mo2::python::show_deprecation_warning( + "isMaster", + "isMaster(str) is deprecated, use isMasterFlagged() or " + "hasMasterExtension() instead."); + return modList->isMasterFlagged(name); + }, + "name"_a) + .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, + "callback"_a) + .def("pluginNames", &MOBase::IPluginList::pluginNames) + .def("setState", &MOBase::IPluginList::setState, "name"_a, "state"_a) + .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, "loadorder"_a); + } + + void add_imodlist_classes(py::module_ m) + { + py::enum_(m, "ModState", py::arithmetic()) + .value("exists", IModList::STATE_EXISTS) + .value("active", IModList::STATE_ACTIVE) + .value("essential", IModList::STATE_ESSENTIAL) + .value("empty", IModList::STATE_EMPTY) + .value("endorsed", IModList::STATE_ENDORSED) + .value("valid", IModList::STATE_VALID) + .value("alternate", IModList::STATE_ALTERNATE) + + .value("EXISTS", IModList::STATE_EXISTS) + .value("ACTIVE", IModList::STATE_ACTIVE) + .value("ESSENTIAL", IModList::STATE_ESSENTIAL) + .value("EMPTY", IModList::STATE_EMPTY) + .value("ENDORSED", IModList::STATE_ENDORSED) + .value("VALID", IModList::STATE_VALID) + .value("ALTERNATE", IModList::STATE_ALTERNATE); + + py::class_(m, "IModList") + .def("displayName", &MOBase::IModList::displayName, "name"_a) + .def("allMods", &MOBase::IModList::allMods) + .def("allModsByProfilePriority", + &MOBase::IModList::allModsByProfilePriority, + "profile"_a = static_cast(nullptr)) + .def("getMod", &MOBase::IModList::getMod, + py::return_value_policy::reference, "name"_a) + .def("removeMod", &MOBase::IModList::removeMod, "mod"_a) + .def("renameMod", &MOBase::IModList::renameMod, + py::return_value_policy::reference, "mod"_a, "name"_a) + + .def("state", &MOBase::IModList::state, "name"_a) + .def("setActive", + py::overload_cast( + &MOBase::IModList::setActive), + "names"_a, "active"_a) + .def("setActive", + py::overload_cast(&MOBase::IModList::setActive), + "name"_a, "active"_a) + .def("priority", &MOBase::IModList::priority, "name"_a) + .def("setPriority", &MOBase::IModList::setPriority, "name"_a, "priority"_a) + + // kept but deprecated for backward compatibility + .def( + "onModStateChanged", + [](IModList* modList, + const std::function& fn) { + mo2::python::show_deprecation_warning( + "onModStateChanged", + "onModStateChanged(Callable[[str, IModList.ModStates], None]) " + "is deprecated, " + "use onModStateChanged(Callable[[Dict[str, " + "IModList.ModStates], None]) instead."); + return modList->onModStateChanged([fn](auto const& map) { + for (const auto& entry : map) { + fn(entry.first, entry.second); + } + }); + }, + "callback"_a) + + .def("onModInstalled", &MOBase::IModList::onModInstalled, "callback"_a) + .def("onModRemoved", &MOBase::IModList::onModRemoved, "callback"_a) + .def("onModStateChanged", &MOBase::IModList::onModStateChanged, + "callback"_a) + .def("onModMoved", &MOBase::IModList::onModMoved, "callback"_a); + } + + void add_iorganizer_classes(py::module_ m) + { + // define INVALID_HANDLE_VALUE for startApplication, etc. + m.attr("INVALID_HANDLE_VALUE") = py::int_((std::uintptr_t)INVALID_HANDLE_VALUE); + + py::class_(m, "FileInfo") + .def(py::init<>()) + .def_readwrite("filePath", &IOrganizer::FileInfo::filePath) + .def_readwrite("archive", &IOrganizer::FileInfo::archive) + .def_readwrite("origins", &IOrganizer::FileInfo::origins); + + py::class_(m, "IOrganizer") + .def("createNexusBridge", &IOrganizer::createNexusBridge, + py::return_value_policy::reference) + .def("instanceName", &IOrganizer::instanceName) + .def("profileName", &IOrganizer::profileName) + .def("profilePath", &IOrganizer::profilePath) + .def("downloadsPath", &IOrganizer::downloadsPath) + .def("overwritePath", &IOrganizer::overwritePath) + .def("basePath", &IOrganizer::basePath) + .def("modsPath", &IOrganizer::modsPath) + .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, + "name"_a) + .def("modDataChanged", &IOrganizer::modDataChanged, "mod"_a) + .def("isPluginEnabled", + py::overload_cast(&IOrganizer::isPluginEnabled, py::const_), + "plugin"_a) + .def("isPluginEnabled", + py::overload_cast(&IOrganizer::isPluginEnabled, + py::const_), + "plugin"_a) + .def("pluginSetting", &IOrganizer::pluginSetting, "plugin_name"_a, "key"_a) + .def("setPluginSetting", &IOrganizer::setPluginSetting, "plugin_name"_a, + "key"_a, "value"_a) + .def("persistent", &IOrganizer::persistent, "plugin_name"_a, "key"_a, + "default"_a = QVariant()) + .def("setPersistent", &IOrganizer::setPersistent, "plugin_name"_a, "key"_a, + "value"_a, "sync"_a = true) + .def("pluginDataPath", &IOrganizer::pluginDataPath) + .def("installMod", wrap_for_filepath<1>(&IOrganizer::installMod), + py::return_value_policy::reference, "filename"_a, + "name_suggestion"_a = "") + .def("resolvePath", wrap_for_filepath(&IOrganizer::resolvePath), + "filename"_a) + .def("listDirectories", &IOrganizer::listDirectories, "directory"_a) + + // "provide multiple overloads of findFiles + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, + std::function const& f) { + return o->findFiles(p, f); + }, + "path"_a, "filter"_a) + + // in C++, it is possible to create a QStringList implicitly from + // a single QString, in Python is not possible with the current + // converters in python (and I do not think it is a good idea to + // have it everywhere), but here it is nice to be able to + // pass a single string, so we add an extra overload + // + // important: the order matters, because a Python string can be + // converted to a QStringList since it is a sequence of + // single-character strings: + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, + const QStringList& gf) { + return o->findFiles(p, gf); + }, + "path"_a, "patterns"_a) + .def( + "findFiles", + [](const IOrganizer* o, DirectoryWrapper const& p, const QString& f) { + return o->findFiles(p, QStringList{f}); + }, + "path"_a, "pattern"_a) + + .def("getFileOrigins", &IOrganizer::getFileOrigins, "filename"_a) + .def("findFileInfos", wrap_for_directory(&IOrganizer::findFileInfos), + "path"_a, "filter"_a) + + .def("virtualFileTree", &IOrganizer::virtualFileTree) + + .def("instanceManager", &IOrganizer::instanceManager, + py::return_value_policy::reference) + .def("downloadManager", &IOrganizer::downloadManager, + py::return_value_policy::reference) + .def("pluginList", &IOrganizer::pluginList, + 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, + py::return_value_policy::reference) + .def("profile", &IOrganizer::profile) + .def("profileNames", &IOrganizer::profileNames) + .def("getProfile", &IOrganizer::getProfile, "name"_a) + + // custom implementation for startApplication and + // waitForApplication because 1) HANDLE (= void*) is not properly + // converted from/to python, and 2) we need to convert the by-ptr + // argument to a return-tuple for waitForApplication + .def( + "startApplication", + [](IOrganizer* o, const FileWrapper& executable, + const QStringList& args, const DirectoryWrapper& cwd, + const QString& profile, const QString& forcedCustomOverwrite, + bool ignoreCustomOverwrite) -> std::uintptr_t { + return (std::uintptr_t)o->startApplication( + executable, args, cwd, profile, forcedCustomOverwrite, + ignoreCustomOverwrite); + }, + "executable"_a, "args"_a = QStringList(), "cwd"_a = "", + "profile"_a = "", "forcedCustomOverwrite"_a = "", + "ignoreCustomOverwrite"_a = false) + .def( + "waitForApplication", + [](IOrganizer* o, std::uintptr_t handle, bool refresh) { + DWORD returnCode; + bool result = + o->waitForApplication((HANDLE)handle, refresh, &returnCode); + + // we force signed return code because it's probably what's expected + // in Python + return std::make_tuple( + result, static_cast>(returnCode)); + }, + "handle"_a, "refresh"_a = true) + + .def("refresh", &IOrganizer::refresh, "save_changes"_a = true) + .def("managedGame", &IOrganizer::managedGame, + py::return_value_policy::reference) + + .def("onAboutToRun", + py::overload_cast const&>( + &IOrganizer::onAboutToRun), + "callback"_a) + .def("onAboutToRun", + py::overload_cast const&>( + &IOrganizer::onAboutToRun), + "callback"_a) + .def("onFinishedRun", &IOrganizer::onFinishedRun, "callback"_a) + .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized, + "callback"_a) + .def("onNextRefresh", &IOrganizer::onNextRefresh, "callback"_a, + "immediate_if_possible"_a = true) + .def("onProfileCreated", &IOrganizer::onProfileCreated, "callback"_a) + .def("onProfileRenamed", &IOrganizer::onProfileRenamed, "callback"_a) + .def("onProfileRemoved", &IOrganizer::onProfileRemoved, "callback"_a) + .def("onProfileChanged", &IOrganizer::onProfileChanged, "callback"_a) + + .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged, + "callback"_a) + .def( + "onPluginEnabled", + [](IOrganizer* o, std::function const& func) { + o->onPluginEnabled(func); + }, + "callback"_a) + .def( + "onPluginEnabled", + [](IOrganizer* o, QString const& name, + std::function const& func) { + o->onPluginEnabled(name, func); + }, + "name"_a, "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, std::function const& func) { + o->onPluginDisabled(func); + }, + "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, QString const& name, + std::function const& func) { + o->onPluginDisabled(name, func); + }, + "name"_a, "callback"_a) + + // DEPRECATED: + .def( + "getMod", + [](IOrganizer* o, QString const& name) { + mo2::python::show_deprecation_warning( + "getMod", "IOrganizer::getMod(str) is deprecated, use " + "IModList::getMod(str) instead."); + return o->modList()->getMod(name); + }, + py::return_value_policy::reference, "name"_a) + .def( + "removeMod", + [](IOrganizer* o, IModInterface* mod) { + mo2::python::show_deprecation_warning( + "removeMod", + "IOrganizer::removeMod(IModInterface) is deprecated, use " + "IModList::removeMod(IModInterface) instead."); + return o->modList()->removeMod(mod); + }, + "mod"_a) + .def("modsSortedByProfilePriority", + [](IOrganizer* o) { + mo2::python::show_deprecation_warning( + "modsSortedByProfilePriority", + "IOrganizer::modsSortedByProfilePriority() is deprecated, use " + "IModList::allModsByProfilePriority() instead."); + return o->modList()->allModsByProfilePriority(); + }) + .def( + "refreshModList", + [](IOrganizer* o, bool s) { + mo2::python::show_deprecation_warning( + "refreshModList", + "IOrganizer::refreshModList(bool) is deprecated, use " + "IOrganizer::refresh(bool) instead."); + o->refresh(s); + }, + "save_changes"_a = true) + .def( + "onModInstalled", + [](IOrganizer* organizer, + const std::function& func) { + mo2::python::show_deprecation_warning( + "onModInstalled", + "IOrganizer::onModInstalled(Callable[[str], None]) is " + "deprecated, " + "use IModList::onModInstalled(Callable[[IModInterface], None]) " + "instead."); + return organizer->modList()->onModInstalled( + [func](MOBase::IModInterface* m) { + func(m->name()); + }); + ; + }, + "callback"_a) + + .def_static("getPluginDataPath", &IOrganizer::getPluginDataPath); + } + + void add_iinstance_manager_classes(py::module_ m) + { + py::class_>(m, "IInstance") + .def("displayName", &IInstance::displayName) + .def("gameName", &IInstance::gameName) + .def("gameDirectory", &IInstance::gameDirectory) + .def("isPortable", &IInstance::isPortable); + + py::class_(m, "IInstanceManager") + .def("currentInstance", &IInstanceManager::currentInstance) + .def("globalInstancePaths", &IInstanceManager::globalInstancePaths) + .def("getGlobalInstance", &IInstanceManager::getGlobalInstance); + } + + void add_idownload_manager_classes(py::module_ m) + { + py::class_(m, "IDownloadManager") + .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a) + .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, + "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("onDownloadComplete", &IDownloadManager::onDownloadComplete, + "callback"_a) + .def("onDownloadPaused", &IDownloadManager::onDownloadPaused, "callback"_a) + .def("onDownloadFailed", &IDownloadManager::onDownloadFailed, "callback"_a) + .def("onDownloadRemoved", &IDownloadManager::onDownloadRemoved, + "callback"_a); + } + + void add_iinstallation_manager_classes(py::module_ m) + { + // add this here to get proper typing + py::enum_(m, "InstallResult") + .value("SUCCESS", IPluginInstaller::RESULT_SUCCESS) + .value("FAILED", IPluginInstaller::RESULT_FAILED) + .value("CANCELED", IPluginInstaller::RESULT_CANCELED) + .value("MANUAL_REQUESTED", IPluginInstaller::RESULT_MANUALREQUESTED) + .value("NOT_ATTEMPTED", IPluginInstaller::RESULT_NOTATTEMPTED); + + py::class_(m, "IInstallationManager") + .def("getSupportedExtensions", + &IInstallationManager::getSupportedExtensions) + .def("extractFile", &IInstallationManager::extractFile, "entry"_a, + "silent"_a = false) + .def("extractFiles", &IInstallationManager::extractFiles, "entries"_a, + "silent"_a = false) + .def("createFile", &IInstallationManager::createFile, "entry"_a) + + // return a tuple to get back the mod name and the mod ID + .def( + "installArchive", + [](IInstallationManager* m, GuessedValue modName, + FileWrapper archive, int modId) { + auto result = m->installArchive(modName, archive, modId); + return std::make_tuple(result, static_cast(modName), + modId); + }, + "mod_name"_a, "archive"_a, "mod_id"_a = 0); + } + + void add_basic_bindings(py::module_ m) + { + add_versioninfo_classes(m); + add_executable_classes(m); + add_guessedstring_classes(m); + + add_ifiletree_bindings(m); + + 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, "PluginRequirementFactory") + // pluginDependency + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugins"_a) + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugin"_a) + // gameDependency + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "games"_a) + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "game"_a) + // diagnose + .def_static("diagnose", &PluginRequirementFactory::diagnose, "diagnose"_a) + // basic + .def_static("basic", &PluginRequirementFactory::basic, "checker"_a, + "description"_a); + + py::class_(m, "Mapping") + .def(py::init<>()) + .def(py::init([](QString src, QString dst, bool dir, bool crt) -> Mapping { + return {src, dst, dir, crt}; + }), + "source"_a, "destination"_a, "is_directory"_a, + "create_target"_a = false) + .def_readwrite("source", &Mapping::source) + .def_readwrite("destination", &Mapping::destination) + .def_readwrite("isDirectory", &Mapping::isDirectory) + .def_readwrite("createTarget", &Mapping::createTarget) + .def("__str__", [](Mapping const& m) { + return std::format(L"Mapping({}, {}, {}, {})", m.source.toStdWString(), + m.destination.toStdWString(), m.isDirectory, + m.createTarget); + }); + + // must be done BEFORE imodlist because there is a default argument to a + // IProfile* in the modlist class + py::class_>(m, "IProfile") + .def("name", &IProfile::name) + .def("absolutePath", &IProfile::absolutePath) + .def("localSavesEnabled", &IProfile::localSavesEnabled) + .def("localSettingsEnabled", &IProfile::localSettingsEnabled) + .def("invalidationActive", + [](const IProfile* p) { + bool supported; + bool active = p->invalidationActive(&supported); + return std::make_tuple(active, supported); + }) + .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a); + + add_ipluginlist_classes(m); + add_imodlist_classes(m); + add_iinstance_manager_classes(m); + add_idownload_manager_classes(m); + add_iinstallation_manager_classes(m); + add_iorganizer_classes(m); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/game_features.cpp b/src/mobase/wrappers/game_features.cpp new file mode 100644 index 00000000..655334b5 --- /dev/null +++ b/src/mobase/wrappers/game_features.cpp @@ -0,0 +1,422 @@ +#include "wrappers.h" + +#include + +#include "../pybind11_all.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pyfiletree.h" + +namespace py = pybind11; + +using namespace MOBase; +using namespace pybind11::literals; + +namespace mo2::python { + + class PyBSAInvalidation : public BSAInvalidation { + public: + bool isInvalidationBSA(const QString& bsaName) override + { + PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, isInvalidationBSA, bsaName); + } + void deactivate(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, deactivate, profile); + } + void activate(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, activate, profile); + } + bool prepareProfile(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, prepareProfile, profile); + } + }; + + class PyDataArchives : public DataArchives { + public: + QStringList vanillaArchives() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, vanillaArchives, ); + } + QStringList archives(const MOBase::IProfile* profile) const override + { + PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, archives, profile); + } + void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override + { + PYBIND11_OVERRIDE_PURE(void, DataArchives, addArchive, profile, index, + archiveName); + } + void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override + { + PYBIND11_OVERRIDE_PURE(void, DataArchives, removeArchive, profile, + archiveName); + } + }; + + class PyGamePlugins : public GamePlugins { + public: + void writePluginLists(const MOBase::IPluginList* pluginList) override + { + PYBIND11_OVERRIDE_PURE(void, GamePlugins, writePluginLists, pluginList); + } + void readPluginLists(MOBase::IPluginList* pluginList) override + { + // TODO: cannot update plugin list or create one from Python so this is + // useless + PYBIND11_OVERRIDE_PURE(void, GamePlugins, readPluginLists, pluginList); + } + QStringList getLoadOrder() override + { + PYBIND11_OVERRIDE_PURE(QStringList, GamePlugins, getLoadOrder, ); + } + bool lightPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, lightPluginsAreSupported, ); + } + bool mediumPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, mediumPluginsAreSupported, ); + } + bool blueprintPluginsAreSupported() override + { + PYBIND11_OVERRIDE(bool, GamePlugins, blueprintPluginsAreSupported, ); + } + }; + + class PyLocalSavegames : public LocalSavegames { + public: + MappingType mappings(const QDir& profileSaveDir) const override + { + PYBIND11_OVERRIDE_PURE(MappingType, LocalSavegames, mappings, + profileSaveDir); + } + bool prepareProfile(MOBase::IProfile* profile) override + { + PYBIND11_OVERRIDE_PURE(bool, LocalSavegames, prepareProfile, profile); + } + }; + + class PyModDataChecker : public ModDataChecker { + public: + CheckReturn + dataLooksValid(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE_PURE(CheckReturn, ModDataChecker, dataLooksValid, + fileTree); + } + + std::shared_ptr + fix(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE(std::shared_ptr, ModDataChecker, fix, + fileTree); + } + }; + + class PyModDataContent : public ModDataContent { + public: + std::vector getAllContents() const override + { + PYBIND11_OVERRIDE_PURE(std::vector, ModDataContent, + getAllContents, ); + ; + } + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE_PURE(std::vector, ModDataContent, getContentsFor, + fileTree); + } + }; + + class PySaveGameInfo : public SaveGameInfo { + public: + MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override + { + PYBIND11_OVERRIDE_PURE(MissingAssets, SaveGameInfo, getMissingAssets, + &save); + } + ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const override + { + PYBIND11_OVERRIDE_PURE(ISaveGameInfoWidget*, SaveGameInfo, + getSaveGameWidget, parent); + } + }; + + class PyScriptExtender : public ScriptExtender { + public: + QString BinaryName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, binaryName, ); + } + + QString PluginPath() const override + { + PYBIND11_OVERRIDE_PURE(DirectoryWrapper, ScriptExtender, pluginPath, ); + } + + QString loaderName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderName, ); + } + + QString loaderPath() const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, ScriptExtender, loaderPath, ); + } + + QString savegameExtension() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, savegameExtension, ); + } + + bool isInstalled() const override + { + PYBIND11_OVERRIDE_PURE(bool, ScriptExtender, isInstalled, ); + } + + QString getExtenderVersion() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, getExtenderVersion, ); + } + + WORD getArch() const override + { + PYBIND11_OVERRIDE_PURE(WORD, ScriptExtender, getArch, ); + } + }; + + class PyUnmanagedMods : public UnmanagedMods { + public: + QStringList mods(bool onlyOfficial) const override + { + PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, mods, onlyOfficial); + } + QString displayName(const QString& modName) const override + { + PYBIND11_OVERRIDE_PURE(QString, UnmanagedMods, displayName, modName); + } + QFileInfo referenceFile(const QString& modName) const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, UnmanagedMods, referenceFile, modName); + } + QStringList secondaryFiles(const QString& modName) const override + { + return toQStringList([&] { + PYBIND11_OVERRIDE_PURE(QList, UnmanagedMods, + secondaryFiles, modName); + }()); + } + }; + + void add_game_feature_bindings(pybind11::module_ m) + { + // this is just to allow accepting GameFeature in function, we do not expose + // anything from game feature to Python since typeInfo() is useless in Python + // + py::class_>(m, "GameFeature"); + + // BSAInvalidation + + py::class_>(m, "BSAInvalidation") + .def(py::init<>()) + .def("isInvalidationBSA", &BSAInvalidation::isInvalidationBSA, "name"_a) + .def("deactivate", &BSAInvalidation::deactivate, "profile"_a) + .def("activate", &BSAInvalidation::activate, "profile"_a); + + // DataArchives + + py::class_>(m, "DataArchives") + .def(py::init<>()) + .def("vanillaArchives", &DataArchives::vanillaArchives) + .def("archives", &DataArchives::archives, "profile"_a) + .def("addArchive", &DataArchives::addArchive, "profile"_a, "index"_a, + "name"_a) + .def("removeArchive", &DataArchives::removeArchive, "profile"_a, "name"_a); + + // GamePlugins + + py::class_>(m, "GamePlugins") + .def(py::init<>()) + .def("writePluginLists", &GamePlugins::writePluginLists, "plugin_list"_a) + .def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a) + .def("getLoadOrder", &GamePlugins::getLoadOrder) + .def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported) + .def("mediumPluginsAreSupported", &GamePlugins::mediumPluginsAreSupported) + .def("blueprintPluginsAreSupported", + &GamePlugins::blueprintPluginsAreSupported); + + // LocalSavegames + + py::class_>(m, "LocalSavegames") + .def(py::init<>()) + .def("mappings", &LocalSavegames::mappings, "profile_save_dir"_a) + .def("prepareProfile", &LocalSavegames::prepareProfile, "profile"_a); + + // ModDataChecker + + py::class_> + pyModDataChecker(m, "ModDataChecker"); + + py::enum_(pyModDataChecker, "CheckReturn") + .value("INVALID", ModDataChecker::CheckReturn::INVALID) + .value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE) + .value("VALID", ModDataChecker::CheckReturn::VALID) + .export_values(); + + pyModDataChecker.def(py::init<>()) + .def("dataLooksValid", &ModDataChecker::dataLooksValid, "filetree"_a) + .def("fix", &ModDataChecker::fix, "filetree"_a); + + // ModDataContent + py::class_> + pyModDataContent(m, "ModDataContent"); + + py::class_(pyModDataContent, "Content") + .def(py::init(), "id"_a, "name"_a, "icon"_a, + "filter_only"_a = false) + .def_property_readonly("id", &ModDataContent::Content::id) + .def_property_readonly("name", &ModDataContent::Content::name) + .def_property_readonly("icon", &ModDataContent::Content::icon) + .def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter); + + pyModDataContent.def(py::init<>()) + .def("getAllContents", &ModDataContent::getAllContents) + .def("getContentsFor", &ModDataContent::getContentsFor, "filetree"_a); + + // SaveGameInfo + + py::class_>(m, "SaveGameInfo") + .def(py::init<>()) + .def("getMissingAssets", &SaveGameInfo::getMissingAssets, "save"_a) + .def("getSaveGameWidget", &SaveGameInfo::getSaveGameWidget, + py::return_value_policy::reference, "parent"_a); + + // ScriptExtender + + py::class_>(m, "ScriptExtender") + .def(py::init<>()) + .def("binaryName", &ScriptExtender::BinaryName) + .def("pluginPath", wrap_return_for_directory(&ScriptExtender::PluginPath)) + .def("loaderName", &ScriptExtender::loaderName) + .def("loaderPath", wrap_return_for_filepath(&ScriptExtender::loaderPath)) + .def("savegameExtension", &ScriptExtender::savegameExtension) + .def("isInstalled", &ScriptExtender::isInstalled) + .def("getExtenderVersion", &ScriptExtender::getExtenderVersion) + .def("getArch", &ScriptExtender::getArch); + + // UnmanagedMods + + py::class_>(m, "UnmanagedMods") + .def(py::init<>()) + .def("mods", &UnmanagedMods::mods, "official_only"_a) + .def("displayName", &UnmanagedMods::displayName, "mod_name"_a) + .def("referenceFile", + wrap_return_for_filepath(&UnmanagedMods::referenceFile), "mod_name"_a) + .def( + "secondaryFiles", + [](UnmanagedMods* m, const QString& modName) -> QList { + auto result = m->secondaryFiles(modName); + return {result.begin(), result.end()}; + }, + "mod_name"_a); + } + + void add_igamefeatures_classes(py::module_ m) + { + py::class_(m, "IGameFeatures") + .def("registerFeature", + py::overload_cast, + int, bool>(&IGameFeatures::registerFeature), + "games"_a, "feature"_a, "priority"_a, "replace"_a = false) + .def("registerFeature", + py::overload_cast, + int, bool>(&IGameFeatures::registerFeature), + "game"_a, "feature"_a, "priority"_a, "replace"_a = false) + .def("registerFeature", + py::overload_cast, int, bool>( + &IGameFeatures::registerFeature), + "feature"_a, "priority"_a, "replace"_a = false) + .def("unregisterFeature", &IGameFeatures::unregisterFeature, "feature"_a) + .def("unregisterFeatures", &unregister_feature, "feature_type"_a) + .def("gameFeature", &extract_feature, "feature_type"_a, + py ::return_value_policy::reference); + } + +} // namespace mo2::python + +namespace mo2::python { + + class GameFeaturesHelper { + template + static void helper(F&& f, std::index_sequence) + { + (f(static_cast< + std::tuple_element_t>( + nullptr)), + ...); + } + + public: + // apply the function f on a null-pointer of type Feature* for each game + // feature + template + static void apply(F&& f) + { + helper(f, std::make_index_sequence< + std::tuple_size_v>{}); + } + }; + + pybind11::object extract_feature(IGameFeatures const& gameFeatures, + pybind11::object type) + { + py::object py_feature = py::none(); + GameFeaturesHelper::apply([&](Feature*) { + if (py::type::of().is(type)) { + py_feature = py::cast(gameFeatures.gameFeature(), + py::return_value_policy::reference); + } + }); + return py_feature; + } + + int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type) + { + int count = 0; + GameFeaturesHelper::apply([&](Feature*) { + if (py::type::of().is(type)) { + count = gameFeatures.unregisterFeatures(); + } + }); + return count; + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/known_folders.h b/src/mobase/wrappers/known_folders.h new file mode 100644 index 00000000..f74238b5 --- /dev/null +++ b/src/mobase/wrappers/known_folders.h @@ -0,0 +1,155 @@ +#include + +namespace mo2::python { + + struct KnownFolder { + const char* name; + KNOWNFOLDERID guid; + }; + + const std::array KNOWN_FOLDERS{{ + {"AccountPictures", FOLDERID_AccountPictures}, + {"AddNewPrograms", FOLDERID_AddNewPrograms}, + {"AdminTools", FOLDERID_AdminTools}, + {"AllAppMods", FOLDERID_AllAppMods}, + {"AppCaptures", FOLDERID_AppCaptures}, + {"AppDataDesktop", FOLDERID_AppDataDesktop}, + {"AppDataDocuments", FOLDERID_AppDataDocuments}, + {"AppDataFavorites", FOLDERID_AppDataFavorites}, + {"AppDataProgramData", FOLDERID_AppDataProgramData}, + {"ApplicationShortcuts", FOLDERID_ApplicationShortcuts}, + {"AppsFolder", FOLDERID_AppsFolder}, + {"AppUpdates", FOLDERID_AppUpdates}, + {"CameraRoll", FOLDERID_CameraRoll}, + {"CameraRollLibrary", FOLDERID_CameraRollLibrary}, + {"CDBurning", FOLDERID_CDBurning}, + {"ChangeRemovePrograms", FOLDERID_ChangeRemovePrograms}, + {"CommonAdminTools", FOLDERID_CommonAdminTools}, + {"CommonOEMLinks", FOLDERID_CommonOEMLinks}, + {"CommonPrograms", FOLDERID_CommonPrograms}, + {"CommonStartMenu", FOLDERID_CommonStartMenu}, + {"CommonStartMenuPlaces", FOLDERID_CommonStartMenuPlaces}, + {"CommonStartup", FOLDERID_CommonStartup}, + {"CommonTemplates", FOLDERID_CommonTemplates}, + {"ComputerFolder", FOLDERID_ComputerFolder}, + {"ConflictFolder", FOLDERID_ConflictFolder}, + {"ConnectionsFolder", FOLDERID_ConnectionsFolder}, + {"Contacts", FOLDERID_Contacts}, + {"ControlPanelFolder", FOLDERID_ControlPanelFolder}, + {"Cookies", FOLDERID_Cookies}, + {"CurrentAppMods", FOLDERID_CurrentAppMods}, + {"Desktop", FOLDERID_Desktop}, + {"DevelopmentFiles", FOLDERID_DevelopmentFiles}, + {"Device", FOLDERID_Device}, + {"DeviceMetadataStore", FOLDERID_DeviceMetadataStore}, + {"Documents", FOLDERID_Documents}, + {"DocumentsLibrary", FOLDERID_DocumentsLibrary}, + {"Downloads", FOLDERID_Downloads}, + {"Favorites", FOLDERID_Favorites}, + {"Fonts", FOLDERID_Fonts}, + {"Games", FOLDERID_Games}, + {"GameTasks", FOLDERID_GameTasks}, + {"History", FOLDERID_History}, + {"HomeGroup", FOLDERID_HomeGroup}, + {"HomeGroupCurrentUser", FOLDERID_HomeGroupCurrentUser}, + {"ImplicitAppShortcuts", FOLDERID_ImplicitAppShortcuts}, + {"InternetCache", FOLDERID_InternetCache}, + {"InternetFolder", FOLDERID_InternetFolder}, + {"Libraries", FOLDERID_Libraries}, + {"Links", FOLDERID_Links}, + {"LocalAppData", FOLDERID_LocalAppData}, + {"LocalAppDataLow", FOLDERID_LocalAppDataLow}, + {"LocalDocuments", FOLDERID_LocalDocuments}, + {"LocalDownloads", FOLDERID_LocalDownloads}, + {"LocalizedResourcesDir", FOLDERID_LocalizedResourcesDir}, + {"LocalMusic", FOLDERID_LocalMusic}, + {"LocalPictures", FOLDERID_LocalPictures}, + {"LocalStorage", FOLDERID_LocalStorage}, + {"LocalVideos", FOLDERID_LocalVideos}, + {"Music", FOLDERID_Music}, + {"MusicLibrary", FOLDERID_MusicLibrary}, + {"NetHood", FOLDERID_NetHood}, + {"NetworkFolder", FOLDERID_NetworkFolder}, + {"Objects3D", FOLDERID_Objects3D}, + {"OneDrive", FOLDERID_OneDrive}, + {"OriginalImages", FOLDERID_OriginalImages}, + {"PhotoAlbums", FOLDERID_PhotoAlbums}, + {"Pictures", FOLDERID_Pictures}, + {"PicturesLibrary", FOLDERID_PicturesLibrary}, + {"Playlists", FOLDERID_Playlists}, + {"PrintersFolder", FOLDERID_PrintersFolder}, + {"PrintHood", FOLDERID_PrintHood}, + {"Profile", FOLDERID_Profile}, + {"ProgramData", FOLDERID_ProgramData}, + {"ProgramFiles", FOLDERID_ProgramFiles}, + {"ProgramFilesCommon", FOLDERID_ProgramFilesCommon}, + {"ProgramFilesCommonX64", FOLDERID_ProgramFilesCommonX64}, + {"ProgramFilesCommonX86", FOLDERID_ProgramFilesCommonX86}, + {"ProgramFilesX64", FOLDERID_ProgramFilesX64}, + {"ProgramFilesX86", FOLDERID_ProgramFilesX86}, + {"Programs", FOLDERID_Programs}, + {"Public", FOLDERID_Public}, + {"PublicDesktop", FOLDERID_PublicDesktop}, + {"PublicDocuments", FOLDERID_PublicDocuments}, + {"PublicDownloads", FOLDERID_PublicDownloads}, + {"PublicGameTasks", FOLDERID_PublicGameTasks}, + {"PublicLibraries", FOLDERID_PublicLibraries}, + {"PublicMusic", FOLDERID_PublicMusic}, + {"PublicPictures", FOLDERID_PublicPictures}, + {"PublicRingtones", FOLDERID_PublicRingtones}, + {"PublicUserTiles", FOLDERID_PublicUserTiles}, + {"PublicVideos", FOLDERID_PublicVideos}, + {"QuickLaunch", FOLDERID_QuickLaunch}, + {"Recent", FOLDERID_Recent}, + {"RecordedCalls", FOLDERID_RecordedCalls}, + {"RecordedTVLibrary", FOLDERID_RecordedTVLibrary}, + {"RecycleBinFolder", FOLDERID_RecycleBinFolder}, + {"ResourceDir", FOLDERID_ResourceDir}, + {"RetailDemo", FOLDERID_RetailDemo}, + {"Ringtones", FOLDERID_Ringtones}, + {"RoamedTileImages", FOLDERID_RoamedTileImages}, + {"RoamingAppData", FOLDERID_RoamingAppData}, + {"RoamingTiles", FOLDERID_RoamingTiles}, + {"SampleMusic", FOLDERID_SampleMusic}, + {"SamplePictures", FOLDERID_SamplePictures}, + {"SamplePlaylists", FOLDERID_SamplePlaylists}, + {"SampleVideos", FOLDERID_SampleVideos}, + {"SavedGames", FOLDERID_SavedGames}, + {"SavedPictures", FOLDERID_SavedPictures}, + {"SavedPicturesLibrary", FOLDERID_SavedPicturesLibrary}, + {"SavedSearches", FOLDERID_SavedSearches}, + {"Screenshots", FOLDERID_Screenshots}, + {"SEARCH_CSC", FOLDERID_SEARCH_CSC}, + {"SEARCH_MAPI", FOLDERID_SEARCH_MAPI}, + {"SearchHistory", FOLDERID_SearchHistory}, + {"SearchHome", FOLDERID_SearchHome}, + {"SearchTemplates", FOLDERID_SearchTemplates}, + {"SendTo", FOLDERID_SendTo}, + {"SidebarDefaultParts", FOLDERID_SidebarDefaultParts}, + {"SidebarParts", FOLDERID_SidebarParts}, + {"SkyDrive", FOLDERID_SkyDrive}, + {"SkyDriveCameraRoll", FOLDERID_SkyDriveCameraRoll}, + {"SkyDriveDocuments", FOLDERID_SkyDriveDocuments}, + {"SkyDriveMusic", FOLDERID_SkyDriveMusic}, + {"SkyDrivePictures", FOLDERID_SkyDrivePictures}, + {"StartMenu", FOLDERID_StartMenu}, + {"StartMenuAllPrograms", FOLDERID_StartMenuAllPrograms}, + {"Startup", FOLDERID_Startup}, + {"SyncManagerFolder", FOLDERID_SyncManagerFolder}, + {"SyncResultsFolder", FOLDERID_SyncResultsFolder}, + {"SyncSetupFolder", FOLDERID_SyncSetupFolder}, + {"System", FOLDERID_System}, + {"SystemX86", FOLDERID_SystemX86}, + {"Templates", FOLDERID_Templates}, + {"UserPinned", FOLDERID_UserPinned}, + {"UserProfiles", FOLDERID_UserProfiles}, + {"UserProgramFiles", FOLDERID_UserProgramFiles}, + {"UserProgramFilesCommon", FOLDERID_UserProgramFilesCommon}, + {"UsersFiles", FOLDERID_UsersFiles}, + {"UsersLibraries", FOLDERID_UsersLibraries}, + {"Videos", FOLDERID_Videos}, + {"VideosLibrary", FOLDERID_VideosLibrary}, + {"Windows", FOLDERID_Windows}, + }}; + +} // namespace mo2::python diff --git a/src/mobase/wrappers/pyfiletree.cpp b/src/mobase/wrappers/pyfiletree.cpp new file mode 100644 index 00000000..6dacf26e --- /dev/null +++ b/src/mobase/wrappers/pyfiletree.cpp @@ -0,0 +1,329 @@ +#include "pyfiletree.h" + +#include +#include + +#include "../pybind11_all.h" + +#include +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::detail { + + // filetree implementation for testing purpose + // + class PyFileTree : public IFileTree { + public: + using callback_t = std::function; + + PyFileTree(std::shared_ptr parent, QString name, + callback_t callback) + : FileTreeEntry(parent, name), IFileTree(), m_Callback(callback) + { + } + + std::shared_ptr addFile(QString name, bool) override + { + if (m_Callback && !m_Callback(name, false)) { + throw UnsupportedOperationException("File rejected by callback."); + } + return IFileTree::addFile(name); + } + + std::shared_ptr addDirectory(QString name) override + { + if (m_Callback && !m_Callback(name, true)) { + throw UnsupportedOperationException("Directory rejected by callback."); + } + return IFileTree::addDirectory(name); + } + + protected: + std::shared_ptr + makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name, m_Callback); + } + + bool doPopulate([[maybe_unused]] std::shared_ptr parent, + std::vector>&) const override + { + return true; + } + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name(), m_Callback); + } + + private: + callback_t m_Callback; + }; + +} // namespace mo2::detail + +#pragma optimize("", off) + +namespace pybind11 { + const void* polymorphic_type_hook::get(const FileTreeEntry* src, + const std::type_info*& type) + { + if (auto p = dynamic_cast(src)) { + type = &typeid(IFileTree); + return p; + } + return src; + } +} // namespace pybind11 + +namespace mo2::python { + + void add_ifiletree_bindings(pybind11::module_& m) + { + // FileTreeEntry class: + auto fileTreeEntryClass = + py::class_>(m, + "FileTreeEntry"); + + // IFileTree class: + auto iFileTreeClass = + py::class_>( + m, "IFileTree", py::multiple_inheritance()); + + // this is FILE_OR_DIRECTORY but as a FileType since we kind of cheat for the + // exposure in Python and this help pybind11 creates proper typing + + const auto FILE_OR_DIRECTORY = static_cast( + FileTreeEntry::FILE_OR_DIRECTORY.toInt()); + + // we do not use the enum directly, we will mostly bind the FileTypes + // (with an S) + py::enum_(fileTreeEntryClass, "FileTypes", + py::arithmetic{}) + .value("FILE", FileTreeEntry::FileType::FILE) + .value("DIRECTORY", FileTreeEntry::FileType::DIRECTORY) + .value("FILE_OR_DIRECTORY", FILE_OR_DIRECTORY) + .export_values(); + + fileTreeEntryClass + + .def("isFile", &FileTreeEntry::isFile) + .def("isDir", &FileTreeEntry::isDir) + .def("fileType", &FileTreeEntry::fileType) + .def("name", &FileTreeEntry::name) + .def("suffix", &FileTreeEntry::suffix) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QStringList suffixes) { + return entry->hasSuffix(suffixes); + }, + py::arg("suffixes")) + .def( + "hasSuffix", + [](FileTreeEntry* entry, QString suffix) { + return entry->hasSuffix(suffix); + }, + py::arg("suffix")) + .def("parent", py::overload_cast<>(&FileTreeEntry::parent)) + .def("path", &FileTreeEntry::path, py::arg("sep") = "\\") + .def("pathFrom", &FileTreeEntry::pathFrom, py::arg("tree"), + py::arg("sep") = "\\") + + // Mutable operation: + .def("detach", &FileTreeEntry::detach) + .def("moveTo", &FileTreeEntry::moveTo, py::arg("tree")) + + // Special methods: + .def("__eq__", + [](const FileTreeEntry* entry, QString other) { + return entry->compare(other) == 0; + }) + .def("__eq__", + [](const FileTreeEntry* entry, std::shared_ptr other) { + return entry == other.get(); + }) + + // Special methods for debug: + .def("__repr__", [](const FileTreeEntry* entry) { + return "FileTreeEntry(\"" + entry->name() + "\")"; + }); + + py::enum_(iFileTreeClass, "InsertPolicy") + .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS) + .value("REPLACE", IFileTree::InsertPolicy::REPLACE) + .value("MERGE", IFileTree::InsertPolicy::MERGE) + .export_values(); + + py::enum_(iFileTreeClass, "WalkReturn") + .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) + .value("STOP", IFileTree::WalkReturn::STOP) + .value("SKIP", IFileTree::WalkReturn::SKIP) + .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_(iFileTreeClass, "GlobPatternType") + .value("GLOB", GlobPatternType::GLOB) + .value("REGEX", GlobPatternType::REGEX) + .export_values(); + + // Non-mutable operations: + iFileTreeClass.def("exists", + py::overload_cast( + &IFileTree::exists, py::const_), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def( + "find", py::overload_cast(&IFileTree::find), + py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY); + iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"), + py::arg("sep") = "\\"); + + iFileTreeClass.def( + "walk", + py::overload_cast< + std::function)>, + 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 tree) { + return make_generator(walk(tree)); + }); + + iFileTreeClass.def( + "glob", + [](std::shared_ptr tree, QString pattern, + GlobPatternType patternType) { + return make_generator(glob(tree, pattern, patternType)); + }, + py::arg("pattern"), py::arg("type") = GlobPatternType::GLOB); + + // Kind-of-static operations: + iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree, + py::arg("name") = ""); + + // addFile() and addDirectory throws exception instead of returning null + // pointer in order to have better traces. + iFileTreeClass.def( + "addFile", + [](IFileTree* w, QString path, bool replaceIfExists) { + auto result = w->addFile(path, replaceIfExists); + if (result == nullptr) { + throw std::logic_error("addFile failed"); + } + return result; + }, + py::arg("path"), py::arg("replace_if_exists") = false); + iFileTreeClass.def( + "addDirectory", + [](IFileTree* w, QString path) { + auto result = w->addDirectory(path); + if (result == nullptr) { + throw std::logic_error("addDirectory failed"); + } + return result; + }, + py::arg("path")); + + // Merge needs custom return types depending if the user wants overrides + // or not. A failure is translated into an exception for easier tracing + // and handling. + iFileTreeClass.def( + "merge", + [](IFileTree* p, std::shared_ptr other, bool returnOverwrites) + -> std::variant { + IFileTree::OverwritesType overwrites; + auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr); + if (result == IFileTree::MERGE_FAILED) { + throw std::logic_error("merge failed"); + } + if (returnOverwrites) { + return {overwrites}; + } + return {result}; + }, + py::arg("other"), py::arg("overwrites") = false); + + // Insert and erase returns an iterator, which makes no sense in python, + // so we convert it to bool. Erase is also renamed "remove" since + // "erase" is very C++. + iFileTreeClass.def( + "insert", + [](IFileTree* p, std::shared_ptr entry, + IFileTree::InsertPolicy insertPolicy) { + return p->insert(entry, insertPolicy) == p->end(); + }, + py::arg("entry"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def( + "remove", + [](IFileTree* p, QString name) { + return p->erase(name).first != p->end(); + }, + py::arg("name")); + iFileTreeClass.def( + "remove", + [](IFileTree* p, std::shared_ptr entry) { + return p->erase(entry) != p->end(); + }, + py::arg("entry")); + + iFileTreeClass.def("move", &IFileTree::move, py::arg("entry"), py::arg("path"), + py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + iFileTreeClass.def( + "copy", + [](IFileTree* w, std::shared_ptr entry, QString path, + IFileTree::InsertPolicy insertPolicy) { + auto result = w->copy(entry, path, insertPolicy); + if (result == nullptr) { + throw std::logic_error("copy failed"); + } + return result; + }, + py::arg("entry"), py::arg("path") = "", + py::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS); + + iFileTreeClass.def("clear", &IFileTree::clear); + iFileTreeClass.def("removeAll", &IFileTree::removeAll, py::arg("names")); + iFileTreeClass.def("removeIf", &IFileTree::removeIf, py::arg("filter")); + + // Special methods: + iFileTreeClass.def("__getitem__", + py::overload_cast(&IFileTree::at)); + + iFileTreeClass.def("__iter__", [](IFileTree* tree) { + return py::make_iterator(*tree); + }); + iFileTreeClass.def("__len__", &IFileTree::size); + iFileTreeClass.def( + "__bool__", +[](const IFileTree* tree) { + return !tree->empty(); + }); + iFileTreeClass.def( + "__repr__", +[](const IFileTree* entry) { + return "IFileTree(\"" + entry->name() + "\")"; + }); + } + + void add_make_tree_function(pybind11::module_& m) + { + m.def( + "makeTree", + [](mo2::detail::PyFileTree::callback_t callback) + -> std::shared_ptr { + return std::make_shared(nullptr, "", callback); + }, + py::arg("callback") = mo2::detail::PyFileTree::callback_t{}); + } + +} // namespace mo2::python + +#pragma optimize("", on) diff --git a/src/mobase/wrappers/pyfiletree.h b/src/mobase/wrappers/pyfiletree.h new file mode 100644 index 00000000..0e946656 --- /dev/null +++ b/src/mobase/wrappers/pyfiletree.h @@ -0,0 +1,34 @@ +#ifndef MO2_PYTHON_FILETREE_H +#define MO2_PYTHON_FILETREE_H + +#include "../pybind11_all.h" + +#include + +namespace pybind11 { + template <> + struct polymorphic_type_hook { + static const void* get(const MOBase::FileTreeEntry* src, + const std::type_info*& type); + }; +} // namespace pybind11 + +namespace mo2::python { + + /** + * @brief Add bindings for FileTreeEntry andIFileTree to the given module. + * + * @param mobase Module to add the bindings to. + */ + void add_ifiletree_bindings(pybind11::module_& m); + + /** + * @brief Add makeTree() function to the given module, useful for debugging. + * + * @param mobase Module to add the function to. + */ + void add_make_tree_function(pybind11::module_& m); + +} // namespace mo2::python + +#endif diff --git a/src/mobase/wrappers/pyplugins.cpp b/src/mobase/wrappers/pyplugins.cpp new file mode 100644 index 00000000..bcce0df2 --- /dev/null +++ b/src/mobase/wrappers/pyplugins.cpp @@ -0,0 +1,262 @@ +#include "wrappers.h" + +#include + +#include "pyplugins.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace MOBase; + +namespace mo2::python { + + // this one is kind of big so it has its own function + void add_iplugingame_bindings(pybind11::module_ m) + { + py::enum_(m, "LoadOrderMechanism") + .value("None", IPluginGame::LoadOrderMechanism::None) + .value("FileTime", IPluginGame::LoadOrderMechanism::FileTime) + .value("PluginsTxt", IPluginGame::LoadOrderMechanism::PluginsTxt) + + .value("NONE", IPluginGame::LoadOrderMechanism::None) + .value("FILE_TIME", IPluginGame::LoadOrderMechanism::FileTime) + .value("PLUGINS_TXT", IPluginGame::LoadOrderMechanism::PluginsTxt); + + py::enum_(m, "SortMechanism") + .value("NONE", IPluginGame::SortMechanism::NONE) + .value("MLOX", IPluginGame::SortMechanism::MLOX) + .value("BOSS", IPluginGame::SortMechanism::BOSS) + .value("LOOT", IPluginGame::SortMechanism::LOOT); + + // this does not actually do the conversion, but might be convenient + // for accessing the names for enum bits + py::enum_(m, "ProfileSetting", py::arithmetic()) + .value("mods", IPluginGame::MODS) + .value("configuration", IPluginGame::CONFIGURATION) + .value("savegames", IPluginGame::SAVEGAMES) + .value("preferDefaults", IPluginGame::PREFER_DEFAULTS) + + .value("MODS", IPluginGame::MODS) + .value("CONFIGURATION", IPluginGame::CONFIGURATION) + .value("SAVEGAMES", IPluginGame::SAVEGAMES) + .value("PREFER_DEFAULTS", IPluginGame::PREFER_DEFAULTS); + + py::class_>( + m, "IPluginGame", py::multiple_inheritance()) + .def(py::init<>()) + .def("detectGame", &IPluginGame::detectGame) + .def("gameName", &IPluginGame::gameName) + .def("displayGameName", &IPluginGame::displayGameName) + .def("initializeProfile", &IPluginGame::initializeProfile, "directory"_a, + "settings"_a) + .def("listSaves", &IPluginGame::listSaves, "folder"_a) + .def("isInstalled", &IPluginGame::isInstalled) + .def("gameIcon", &IPluginGame::gameIcon) + .def("gameDirectory", &IPluginGame::gameDirectory) + .def("dataDirectory", &IPluginGame::dataDirectory) + .def("modDataDirectory", &IPluginGame::modDataDirectory) + .def("secondaryDataDirectories", &IPluginGame::secondaryDataDirectories) + .def("setGamePath", &IPluginGame::setGamePath, "path"_a) + .def("documentsDirectory", &IPluginGame::documentsDirectory) + .def("savesDirectory", &IPluginGame::savesDirectory) + .def("executables", &IPluginGame::executables) + .def("executableForcedLoads", &IPluginGame::executableForcedLoads) + .def("steamAPPId", &IPluginGame::steamAPPId) + .def("primaryPlugins", &IPluginGame::primaryPlugins) + .def("enabledPlugins", &IPluginGame::enabledPlugins) + .def("gameVariants", &IPluginGame::gameVariants) + .def("setGameVariant", &IPluginGame::setGameVariant, "variant"_a) + .def("binaryName", &IPluginGame::binaryName) + .def("gameShortName", &IPluginGame::gameShortName) + .def("lootGameName", &IPluginGame::lootGameName) + .def("primarySources", &IPluginGame::primarySources) + .def("validShortNames", &IPluginGame::validShortNames) + .def("gameNexusName", &IPluginGame::gameNexusName) + .def("iniFiles", &IPluginGame::iniFiles) + .def("DLCPlugins", &IPluginGame::DLCPlugins) + .def("CCPlugins", &IPluginGame::CCPlugins) + .def("loadOrderMechanism", &IPluginGame::loadOrderMechanism) + .def("sortMechanism", &IPluginGame::sortMechanism) + .def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID) + .def("nexusGameID", &IPluginGame::nexusGameID) + .def("looksValid", &IPluginGame::looksValid, "directory"_a) + .def("gameVersion", &IPluginGame::gameVersion) + .def("getLauncherName", &IPluginGame::getLauncherName) + .def("getSupportURL", &IPluginGame::getSupportURL) + .def("getModMappings", &IPluginGame::getModMappings); + } + + // multiple installers + void add_iplugininstaller_bindings(pybind11::module_ m) + { + // this is bind but should not be inherited in Python - does not make sense, + // having it makes it simpler to bind the Simple and Custom installers + py::class_, IPlugin, + std::unique_ptr>( + m, "IPluginInstaller", py::multiple_inheritance()) + .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, "tree"_a) + .def("priority", &IPluginInstaller::priority) + .def("onInstallationStart", &IPluginInstaller::onInstallationStart, + "archive"_a, "reinstallation"_a, "current_mod"_a) + .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, "result"_a, + "new_mod"_a) + .def("isManualInstaller", &IPluginInstaller::isManualInstaller) + .def("setParentWidget", &IPluginInstaller::setParentWidget, "parent"_a) + .def("setInstallationManager", &IPluginInstaller::setInstallationManager, + "manager"_a) + .def("_parentWidget", + &PyPluginInstallerBase::parentWidget) + .def("_manager", &PyPluginInstallerBase::manager, + py::return_value_policy::reference); + + py::class_>( + m, "IPluginInstallerSimple", py::multiple_inheritance()) + .def(py::init<>()) + + // note: keeping the variant here even if we always return a tuple + // to be consistent with the wrapper and have proper stubs generation. + .def( + "install", + [](IPluginInstallerSimple* p, GuessedValue& modName, + std::shared_ptr& tree, QString& version, + int& nexusID) -> PyPluginInstallerSimple::py_install_return_type { + auto result = p->install(modName, tree, version, nexusID); + return std::make_tuple(result, tree, version, nexusID); + }, + "name"_a, "tree"_a, "version"_a, "nexus_id"_a); + + py::class_>( + m, "IPluginInstallerCustom", py::multiple_inheritance()) + .def(py::init<>()) + .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported, + "archive_name"_a) + .def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions) + .def("install", &IPluginInstallerCustom::install, "mod_name"_a, + "game_name"_a, "archive_name"_a, "version"_a, "nexus_id"_a); + } + + void add_plugins_bindings(pybind11::module_ m) + { + py::class_>( + m, "IPluginBase", py::multiple_inheritance()) + .def(py::init<>()) + .def("init", &IPlugin::init, "organizer"_a) + .def("name", &IPlugin::name) + .def("localizedName", &IPlugin::localizedName) + .def("master", &IPlugin::master) + .def("author", &IPlugin::author) + .def("description", &IPlugin::description) + .def("version", &IPlugin::version) + .def("requirements", &IPlugin::requirements) + .def("settings", &IPlugin::settings) + .def("enabledByDefault", &IPlugin::enabledByDefault); + + py::class_>(m, "IPlugin", + py::multiple_inheritance()) + .def(py::init<>()); + + py::class_>( + m, "IPluginFileMapper", py::multiple_inheritance()) + .def(py::init<>()) + .def("mappings", &IPluginFileMapper::mappings); + + py::class_>( + m, "IPluginDiagnose", py::multiple_inheritance()) + .def(py::init<>()) + .def("activeProblems", &IPluginDiagnose::activeProblems) + .def("shortDescription", &IPluginDiagnose::shortDescription, "key"_a) + .def("fullDescription", &IPluginDiagnose::fullDescription, "key"_a) + .def("hasGuidedFix", &IPluginDiagnose::hasGuidedFix, "key"_a) + .def("startGuidedFix", &IPluginDiagnose::startGuidedFix, "key"_a) + .def("_invalidate", &PyPluginDiagnose::invalidate); + + py::class_>( + m, "IPluginTool", py::multiple_inheritance()) + .def(py::init<>()) + .def("displayName", &IPluginTool::displayName) + .def("tooltip", &IPluginTool::tooltip) + .def("icon", &IPluginTool::icon) + .def("display", &IPluginTool::display) + .def("setParentWidget", &IPluginTool::setParentWidget) + .def("_parentWidget", &PyPluginTool::parentWidget); + + py::class_>( + m, "IPluginPreview", py::multiple_inheritance()) + .def(py::init<>()) + .def("supportedExtensions", &IPluginPreview::supportedExtensions) + .def("supportsArchives", &IPluginPreview::supportsArchives) + .def("genFilePreview", &IPluginPreview::genFilePreview, "filename"_a, + "max_size"_a) + .def("genDataPreview", &IPluginPreview::genDataPreview, "file_data"_a, + "filename"_a, "max_size"_a); + + py::class_>( + m, "IPluginModPage", py::multiple_inheritance()) + .def(py::init<>()) + .def("displayName", &IPluginModPage::displayName) + .def("icon", &IPluginModPage::icon) + .def("pageURL", &IPluginModPage::pageURL) + .def("useIntegratedBrowser", &IPluginModPage::useIntegratedBrowser) + .def("handlesDownload", &IPluginModPage::handlesDownload, "page_url"_a, + "download_url"_a, "fileinfo"_a) + .def("setParentWidget", &IPluginModPage::setParentWidget, "parent"_a) + .def("_parentWidget", &PyPluginModPage::parentWidget); + + add_iplugingame_bindings(m); + add_iplugininstaller_bindings(m); + } + + struct extract_plugins_helper { + QList objects; + + template + void append_if_instance(pybind11::object plugin_obj) + { + if (py::isinstance(plugin_obj)) { + objects.append(plugin_obj.cast()); + } + } + }; + + QList extract_plugins(pybind11::object plugin_obj) + { + extract_plugins_helper helper; + + // we need to check the trampoline class for these since the interfaces do not + // extend IPlugin + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(plugin_obj); + + // we need to check the two installer types because IPluginInstaller does not + // inherit QObject, and the trampoline do not have a common ancestor + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + if (helper.objects.isEmpty()) { + helper.append_if_instance(plugin_obj); + } + + // tie the lifetime of the Python object to the lifetime of the QObject + for (auto* object : helper.objects) { + py::qt::set_qt_owner(object, plugin_obj); + } + + return helper.objects; + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/pyplugins.h b/src/mobase/wrappers/pyplugins.h new file mode 100644 index 00000000..17f7ab56 --- /dev/null +++ b/src/mobase/wrappers/pyplugins.h @@ -0,0 +1,527 @@ +#ifndef PYTHON_WRAPPERS_PYPLUGINS_H +#define PYTHON_WRAPPERS_PYPLUGINS_H + +#include "../pybind11_all.h" + +#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 +namespace mo2::python { + + using namespace MOBase; + + // we need two base trampoline because IPluginGame has some final methods. + template + class PyPluginBaseNoFinal : public PluginBase { + public: + using PluginBase::PluginBase; + + PyPluginBaseNoFinal(PyPluginBaseNoFinal const&) = delete; + PyPluginBaseNoFinal(PyPluginBaseNoFinal&&) = delete; + PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal const&) = delete; + PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal&&) = delete; + + bool init(IOrganizer* organizer) override + { + PYBIND11_OVERRIDE_PURE(bool, PluginBase, init, organizer); + } + QString name() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, name, ); + } + QString localizedName() const override + { + PYBIND11_OVERRIDE(QString, PluginBase, localizedName, ); + } + QString master() const override + { + PYBIND11_OVERRIDE(QString, PluginBase, master, ); + } + QString author() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, ); + } + QString description() const override + { + PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, ); + } + VersionInfo version() const override + { + PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, ); + } + QList settings() const override + { + PYBIND11_OVERRIDE_PURE(QList, PluginBase, settings, ); + } + }; + + template + class PyPluginBase : public PyPluginBaseNoFinal { + public: + using PyPluginBaseNoFinal::PyPluginBaseNoFinal; + + std::vector> requirements() const + { + PYBIND11_OVERRIDE(std::vector>, + PluginBase, requirements, ); + } + bool enabledByDefault() const override + { + PYBIND11_OVERRIDE(bool, PluginBase, enabledByDefault, ); + } + }; + + // these classes do not inherit IPlugin or QObject so we need intermediate class to + // get proper bindings + class IPyPlugin : public QObject, public IPlugin {}; + class IPyPluginFileMapper : public IPyPlugin, public IPluginFileMapper {}; + class IPyPluginDiagnose : public IPyPlugin, public IPluginDiagnose {}; + + // PyXXX classes - trampoline classes for the plugins + + class PyPlugin : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) + }; + + class PyPluginFileMapper : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) + public: + MappingType mappings() const override + { + PYBIND11_OVERRIDE_PURE(MappingType, IPyPluginFileMapper, mappings, ); + } + }; + + class PyPluginDiagnose : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) + public: + std::vector activeProblems() const + { + PYBIND11_OVERRIDE_PURE(std::vector, IPyPluginDiagnose, + activeProblems, ); + } + + QString shortDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, shortDescription, key); + } + + QString fullDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, fullDescription, key); + } + + bool hasGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(bool, IPyPluginDiagnose, hasGuidedFix, key); + } + + void startGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(void, IPyPluginDiagnose, startGuidedFix, key); + } + + // we need to bring this in public scope + using IPluginDiagnose::invalidate; + }; + + class PyPluginTool : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) + public: + QString displayName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginTool, displayName, ); + } + QString tooltip() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginTool, tooltip, ); + } + QIcon icon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginTool, icon, ); + } + void setParentWidget(QWidget* widget) override + { + PYBIND11_OVERRIDE(void, IPluginTool, setParentWidget, widget); + } + void display() const override + { + PYBIND11_OVERRIDE_PURE(void, IPluginTool, display, ); + } + + // we need to bring this in public scope + using IPluginTool::parentWidget; + }; + + class PyPluginPreview : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + public: + std::set supportedExtensions() const override + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginPreview, + supportedExtensions, ); + } + + bool supportsArchives() const override + { + PYBIND11_OVERRIDE(bool, IPluginPreview, supportsArchives, ); + } + + QWidget* genFilePreview(const QString& fileName, + const QSize& maxSize) const override + { + PYBIND11_OVERRIDE_PURE(QWidget*, IPluginPreview, genFilePreview, fileName, + maxSize); + } + + QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName, + const QSize& maxSize) const override + { + PYBIND11_OVERRIDE(QWidget*, IPluginPreview, genDataPreview, fileData, + fileName, maxSize); + } + }; + + class PyPluginModPage : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage) + public: + QString displayName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginModPage, displayName, ); + } + + QIcon icon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginModPage, icon, ); + } + + QUrl pageURL() const override + { + PYBIND11_OVERRIDE_PURE(QUrl, IPluginModPage, pageURL, ); + } + + bool useIntegratedBrowser() const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, useIntegratedBrowser, ); + } + + bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL, + ModRepositoryFileInfo& fileInfo) const override + { + // TODO: cannot modify fileInfo from Python + PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, handlesDownload, pageURL, + downloadURL, &fileInfo); + } + + void setParentWidget(QWidget* widget) override + { + PYBIND11_OVERRIDE(void, IPluginModPage, setParentWidget, widget); + } + + // we need to bring this in public scope + using IPluginModPage::parentWidget; + }; + + // installers + template + class PyPluginInstallerBase : public PyPluginBase { + public: + using PyPluginBase::PyPluginBase; + + unsigned int priority() const override + { + PYBIND11_OVERRIDE_PURE(unsigned int, PluginInstallerBase, priority); + } + + bool isManualInstaller() const override + { + PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isManualInstaller, ); + } + + void onInstallationStart(QString const& archive, bool reinstallation, + IModInterface* currentMod) + { + PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationStart, archive, + reinstallation, currentMod); + } + + void onInstallationEnd(IPluginInstaller::EInstallResult result, + IModInterface* newMod) + { + PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationEnd, result, + newMod); + } + + bool isArchiveSupported(std::shared_ptr tree) const override + { + PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isArchiveSupported, tree); + } + + // we need to bring these in public scope + using PluginInstallerBase::manager; + using PluginInstallerBase::parentWidget; + }; + + class PyPluginInstallerCustom + : public PyPluginInstallerBase { + Q_OBJECT + Q_INTERFACES( + MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom) + public: + bool isArchiveSupported(const QString& archiveName) const + { + PYBIND11_OVERRIDE_PURE(bool, IPluginInstallerCustom, isArchiveSupported, + archiveName); + } + + std::set supportedExtensions() const + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginInstallerCustom, + supportedExtensions, ); + } + + EInstallResult install(GuessedValue& modName, QString gameName, + const QString& archiveName, const QString& version, + int nexusID) override + { + PYBIND11_OVERRIDE_PURE(EInstallResult, IPluginInstallerCustom, install, + &modName, gameName, archiveName, version, nexusID); + } + }; + + class PyPluginInstallerSimple + : public PyPluginInstallerBase { + Q_OBJECT + Q_INTERFACES( + MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + public: + using py_install_return_type = + std::variant, + std::tuple, QString, int>>; + + EInstallResult install(GuessedValue& modName, + std::shared_ptr& tree, QString& version, + int& nexusID) override + { + const auto result = [&, this]() { + PYBIND11_OVERRIDE_PURE(py_install_return_type, IPluginInstallerSimple, + install, &modName, tree, version, nexusID); + }(); + + return std::visit( + [&tree, &version, &nexusID](auto const& t) { + using type = std::decay_t; + if constexpr (std::is_same_v) { + return t; + } + else if constexpr (std::is_same_v>) { + tree = t; + return RESULT_SUCCESS; + } + else if constexpr (std::is_same_v< + type, std::tuple, + QString, int>>) { + tree = std::get<1>(t); + version = std::get<2>(t); + nexusID = std::get<3>(t); + return std::get<0>(t); + } + }, + result); + } + }; + + // game + class PyPluginGame : public PyPluginBaseNoFinal { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame) + public: + void detectGame() override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, detectGame, ); + } + QString gameName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameName, ); + } + QString displayGameName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, displayGameName, ); + } + void initializeProfile(const QDir& directory, + ProfileSettings settings) const override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, initializeProfile, directory, + settings); + } + std::vector> + listSaves(QDir folder) const override + { + PYBIND11_OVERRIDE_PURE(std::vector>, + IPluginGame, listSaves, folder); + } + bool isInstalled() const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginGame, isInstalled, ); + } + QIcon gameIcon() const override + { + PYBIND11_OVERRIDE_PURE(QIcon, IPluginGame, gameIcon, ); + } + QDir gameDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, gameDirectory, ); + } + QDir dataDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, ); + } + QString modDataDirectory() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, modDataDirectory, ); + } + QMap secondaryDataDirectories() const override + { + using string_dir_map = QMap; + PYBIND11_OVERRIDE(string_dir_map, IPluginGame, secondaryDataDirectories, ); + } + void setGamePath(const QString& path) override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGamePath, path); + } + QDir documentsDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, documentsDirectory, ); + } + QDir savesDirectory() const override + { + PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, savesDirectory, ); + } + QList executables() const override + { + PYBIND11_OVERRIDE(QList, IPluginGame, executables, ); + } + QList executableForcedLoads() const override + { + PYBIND11_OVERRIDE_PURE(QList, IPluginGame, + executableForcedLoads, ); + } + QString steamAPPId() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, steamAPPId, ); + } + QStringList primaryPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, primaryPlugins, ); + } + QStringList enabledPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, enabledPlugins, ); + } + QStringList gameVariants() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, gameVariants, ); + } + void setGameVariant(const QString& variant) override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGameVariant, variant); + } + QString binaryName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, binaryName, ); + } + QString gameShortName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameShortName, ); + } + QString lootGameName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, lootGameName, ); + } + QStringList primarySources() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, primarySources, ); + } + QStringList validShortNames() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, validShortNames, ); + } + QString gameNexusName() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, gameNexusName, ); + } + QStringList iniFiles() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, iniFiles, ); + } + QStringList DLCPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, DLCPlugins, ); + } + QStringList CCPlugins() const override + { + PYBIND11_OVERRIDE(QStringList, IPluginGame, CCPlugins, ); + } + LoadOrderMechanism loadOrderMechanism() const override + { + PYBIND11_OVERRIDE(LoadOrderMechanism, IPluginGame, loadOrderMechanism, ); + } + SortMechanism sortMechanism() const override + { + PYBIND11_OVERRIDE(SortMechanism, IPluginGame, sortMechanism, ); + } + int nexusModOrganizerID() const override + { + PYBIND11_OVERRIDE(int, IPluginGame, nexusModOrganizerID, ); + } + int nexusGameID() const override + { + PYBIND11_OVERRIDE_PURE(int, IPluginGame, nexusGameID, ); + } + bool looksValid(QDir const& dir) const override + { + PYBIND11_OVERRIDE_PURE(bool, IPluginGame, looksValid, dir); + } + QString gameVersion() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameVersion, ); + } + QString getLauncherName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, getLauncherName, ); + } + QString getSupportURL() const override + { + PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, ); + } + QMap getModMappings() const override + { + using vfs_map = QMap; + PYBIND11_OVERRIDE(vfs_map, IPluginGame, getModMappings, ); + } + }; + +} // namespace mo2::python + +#endif diff --git a/src/mobase/wrappers/utils.cpp b/src/mobase/wrappers/utils.cpp new file mode 100644 index 00000000..eb3e9e28 --- /dev/null +++ b/src/mobase/wrappers/utils.cpp @@ -0,0 +1,46 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include + +#include "known_folders.h" + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + void add_utils_bindings(pybind11::module_ m) + { + py::class_ pyKnownFolder(m, "KnownFolder"); + for (std::size_t i = 0; i < KNOWN_FOLDERS.size(); ++i) { + pyKnownFolder.attr(KNOWN_FOLDERS[i].name) = py::int_(i); + } + + m.def( + "getKnownFolder", + [](std::size_t knownFolderId, QString what) { + return getKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid, what); + }, + py::arg("known_folder"), py::arg("what") = ""); + + m.def( + "getOptionalKnownFolder", + [](std::size_t knownFolderId) { + const auto r = + getOptionalKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid); + return r.isEmpty() ? py::none{} : py::cast(r); + }, + py::arg("known_folder")); + + m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion), + py::arg("filepath")); + m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion), + py::arg("executable")); + m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable), + py::arg("executable")); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/widgets.cpp b/src/mobase/wrappers/widgets.cpp new file mode 100644 index 00000000..e626540a --- /dev/null +++ b/src/mobase/wrappers/widgets.cpp @@ -0,0 +1,77 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + void add_widget_bindings(pybind11::module_ m) + { + // TaskDialog is also in Windows System. + using TaskDialog = MOBase::TaskDialog; + + // TaskDialog + py::class_(m, "TaskDialogButton") + .def(py::init(), + py::arg("text"), py::arg("description"), py::arg("button")) + .def(py::init(), py::arg("text"), + py::arg("button")) + .def_readwrite("text", &TaskDialogButton::text) + .def_readwrite("description", &TaskDialogButton::description) + .def_readwrite("button", &TaskDialogButton::button); + + py::class_(m, "TaskDialog") + .def(py::init([](QWidget* parent, QString const& title, QString const& main, + QString const& content, QString const& details, + QMessageBox::Icon icon, + std::vector const& buttons, + std::variant> const& + remember) { + auto* dialog = new TaskDialog(parent, title); + dialog->main(main).content(content).details(details).icon(icon); + + for (auto& button : buttons) { + dialog->button(button); + } + + std::visit( + [dialog](auto const& item) { + QString action, file; + if constexpr (std::is_same_v, + QString>) { + action = item; + } + else { + action = std::get<0>(item); + file = std::get<1>(item); + } + dialog->remember(action, file); + }, + remember); + + return dialog; + }), + py::return_value_policy::take_ownership, + py::arg("parent") = static_cast(nullptr), + py::arg("title") = "", py::arg("main") = "", py::arg("content") = "", + py::arg("details") = "", py::arg("icon") = QMessageBox::NoIcon, + py::arg("buttons") = std::vector{}, + py::arg("remember") = "") + .def("setTitle", &TaskDialog::title, py::arg("title")) + .def("setMain", &TaskDialog::main, py::arg("main")) + .def("setContent", &TaskDialog::content, py::arg("content")) + .def("setDetails", &TaskDialog::details, py::arg("details")) + .def("setIcon", &TaskDialog::icon, py::arg("icon")) + .def("addButton", &TaskDialog::button, py::arg("button")) + .def("setRemember", &TaskDialog::remember, py::arg("action"), + py::arg("file") = "") + .def("setWidth", &TaskDialog::setWidth, py::arg("width")) + .def("addContent", &TaskDialog::addContent, py::arg("widget")) + .def("exec", &TaskDialog::exec); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/wrappers.cpp b/src/mobase/wrappers/wrappers.cpp new file mode 100644 index 00000000..10f043df --- /dev/null +++ b/src/mobase/wrappers/wrappers.cpp @@ -0,0 +1,120 @@ + +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include +#include +#include + +// IOrganizer must be bring here to properly compile the Python bindings of +// plugin requirements +#include +#include +#include +#include + +using namespace pybind11::literals; +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + class PyPluginRequirement : public IPluginRequirement { + public: + std::optional check(IOrganizer* organizer) const override + { + PYBIND11_OVERRIDE_PURE(std::optional, IPluginRequirement, check, + organizer); + }; + }; + + class PySaveGame : public ISaveGame { + public: + QString getFilepath() const override + { + PYBIND11_OVERRIDE_PURE(FileWrapper, ISaveGame, getFilepath, ); + } + + QDateTime getCreationTime() const override + { + PYBIND11_OVERRIDE_PURE(QDateTime, ISaveGame, getCreationTime, ); + } + + QString getName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getName, ); + } + + QString getSaveGroupIdentifier() const override + { + PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getSaveGroupIdentifier, ); + } + + QStringList allFiles() const override + { + return toQStringList([&] { + PYBIND11_OVERRIDE_PURE(QList, ISaveGame, allFiles, ); + }()); + } + + ~PySaveGame() { std::cout << "~PySaveGame()" << std::endl; } + }; + + class PySaveGameInfoWidget : public ISaveGameInfoWidget { + public: + // Bring the constructor: + using ISaveGameInfoWidget::ISaveGameInfoWidget; + + void setSave(ISaveGame const& save) override + { + PYBIND11_OVERRIDE_PURE(void, ISaveGameInfoWidget, setSave, &save); + } + + ~PySaveGameInfoWidget() { std::cout << "~PySaveGameInfoWidget()" << std::endl; } + }; + + void add_wrapper_bindings(pybind11::module_ m) + { + // ISaveGame - custom type_caster<> for shared_ptr<> to keep the Python object + // alive when returned from Python (see shared_cpp_owner.h) + + py::class_>(m, "ISaveGame") + .def(py::init<>()) + .def("getFilepath", wrap_return_for_filepath(&ISaveGame::getFilepath)) + .def("getCreationTime", &ISaveGame::getCreationTime) + .def("getName", &ISaveGame::getName) + .def("getSaveGroupIdentifier", &ISaveGame::getSaveGroupIdentifier) + .def("allFiles", [](ISaveGame* s) -> QList { + const auto result = s->allFiles(); + return {result.begin(), result.end()}; + }); + + // ISaveGameInfoWidget - custom holder to keep the Python object alive alongside + // the widget + + py::class_> + iSaveGameInfoWidget(m, "ISaveGameInfoWidget"); + iSaveGameInfoWidget.def(py::init(), "parent"_a = (QWidget*)nullptr) + .def("setSave", &ISaveGameInfoWidget::setSave, "save"_a); + py::qt::add_qt_delegate(iSaveGameInfoWidget, "_widget"); + + // IPluginRequirement - custom type_caster<> for shared_ptr<> to keep the Python + // object alive when returned from Python (see shared_cpp_owner.h) + + py::class_, + PyPluginRequirement> + iPluginRequirementClass(m, "IPluginRequirement"); + + py::class_(iPluginRequirementClass, "Problem") + .def(py::init(), "short_description"_a, + "long_description"_a = "") + .def("shortDescription", &IPluginRequirement::Problem::shortDescription) + .def("longDescription", &IPluginRequirement::Problem::longDescription); + + iPluginRequirementClass.def("check", &IPluginRequirement::check, "organizer"_a); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/wrappers.h b/src/mobase/wrappers/wrappers.h new file mode 100644 index 00000000..43dd5742 --- /dev/null +++ b/src/mobase/wrappers/wrappers.h @@ -0,0 +1,109 @@ +#ifndef PYTHON_WRAPPERS_WRAPPERS_H +#define PYTHON_WRAPPERS_WRAPPERS_H + +#include +#include +#include + +#include + +#include +#include + +#include + +namespace mo2::python { + + /** + * @brief Add bindings for the various classes in uibase that are not + * wrappers (i.e., cannot be extended from Python). + * + * @param m Python module to add bindings to. + */ + void add_basic_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various custom widget classes in uibase that + * cannot be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_widget_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various utilities classes and functions in uibase + * that cannot be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_utils_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the uibase wrappers to the given module. uibase + * wrappers include classes from uibase that can be extended from Python but + * are neither plugins nor game features (e.g., ISaveGame). + * + * @param m Python module to add bindings to. + */ + void add_wrapper_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for the various plugin classes in uibase that can be + * extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_plugins_bindings(pybind11::module_ m); + + /** + * @brief Extract plugins from the given object. For each plugin implemented, an + * object is returned. + * + * The returned QObject* are set as owner of the given object so that the Python + * object lifetime does not end immediately after returning to C++. + * + * @param object Python object to extract plugins from. + * + * @return a QObject* for each plugin implemented by the given object. + */ + QList extract_plugins(pybind11::object object); + + /** + * @brief Add bindings for the various game features classes in uibase that + * can be extended from Python. + * + * @param m Python module to add bindings to. + */ + void add_game_feature_bindings(pybind11::module_ m); + + /** + * @brief Add bindings for IGameFeatures. + * + * @param m Python module to add bindings to. + */ + void add_igamefeatures_classes(pybind11::module_ m); + + /** + * @brief Extract the game feature corresponding to the given Python type. + * + * @param gameFeatures Game features to extract the feature from. + * @param type Type of the feature to extract. + * + * @return the feature from the game, or None is the game as no such feature. + */ + pybind11::object extract_feature(MOBase::IGameFeatures const& gameFeatures, + pybind11::object type); + + /** + * @brief Unregister the game feature corresponding to the given Python type. + * + * @param gameFeatures Game features to unregister the feature from. + * @param type Type of the feature to unregister. + * + * @return the feature from the game, or None is the game as no such feature. + */ + int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type); + +} // namespace mo2::python + +#endif // PYTHON_WRAPPERS_WRAPPERS_H diff --git a/src/plugin_python_en.ts b/src/plugin_python_en.ts new file mode 100644 index 00000000..9ef78363 --- /dev/null +++ b/src/plugin_python_en.ts @@ -0,0 +1,83 @@ + + + + + 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. + + + + + QObject + + + An unknown exception was thrown in python code. + + + + diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index ff53fef6..be59295a 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -1,70 +1,76 @@ -CMAKE_MINIMUM_REQUIRED (VERSION 2.8.11) - -CMAKE_POLICY(SET CMP0020 NEW) -CMAKE_POLICY(SET CMP0043 NEW) - -SET(${PROJ_NAME}_SRCS proxypython.cpp) - -SET(${PROJ_NAME}_HDRS - proxypython.h - resource.h) - -SET(CMAKE_INCLUDE_CURRENT_DIR ON) -SET(CMAKE_AUTOMOC ON) -SET(CMAKE_AUTOUIC ON) -FIND_PACKAGE(Qt5Widgets REQUIRED) -FIND_PACKAGE(Qt5Network REQUIRED) -#QT5_WRAP_UI(${PROJ_NAME}_UIHDRS ${${PROJ_NAME}_FORMS}) - -SET(Boost_USE_STATIC_LIBS ON) -SET(Boost_USE_MULTITHREADED ON) -SET(Boost_USE_STATIC_RUNTIME OFF) -FIND_PACKAGE(Boost) - -IF (Boost_FOUND) - INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS}) -ENDIF (Boost_FOUND) - -SET(default_project_path "${DEPENDENCIES_DIR}/modorganizer_super") -GET_FILENAME_COMPONENT(${default_project_path} ${default_project_path} REALPATH) - -SET(project_path "${default_project_path}" CACHE PATH "path to the other mo projects") -SET(lib_path "${project_path}/../../install/libs") - -ADD_DEFINITIONS(-DUNICODE -D_UNICODE) - -INCLUDE_DIRECTORIES(${project_path}/uibase/src - ${CMAKE_SOURCE_DIR}/src/runner/ - ${PYTHON_ROOT}/Include) -LINK_DIRECTORIES(${lib_path}) - - -ADD_LIBRARY(${PROJ_NAME} SHARED ${${PROJ_NAME}_HDRS} ${${PROJ_NAME}_SRCS} ${${PROJ_NAME}_UIHDRS}) -TARGET_LINK_LIBRARIES(${PROJ_NAME} - Qt5::Widgets - Qt5::Network - ${Boost_LIBRARIES} - uibase) - -IF (MSVC) - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES COMPILE_FLAGS "/std:c++latest") -ENDIF() -IF (MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 4) - # 32 bits - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES LINK_FLAGS "/LARGEADDRESSAWARE") -ENDIF() - -IF (NOT "${OPTIMIZE_COMPILE_FLAGS}" STREQUAL "") - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES COMPILE_FLAGS_RELWITHDEBINFO ${OPTIMIZE_COMPILE_FLAGS}) -ENDIF() -IF (NOT "${OPTIMIZE_LINK_FLAGS}" STREQUAL "") - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES LINK_FLAGS_RELWITHDEBINFO ${OPTIMIZE_LINK_FLAGS}) -ENDIF() - -############### -## Installation - -INSTALL(TARGETS ${PROJ_NAME} - RUNTIME DESTINATION bin/plugins) -INSTALL(FILES $ - DESTINATION pdb) +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-uibase CONFIG REQUIRED) + +set(PLUGIN_NAME "plugin_python") + +add_library(proxy SHARED proxypython.cpp proxypython.h) +mo2_configure_plugin(proxy + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + TRANSLATIONS OFF + EXTRA_TRANSLATIONS + ${CMAKE_CURRENT_SOURCE_DIR}/../runner + ${CMAKE_CURRENT_SOURCE_DIR}/../mobase + ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) +mo2_default_source_group() +target_link_libraries(proxy PRIVATE runner mo2::uibase) +set_target_properties(proxy PROPERTIES OUTPUT_NAME ${PLUGIN_NAME}) +mo2_install_plugin(proxy FOLDER) + +set(PLUGIN_PYTHON_DIR bin/plugins/${PLUGIN_NAME}) + +# install runner +target_link_options(proxy PRIVATE "/DELAYLOAD:runner.dll") +install(FILES $ DESTINATION ${PLUGIN_PYTHON_DIR}/dlls) + +# translations (custom location) +mo2_add_translations(proxy + TS_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../${PLUGIN_NAME}_en.ts + SOURCES + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../runner + ${CMAKE_CURRENT_SOURCE_DIR}/../mobase + ${CMAKE_CURRENT_SOURCE_DIR}/../pybind11-qt) + +# install DLLs files needed +set(DLL_DIRS ${PLUGIN_PYTHON_DIR}/dlls) +file(GLOB dlls_to_install + ${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) +install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS}) + +# install Python .pyd files +set(PYLIB_DIR ${PLUGIN_PYTHON_DIR}/libs) +file(GLOB libs_to_install ${Python_DLL_DIR}/*.pyd) +install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR}) + +# 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 +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/SConscript b/src/proxy/SConscript deleted file mode 100644 index 7410f989..00000000 --- a/src/proxy/SConscript +++ /dev/null @@ -1,32 +0,0 @@ -import os - -Import('qt_env') - -env = qt_env.Clone() - -env.AppendUnique(CPPDEFINES = [ - 'PROXYPYTHON_LIBRARY', - # suppress a few warnings caused by boost vs vc++ paranoia - '_SCL_SECURE_NO_WARNINGS' -]) - -env.AppendUnique(CPPPATH = [ - '${BOOSTPATH}', - '..\\..\\pythonRunner' -]) - -env.AppendUnique(LIBS = 'shell32') - -runner = env.File(os.path.join('..', '..', 'pythonRunner', 'pythonRunner.dll')) - -runner_env = env.Clone() -runner_env.AppendUnique(CPPDEFINES = 'SCONS_BUILD') -runner_env.AppendUnique(CPPPATH = runner.dir) - -# There's a whole load of unused C++ files in here. -lib = env.SharedLibrary('proxyPython', [ 'proxypython.cpp' ]) - -env.InstallModule(lib) - -res = env['QT_USED_MODULES'] -Return('res') diff --git a/src/proxy/build_pythoncore.py b/src/proxy/build_pythoncore.py new file mode 100644 index 00000000..481dec59 --- /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/embedrunner.rc b/src/proxy/embedrunner.rc deleted file mode 100644 index dfdf8ef3..00000000 --- a/src/proxy/embedrunner.rc +++ /dev/null @@ -1,7 +0,0 @@ -#include "resource.h" - -#ifdef SCONS_BUILD -IDR_LOADER_DLL BINARY MOVEABLE PURE pythonRunner.dll -#else -IDR_LOADER_DLL BINARY MOVEABLE PURE "..\\runner\\pythonRunner.dll" -#endif diff --git a/src/proxy/proxyPython.pro b/src/proxy/proxyPython.pro deleted file mode 100644 index 0458fefe..00000000 --- a/src/proxy/proxyPython.pro +++ /dev/null @@ -1,48 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2013-04-05T18:26:04 -# -#------------------------------------------------- - - -TARGET = proxyPython -TEMPLATE = lib - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets - -CONFIG += plugins -CONFIG += dll - -CONFIG(release, debug|release) { - QMAKE_CXXFLAGS += /Zi - QMAKE_LFLAGS += /DEBUG -} - -DEFINES += PROXYPYTHON_LIBRARY - -# suppress a few warnings caused by boost vs vc++ paranoia -DEFINES += _SCL_SECURE_NO_WARNINGS - -SOURCES += proxypython.cpp -HEADERS += proxypython.h \ - resource.h - -OTHER_FILES += \ - proxypython.json \ - SConscript - -RC_FILE += - -include(../plugin_template.pri) - -INCLUDEPATH += "../../pythonRunner" "$${BOOSTPATH}" - -WINPWD = $$PWD -WINPWD ~= s,/,$$QMAKE_DIR_SEP,g - -#QMAKE_POST_LINK += SET VS90COMNTOOLS=%VS100COMNTOOLS% $$escape_expand(\\n) - - -#QMAKE_POST_LINK += copy $$(PYTHONPATH)\\lib\\site-packages\\sip.pyd $$quote($$DSTDIR)\\plugins\\data\\ $$escape_expand(\\n) -#QMAKE_POST_LINK += copy $$(PYTHONPATH)\\lib\\site-packages\\PyQt4\\QtCore.pyd $$quote($$DSTDIR)\\plugins\\data\\PyQt4\\ $$escape_expand(\\n) -#QMAKE_POST_LINK += copy $$(PYTHONPATH)\\lib\\site-packages\\PyQt4\\QtGui.pyd $$quote($$DSTDIR)\\plugins\\data\\PyQt4\\ $$escape_expand(\\n) diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index 2c931c6d..0ecbfc08 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -1,340 +1,290 @@ -/* -Copyright (C) 2013 Sebastian Herbord. All rights reserved. - -This file is part of python proxy plugin for MO - -python proxy plugin is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Python proxy plugin is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with python proxy plugin. If not, see . -*/ - - -#include "proxypython.h" -#include -#include -#include -#include -#include -#include -#include -#include "resource.h" - - -using namespace MOBase; - - -const char *ProxyPython::s_DownloadPythonURL = "http://www.python.org/download/releases/"; - - -HMODULE GetOwnModuleHandle() -{ - HMODULE hMod = nullptr; - GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(&GetOwnModuleHandle), &hMod); - - return hMod; -} - - -QString ExtractResource(WORD resourceID, const QString &szFilename) -{ - HMODULE mod = GetOwnModuleHandle(); - - HRSRC hResource = FindResourceW(mod, MAKEINTRESOURCE(resourceID), L"BINARY"); - if (hResource == nullptr) { - throw MyException("embedded dll not available: " + windowsErrorString(::GetLastError())); - } - - HGLOBAL hFileResource = LoadResource(mod, hResource); - if (hFileResource == nullptr) { - throw MyException("failed to load embedded dll resource: " + windowsErrorString(::GetLastError())); - } - - LPVOID lpFile = LockResource(hFileResource); - if (lpFile == nullptr) { - throw MyException(QString("failed to lock resource: %1").arg(windowsErrorString(::GetLastError()))); - } - - DWORD dwSize = SizeofResource(mod, hResource); - - QString outFile = QDir::tempPath() + "/" + szFilename; - - HANDLE hFile = CreateFileW(outFile.toStdWString().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile == INVALID_HANDLE_VALUE) { - if (::GetLastError() == ERROR_SHARING_VIOLATION) { - // dll exists and is opened by another instance of MO, shouldn't be outdated then... - return outFile; - } else { - throw MyException(QString("failed to open python runner: %1").arg(windowsErrorString(::GetLastError()))); - } - } - HANDLE hFileMap = CreateFileMapping(hFile, nullptr, PAGE_READWRITE, 0, dwSize, nullptr); - if (hFileMap == NULL) { - throw MyException(QString("failed to map python runner: %1").arg(windowsErrorString(::GetLastError()))); - } - LPVOID lpAddress = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); - if (lpAddress == nullptr) { - throw MyException(QString("failed to map view of file: %1").arg(windowsErrorString(::GetLastError()))); - } - - CopyMemory(lpAddress, lpFile, dwSize); - - UnmapViewOfFile(lpAddress); - - CloseHandle(hFileMap); - ::FlushFileBuffers(hFile); - CloseHandle(hFile); - - return outFile; -} - - -ProxyPython::ProxyPython() - : m_MOInfo(nullptr), m_Runner(nullptr), m_LoadFailure(FAIL_NOTINIT) -{ -} - -ProxyPython::~ProxyPython() -{ - if (!m_TempRunnerFile.isEmpty()) { - ::FreeLibrary(m_RunnerLib); - QFile(m_TempRunnerFile).remove(); - } -} - -typedef IPythonRunner* (*CreatePythonRunner_func)(const MOBase::IOrganizer *moInfo, const QString &pythonPath); - - -bool ProxyPython::init(IOrganizer *moInfo) -{ - m_MOInfo = moInfo; - if (!m_MOInfo->pluginSetting(name(), "enabled").toBool()) { - m_LoadFailure = FAIL_NONE; - return false; - } - - m_LoadFailure = FAIL_OTHER; - if (QCoreApplication::applicationDirPath().contains(';')) { - m_LoadFailure = FAIL_SEMICOLON; - return true; - } - - QString pythonPath = m_MOInfo->pluginSetting(name(), "python_dir").toString(); - - if (!pythonPath.isEmpty() && !QFile::exists(pythonPath + "/python.exe")) { - m_LoadFailure = FAIL_WRONGPYTHONPATH; - return true; - } - - m_RunnerLib = ::LoadLibraryW(QDir::toNativeSeparators(m_MOInfo->pluginDataPath() + "/pythonRunner.dll").toStdWString().c_str()); - if (m_RunnerLib != nullptr) { - CreatePythonRunner_func CreatePythonRunner = (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); - if (CreatePythonRunner == nullptr) { - throw MyException("embedded dll is invalid: " + windowsErrorString(::GetLastError())); - } - if (m_MOInfo->persistent(name(), "tryInit", false).toBool()) { - if (pythonPath.isEmpty()) { - m_LoadFailure = FAIL_PYTHONDETECTION; - } else { - m_LoadFailure = FAIL_WRONGPYTHONPATH; - } - if (QMessageBox::question(parentWidget(), tr("Python Initialization failed"), - tr("On a previous start the Python Plugin failed to initialize.\n" - "Either the value in Settings->Plugins->ProxyPython->plugin_dir is set incorrectly or it is empty and auto-detection doesn't work " - "for whatever reason.\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) { - m_MOInfo->setPluginSetting(name(), "enabled", false); - return true; - } - } - - m_MOInfo->setPersistent(name(), "tryInit", true); - m_Runner = CreatePythonRunner(moInfo, pythonPath); - m_MOInfo->setPersistent(name(), "tryInit", false); - - if (m_Runner != nullptr) { - m_LoadFailure = FAIL_NONE; - } else { - m_LoadFailure = FAIL_INITFAIL; - } - return true; - } else { - DWORD error = ::GetLastError(); - qCritical("Failed to load python runner (%s): %s", qUtf8Printable(m_TempRunnerFile), qUtf8Printable(windowsErrorString(error))); - if (error == ERROR_MOD_NOT_FOUND) { - m_LoadFailure = FAIL_MISSINGDEPENDENCIES; - } - return true; - } -} - -QString ProxyPython::name() const -{ - return "Python Proxy"; -} - -QString ProxyPython::author() const -{ - return "Tannin"; -} - -QString ProxyPython::description() const -{ - return tr("Proxy Plugin to allow plugins written in python to be loaded"); -} - -VersionInfo ProxyPython::version() const -{ - return VersionInfo(2, 1, 0, VersionInfo::RELEASE_FINAL); -} - -bool ProxyPython::isActive() const -{ - return m_LoadFailure == FAIL_NOTINIT; -} - -QList ProxyPython::settings() const -{ - QList result; - result.push_back(PluginSetting("python_dir", "Path to your python installation. Leave empty for auto-detection", "")); - result.push_back(PluginSetting("enabled", "Set to true to enable support for python plugins", true)); - return result; -} - -QStringList ProxyPython::pluginList(const QString &pluginPath) const -{ - QDirIterator iter(pluginPath, QStringList("*.py")); - - QStringList result; - while (iter.hasNext()) { - result.append(iter.next()); - } - - return result; -} - - -QList ProxyPython::instantiate(const QString &pluginName) -{ - if (m_Runner != nullptr) { - QList result = m_Runner->instantiate(pluginName); - return result; - } else { - return QList(); - } -} - - -std::vector ProxyPython::activeProblems() const -{ - std::vector result; - if (m_LoadFailure == FAIL_MISSINGDEPENDENCIES) { - result.push_back(PROBLEM_PYTHONMISSING); - } else if (m_LoadFailure == FAIL_WRONGPYTHONPATH) { - result.push_back(PROBLEM_WRONGPYTHONPATH); - } else if (m_LoadFailure == FAIL_PYTHONDETECTION) { - result.push_back(PROBLEM_PYTHONDETECTION); - } else if (m_LoadFailure == FAIL_INITFAIL) { - result.push_back(PROBLEM_INITFAIL); - } else if (m_LoadFailure == FAIL_SEMICOLON) { - result.push_back(PROBLEM_SEMICOLON); - } else if (m_Runner != nullptr) { - if (!m_Runner->isPythonInstalled()) { - // don't know how this could happen but wth - result.push_back(PROBLEM_PYTHONMISSING); - } - if (!m_Runner->isPythonVersionSupported()) { - result.push_back(PROBLEM_PYTHONWRONGVERSION); - } - } - - return result; -} - -QString ProxyPython::shortDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_PYTHONMISSING: { - return tr("Python not installed or not found"); - } break; - case PROBLEM_PYTHONWRONGVERSION: { - return tr("Python version is incompatible"); - } break; - case PROBLEM_WRONGPYTHONPATH: { - return tr("Invalid python path"); - } break; - case PROBLEM_INITFAIL: { - return tr("Initializing Python failed"); - } break; - case PROBLEM_PYTHONDETECTION: { - return tr("Python auto-detection failed"); - } break; - case PROBLEM_SEMICOLON: { - return tr("ModOrganizer path contains a semicolon"); - } break; - default: - throw MyException(tr("invalid problem key %1").arg(key)); - } -} - - -QString ProxyPython::fullDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_PYTHONMISSING: { - return tr("Some MO plugins require the python interpreter to be installed. " - "These plugins will not even show up in settings->plugins.
" - "If you want to use those plugins, please install the 32-bit version of Python 2.7.x from %1.
" - "This is only required to use some extended functionality in MO, you do not need Python to play the game.").arg(s_DownloadPythonURL); - } break; - case PROBLEM_PYTHONWRONGVERSION: { - return tr("Your installed python version has a different version than 2.7. " - "Some MO plugins may not work.
" - "If you have multiple versions of python installed you may have to configure the path to 2.7 (32 bit) " - "in the settings dialog.
" - "This is only required to use some extended functionality in MO, you do not need Python to play the game."); - } break; - case PROBLEM_WRONGPYTHONPATH: { - return tr("Please set python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation."); - } break; - case PROBLEM_PYTHONDETECTION: { - return tr("The auto-detection of the python path failed. I don't know why this would happen but you can try to fix it " - "by setting python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation."); - } break; - case PROBLEM_INITFAIL: { - return tr("Sorry, I don't know any details. Most likely your python installation is not supported."); - } break; - case PROBLEM_SEMICOLON: { - return tr("The path to Mod Organizer (%1) contains a semicolon.
" - "While this is legal on NTFS drives there is a lot of software that doesn't handle it correctly.
" - "Unfortunately MO depends on libraries that seem to fall into that group.
" - "As a result the python plugin can't be loaded.
" - "The only solution I can offer is to remove the semicolon / move MO to a path without a semicolon.").arg(QCoreApplication::applicationDirPath()); - } break; - default: - throw MyException(QString("invalid problem key %1").arg(key)); - } -} - -bool ProxyPython::hasGuidedFix(unsigned int key) const -{ - return (key == PROBLEM_PYTHONMISSING) || (key == PROBLEM_PYTHONWRONGVERSION); -} - -void ProxyPython::startGuidedFix(unsigned int key) const -{ - if ((key == PROBLEM_PYTHONMISSING) || (key == PROBLEM_PYTHONWRONGVERSION)) { - ::ShellExecuteA(nullptr, "open", s_DownloadPythonURL, nullptr, nullptr, SW_SHOWNORMAL); - } -} +/* +Copyright (C) 2022 Sebastian Herbord & MO2 Team. All rights reserved. + +This file is part of python proxy plugin for MO + +python proxy plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Python proxy plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with python proxy plugin. If not, see . +*/ +#include "proxypython.h" + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace fs = std::filesystem; +using namespace MOBase; + +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) { + return {}; + } + + return fs::path(path).parent_path(); +} + +ProxyPython::ProxyPython() + : m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr}, + m_LoadFailure(FailureType::NONE) +{ +} + +bool ProxyPython::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { + return false; + } + + if (QCoreApplication::applicationDirPath().contains(';')) { + m_LoadFailure = FailureType::SEMICOLON; + return true; + } + + const auto pluginFolder = getPluginFolder(); + + if (pluginFolder.empty()) { + DWORD error = ::GetLastError(); + m_LoadFailure = 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 + // will look for DLLs in it, this is required to find the Python and libffi DLL, but + // also the runner DLL + // + const auto dllPaths = pluginFolder / "dlls"; + if (SetDllDirectoryW(dllPaths.c_str()) == 0) { + DWORD error = ::GetLastError(); + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to add python DLL directory ({}): {}", error, + qUtf8Printable(windowsErrorString(::GetLastError()))); + return false; + } + + // 2. we create the Python runner, we do not need to use ::LinkLibrary and + // ::GetProcAddress because we use delayed load for the runner DLL (see the + // CMakeLists.txt) + // + m_Runner = mo2::python::createPythonRunner(); + + if (m_Runner) { + const auto libpath = pluginFolder / "libs"; + const std::vector paths{ + libpath / "pythoncore.zip", libpath, + std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; + 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; + } + else { + m_Runner->addDllSearchPath(pluginFolder / "dlls"); + } + + 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 +{ + QDir dir(pluginPath); + dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); + QDirIterator iter(dir); + + // Note: We put python script (.py) and directory names, not the __init__.py + // files in those since it is easier for the runner to import them. + QStringList result; + while (iter.hasNext()) { + QString name = iter.next(); + QFileInfo info = iter.fileInfo(); + + if (info.isFile() && name.endsWith(".py")) { + result.append(name); + } + else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { + result.append(name); + } + } + + return result; +} + +QList ProxyPython::load(const QString& identifier) +{ + if (!m_Runner) { + return {}; + } + return m_Runner->load(identifier); +} + +void ProxyPython::unload(const QString& identifier) +{ + if (m_Runner) { + return m_Runner->unload(identifier); + } +} + +std::vector ProxyPython::activeProblems() const +{ + auto failure = m_LoadFailure; + + // don't know how this could happen but wth + if (m_Runner && !m_Runner->isInitialized()) { + failure = FailureType::INITIALIZATION; + } + + if (failure != FailureType::NONE) { + return {static_cast>(failure)}; + } + + return {}; +} + +QString ProxyPython::shortDescription(unsigned int key) const +{ + 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); + } +} + +QString ProxyPython::fullDescription(unsigned int key) const +{ + switch (static_cast(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 " + "handle it correctly.
" + "Unfortunately MO depends on libraries that seem to fall into that " + "group.
" + "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.") + .arg(QCoreApplication::applicationDirPath()); + case FailureType::DLL_NOT_FOUND: + return tr("The Python plugin DLL was not found, maybe your antivirus deleted " + "it. Re-installing MO2 might fix the problem."); + case FailureType::INVALID_DLL: + return tr( + "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."); + case FailureType::INITIALIZATION: + return tr("The initialization of the Python plugin DLL failed, unfortunately " + "without any details."); + default: + return tr("invalid problem key %1").arg(key); + } +} + +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 a41f8564..3d52da54 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -1,96 +1,76 @@ -/* -Copyright (C) 2013 Sebastian Herbord. All rights reserved. - -This file is part of python proxy plugin for MO - -python proxy plugin is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Python proxy plugin is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with python proxy plugin. If not, see . -*/ - -#ifndef PROXYPYTHON_H -#define PROXYPYTHON_H - - -#include -#include -#include -#include -#include - - -class ProxyPython : public QObject, public MOBase::IPluginProxy, public MOBase::IPluginDiagnose -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) - Q_PLUGIN_METADATA(IID "org.tannin.ProxyPython" FILE "proxypython.json") -#endif - -public: - ProxyPython(); - ~ProxyPython(); - - virtual bool init(MOBase::IOrganizer *moInfo); - virtual QString name() const; - virtual QString author() const; - virtual QString description() const; - virtual MOBase::VersionInfo version() const; - virtual bool isActive() const; - virtual QList settings() const; - - QStringList pluginList(const QString &pluginPath) const; - QList instantiate(const QString &pluginName); - - /** - * @return the parent widget for newly created dialogs - * @note needs to be public so it can be exposed to plugins - */ - virtual QWidget *getParentWidget() { return parentWidget(); } - -public: // IPluginDiagnose - - virtual std::vector activeProblems() const; - virtual QString shortDescription(unsigned int key) const; - virtual QString fullDescription(unsigned int key) const; - virtual bool hasGuidedFix(unsigned int key) const; - virtual void startGuidedFix(unsigned int key) const; - -private: - - static const unsigned int PROBLEM_PYTHONMISSING = 1; - static const unsigned int PROBLEM_PYTHONWRONGVERSION = 2; - static const unsigned int PROBLEM_WRONGPYTHONPATH = 3; - static const unsigned int PROBLEM_INITFAIL = 4; - static const unsigned int PROBLEM_PYTHONDETECTION = 5; - static const unsigned int PROBLEM_SEMICOLON = 6; - static const char *s_DownloadPythonURL; - - MOBase::IOrganizer *m_MOInfo; - QString m_TempRunnerFile; - HMODULE m_RunnerLib; - IPythonRunner *m_Runner; - - enum { - FAIL_NONE, - FAIL_SEMICOLON, - FAIL_NOTINIT, - FAIL_MISSINGDEPENDENCIES, - FAIL_INITFAIL, - FAIL_WRONGPYTHONPATH, - FAIL_PYTHONDETECTION, - FAIL_OTHER - } m_LoadFailure; - -}; - -#endif // PROXYPYTHON_H +/* +Copyright (C) 2022 Sebastian Herbord & MO2 Team. All rights reserved. + +This file is part of python proxy plugin for MO + +python proxy plugin is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Python proxy plugin is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with python proxy plugin. If not, see . +*/ + +#ifndef PROXYPYTHON_H +#define PROXYPYTHON_H + +#include +#include + +#include +#include + +#include + +class ProxyPython : public QObject, + public MOBase::IPluginProxy, + public MOBase::IPluginDiagnose { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) + 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; + +private: + MOBase::IOrganizer* m_MOInfo; + HMODULE m_RunnerLib; + std::unique_ptr m_Runner; + + enum class FailureType : unsigned int { + NONE = 0, + SEMICOLON = 1, + DLL_NOT_FOUND = 2, + INVALID_DLL = 3, + INITIALIZATION = 4 + }; + + FailureType m_LoadFailure; +}; + +#endif // PROXYPYTHON_H diff --git a/src/proxy/proxypython.json b/src/proxy/proxypython.json deleted file mode 100644 index 69a88e3b..00000000 --- a/src/proxy/proxypython.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/proxy/resource.h b/src/proxy/resource.h deleted file mode 100644 index 5d1c2736..00000000 --- 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 new file mode 100644 index 00000000..dd9d2178 --- /dev/null +++ b/src/pybind11-qt/CMakeLists.txt @@ -0,0 +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_target(pybind11-qt + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC OFF + TRANSLATIONS OFF +) +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/README.md b/src/pybind11-qt/README.md new file mode 100644 index 00000000..784f9a89 --- /dev/null +++ b/src/pybind11-qt/README.md @@ -0,0 +1,64 @@ +# pybind11-qt + +This library contains code to interface pybind11 with Qt and PyQt. + +## Type casters + +The main part of this library is a set of (templated) type casters for Qt types that +can be used by simply importing `pybind11_qt/pybind11_qt.h`. +This provides type casters for: + +- Standard Qt types, such as `QString` or `QVariant`. + - `QString` is equivalent to Python `str` (unicode or not). + - `QVariant` is not exposed but the object is directly converted, similarly to + `std::variant` default type caster. +- Qt containers (`QList`, `QSet`, `QMap`, `QStringList`). + - The `QList` type-caster is more flexible than the standard container type-casters + from pybind11 as it accepts any iterable. +- `QFlags` - Delegates the cast to the underlying type, basically. +- Qt enumerations: a lot of enumerations are provided in + [`pybind11_qt_enums.h`](include/pybind11_qt/pybind11_qt_enums.h) and new ones can be + easily added using the `PYQT_ENUM` macro (inside the header file). +- Qt objects: very few are provided in + [`pybind11_qt_objects.h`](include/pybind11_qt/pybind11_qt_objects.h) and new ones can + be added using the `PYQT_OBJECT` macro (inside the header file). + - Copy-constructible Qt objects are copied when passing from C++ to Python or + vice-versa. + - Non copy-constructible Qt objects, e.g., `QObject` or `QWidget` should always be + exposed as pointer, and their ownership is transferred to C++ when coming from + Python. + +## Qt holder + +The library also provides a `pybind11::qt::qobject_holder` holder for pybind11 that +transfer ownerships of the Python object to the underlying `QObject`. + +This holder is useful when exposing classes inheriting `QObject` (or a child class of +`QObject`) that can be extended to Python. + +The library also provides two `set_qt_owner` functions that can be used to transfer +ownership manually. + +## Qt delegates + +The library provides a `add_qt_delegate` function that can be used to delegate Python +call to Qt functions to C++: + +```cpp +py::class_< + // the C++ class extending QObject to expose + ISaveGameInfoWidget, + + // the trampoline class + PySaveGameInfoWidget, + + // the Qt holder to keep the Python object alive alongside the C++ one + py::qt::qobject_holder + +> iSaveGameInfoWidget(m, "ISaveGameInfoWidget"); + +// allow to access most of the class attributes through Python via an overload of +// __getattr__ and add a _widget() method to access the widget itself if needed +// +py::qt::add_qt_delegate(iSaveGameInfoWidget, "_widget"); +``` diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h new file mode 100644 index 00000000..45afd0c6 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h @@ -0,0 +1,59 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP +#define PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP + +#include + +#include + +#include "pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + // EnumData, with static members (const char[]) + // - package: name of the Python package containing the enum (e.g., + // PyQt6.QtCore) + // - name: full path to the enum, e.g. Qt.QGlobalColor + // + template + struct EnumData; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_enum_caster { + + public: + PYBIND11_TYPE_CASTER(Enum, EnumData::package + const_name(".") + + EnumData::name); + + bool load(pybind11::handle src, bool) + { + if (PyLong_Check(src.ptr())) { + value = static_cast(PyLong_AsLong(src.ptr())); + return true; + } + + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + + if (isinstance(src, pyenum)) { + value = static_cast(src.attr("value").cast()); + return true; + } + + return false; + } + + static pybind11::handle cast(Enum src, + pybind11::return_value_policy /* policy */, + pybind11::handle /* parent */) + { + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + return pyenum(static_cast(src)); + } + }; +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h new file mode 100644 index 00000000..e640725d --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h @@ -0,0 +1,52 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP + +#include +#include + +namespace pybind11::detail::qt { + + // helper class for QList to construct from any proper iterable + // + template + struct qlist_caster { + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src) || isinstance(src) || + isinstance(src)) { + return false; + } + auto s = reinterpret_borrow(src); + value.clear(); + + if (isinstance(src)) { + value.reserve(s.cast().size()); + } + for (auto it : s) { + value_conv conv; + if (!conv.load(it, convert)) { + return false; + } + value.push_back(cast_op(std::move(conv))); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + return list_caster, Value>{}.cast(std::forward(src), policy, + parent); + } + + // we type these as "Sequence" even if these can be constructed from Iterable, + // otherwise the return type will be typed as "Iterable" which is problematic + PYBIND11_TYPE_CASTER(Type, const_name("Sequence[") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h new file mode 100644 index 00000000..ea743b7b --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h @@ -0,0 +1,70 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP + +#include + +namespace pybind11::detail::qt { + + // helper class for QMap because QMap do not follow the standard std:: maps + // interface, for other containers, the pybind11 built-in xxx_caster works + // + // this code is basically a copy/paste from the pybind11 stl stuff with + // minor modifications + // + template + struct qmap_caster { + using key_conv = make_caster; + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src)) { + return false; + } + auto d = reinterpret_borrow(src); + value.clear(); + for (auto it : d) { + key_conv kconv; + value_conv vconv; + if (!kconv.load(it.first.ptr(), convert) || + !vconv.load(it.second.ptr(), convert)) { + return false; + } + value[cast_op(std::move(kconv))] = + cast_op(std::move(vconv)); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + dict d; + return_value_policy policy_key = policy; + return_value_policy policy_value = policy; + if (!std::is_lvalue_reference::value) { + policy_key = return_value_policy_override::policy(policy_key); + policy_value = + return_value_policy_override::policy(policy_value); + } + for (auto it = src.begin(); it != src.end(); ++it) { + auto key = reinterpret_steal( + key_conv::cast(forward_like(it.key()), policy_key, parent)); + auto value = reinterpret_steal(value_conv::cast( + forward_like(it.value()), policy_value, parent)); + if (!key || !value) { + return handle(); + } + d[key] = value; + } + return d.release(); + } + + PYBIND11_TYPE_CASTER(Type, const_name("Dict[") + key_conv::name + + const_name(", ") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif 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 new file mode 100644 index 00000000..556a9802 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -0,0 +1,221 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_SIP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_SIP_HPP + +#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 { + + // 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; + + template + struct MetaData>> + : MetaData> {}; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_type_caster { + + static constexpr bool is_pointer = std::is_pointer_v; + using pointer = std::conditional_t; + + QClass value; + + public: + static constexpr auto name = MetaData::python_name; + + operator pointer() + { + if constexpr (is_pointer) { + return value; + } + else { + return &value; + } + } + + // pybind11 requires operator T&() & and operator T&&() && but here we want to + // use SFINAE with is_pointer so we need to template the operator + // + // having a template operator U&&() does not work since it will not + // deduce the proper return type for QClass&& or QClass& so we have two separate + // overloads, and in each one, U is actually a reference type (lvalue or rvalue) + // + + template , QClass> && + std::is_lvalue_reference_v && !is_pointer, + int> = 0> + operator U() + { + return value; + } + + template , QClass> && + std::is_rvalue_reference_v && !is_pointer, + int> = 0> + operator U() && + { + return std::move(value); + } + + template + using cast_op_type = + std::conditional_t>; + + bool load(pybind11::handle src, bool) + { + // special check for none for pointer classes + if constexpr (is_pointer) { + if (src.is_none()) { + value = nullptr; + return true; + } + } + + const auto* type = sip::api_find_type(MetaData::class_name); + if (type == nullptr) { + return false; + } + if (!sip::api_can_convert_to_type(src.ptr(), type, 0)) { + return false; + } + + // this would transfer responsibility for deconstructing the + // object to C++, but pybind11 assumes l-value converters (such + // as this) don't do that instead, this should be called within + // the wrappers for functions which return deletable pointers. + // + // sipAPI()->api_transfer_to(objPtr, Py_None); + // + void* const data = sip::extract_data(src.ptr()); + + if (data) { + if constexpr (is_pointer) { + value = reinterpret_cast(data); + + // transfer ownership + sip::api_transfer_to(src.ptr(), Py_None); + + // tie the py::object to the C++ one + new pybind11::detail::qt::qobject_holder_impl(value); + } + else { + value = *reinterpret_cast(data); + } + return true; + } + else { + return false; + } + } + + template < + typename T, + std::enable_if_t>::value, int> = 0> + static handle cast(T* src, return_value_policy policy, handle parent) + { + // note: when QClass is a pointer type, e.g. a QWidget*, T is a + // pointer to pointer, so we can defer to the standard cast() + + if (!src) { + return none().release(); + } + + if (!is_pointer && policy == return_value_policy::take_ownership) { + auto h = cast(std::move(*src), policy, parent); + delete src; + return h; + } + return cast(*src, policy, parent); + } + + static pybind11::handle cast(QClass src, pybind11::return_value_policy policy, + pybind11::handle /* parent */) + { + if constexpr (is_pointer) { + if (!src) { + return none().release(); + } + } + + const sipTypeDef* type = sip::api_find_type(MetaData::class_name); + if (type == nullptr) { + return Py_None; + } + + PyObject* sipObj; + void* sipData; + + if constexpr (is_pointer) { + sipData = src; + } + else if (std::is_copy_assignable_v) { + // we send to SIP a newly allocated object, and transfer the + // owernship to it + sipData = + new QClass(policy == ::pybind11::return_value_policy::take_ownership + ? std::move(src) + : src); + } + else { + sipData = &src; + } + + sipObj = sip::api_convert_from_type(sipData, type, 0); + + if (sipObj == nullptr) { + return Py_None; + } + + // ensure Python deletes the C++ component + if constexpr (!is_pointer) { + sip::api_transfer_back(sipObj); + } + else { + if (policy == return_value_policy::take_ownership) { + sip::api_transfer_back(sipObj); + } + } + + return sipObj; + } + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h new file mode 100644 index 00000000..c877c157 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h @@ -0,0 +1,47 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP +#define PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP + +#include +#include + +#include + +namespace pybind11::detail::qt { + + /** + * @brief Convert a XXX::YYY compile time string to a XXX.YYY compile time + * string. Only one :: is allowed. + * + */ + template + constexpr descr qt_name_cpp2py(const char (&name)[N]) + { + descr res; + for (std::size_t i = 0, j = 0; i < N - 2; ++i) { + + res.text[i] = name[j]; + + if (res.text[i] == ':') { + res.text[i] = '.'; + j += 2; + } + else { + ++j; + } + } + return res; + } + + /** + * @brief Retrieve the class from the given package at the given path + * + * @param package Name of the module. + * @param path Path to the class/object in the module. + * + * @return the object at the given path in the given module + */ + pybind11::object get_attr_rec(std::string_view package, std::string_view path); + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h new file mode 100644 index 00000000..e803e9f7 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h @@ -0,0 +1,88 @@ +#ifndef PYTHON_PYBIND11_QT_HPP +#define PYTHON_PYBIND11_QT_HPP + +// this header defines many type casters for Qt types, including: +// - basic Qt types such as QString and QVariant - those do not have PyQt6 equivalent +// - QFlags<> class templates +// - containers such as QList<>, QSet<>, etc., the QList<> casters is more flexible than +// the std::vector<> or std::list<> ones as it accepts any iterable +// - many Qt enumeration types (see pybind11_qt_enums) +// - many Qt classes with PyQt6 equivalent +// - copyable type are copied between Python and C++ +// - non-copyable type (QObject, QWidget, QMainWindow) are always owned by the C++ +// side, even when constructed on the Python side, and owned their corresponding +// Python object, e.g., an instance of a class inheriting QWidget created on the +// Python side can be safely used in C++ since the Python object will be owned by +// the C++ QWidget object +// + +#include "pybind11_qt_basic.h" +#include "pybind11_qt_containers.h" +#include "pybind11_qt_enums.h" +#include "pybind11_qt_holder.h" +#include "pybind11_qt_objects.h" +#include "pybind11_qt_qflags.h" + +namespace pybind11::qt { + + /** + * @brief Tie the lifetime of the Python object to the lifetime of the given + * QObject. + * + * @param owner QObject that will own the python object. + * @param child Python object that the QObject will own. + */ + inline void set_qt_owner(QObject* owner, object child) + { + new detail::qt::qobject_holder_impl{owner, child}; + } + + /** + * @brief Tie the lifetime of the given object to the lifetime of the corresponding + * Python object. + * + * This object must have been created from Python and must inherit QObject. + * + * @param object Object to tie. + */ + template + void set_qt_owner(Class* object) + { + static_assert(std::is_base_of_v); + new detail::qt::qobject_holder_impl{object}; + } + + /** + * @brief Add Qt "delegate" to the given class. + * + * This function defines two methods: __getattr__ and name, where name will + * 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 pybind11 + * (e.g., signals, methods from QObject or QWidget, etc.). + * + * @param pyclass Python class to define the methods on. + * @param name Name of the method to retrieve the underlying object. + * + * @tparam QClass Name of the Qt class, cannot be deduced. + * @tparam Class Class being wrapped, deduced. + * @tparam Args... Arguments of the class template parameters, deduced. + */ + template + auto& add_qt_delegate(pybind11::class_& pyclass, const char* name) + { + return pyclass + .def(name, + [](Class* w) -> QClass* { + return w; + }) + .def( + "__getattr__", +[](Class* w, pybind11::str str) -> pybind11::object { + return pybind11::cast((QClass*)w).attr(str); + }); + } + +} // namespace pybind11::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h new file mode 100644 index 00000000..911de342 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h @@ -0,0 +1,36 @@ +#ifndef PYTHON_PYBIND11_QT_BASIC_HPP +#define PYTHON_PYBIND11_QT_BASIC_HPP + +#include +#include + +#include + +namespace pybind11::detail { + + // QString + // + template <> + struct type_caster { + PYBIND11_TYPE_CASTER(QString, const_name("str")); + + bool load(handle src, bool); + + static handle cast(QString src, return_value_policy policy, handle parent); + }; + + // QVariant - this needs to be defined BEFORE QVariantList + // + template <> + struct type_caster { + public: + PYBIND11_TYPE_CASTER(QVariant, const_name("MoVariant")); + + bool load(handle src, bool); + + static handle cast(QVariant var, return_value_policy policy, handle parent); + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h new file mode 100644 index 00000000..120b5e8f --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h @@ -0,0 +1,51 @@ +#ifndef PYTHON_PYBIND11_QT_CONTAINERS_HPP +#define PYTHON_PYBIND11_QT_CONTAINERS_HPP + +#include +#include +#include + +#include +#include + +// this needs to be included here to get proper QVariantList and QVariantMap +#include "details/pybind11_qt_qlist.h" +#include "details/pybind11_qt_qmap.h" +#include "pybind11_qt_basic.h" + +namespace pybind11::detail { + + // QList + // + template + struct type_caster> : qt::qlist_caster, T> {}; + + // QSet + // + template + struct type_caster> : set_caster, T> {}; + + // QMap + // + template + struct type_caster> : qt::qmap_caster, K, V> {}; + + // QStringList + // + template <> + struct type_caster : qt::qlist_caster {}; + + // QVariantList + // + template <> + struct type_caster : qt::qlist_caster {}; + + // QVariantMap + // + template <> + struct type_caster : qt::qmap_caster { + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h new file mode 100644 index 00000000..b58cc41b --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h @@ -0,0 +1,115 @@ +#ifndef PYTHON_PYBIND11_QT_ENUMS_HPP +#define PYTHON_PYBIND11_QT_ENUMS_HPP + +#include +#include + +#include "details/pybind11_qt_enum.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_ENUM(QPackage, QEnum) \ + namespace pybind11::detail { \ + namespace qt { \ + template <> \ + struct EnumData { \ + constexpr static const auto package = \ + const_name("PyQt6.") + const_name(#QPackage); \ + constexpr static const auto name = qt_name_cpp2py(#QEnum); \ + }; \ + } \ + template <> \ + struct type_caster : qt::qt_enum_caster {}; \ + } + +PYQT_ENUM(QtCore, Qt::AlignmentFlag); +PYQT_ENUM(QtCore, Qt::AnchorPoint); +PYQT_ENUM(QtCore, Qt::ApplicationAttribute); +PYQT_ENUM(QtCore, Qt::ApplicationState); +PYQT_ENUM(QtCore, Qt::ArrowType); +PYQT_ENUM(QtCore, Qt::AspectRatioMode); +PYQT_ENUM(QtCore, Qt::Axis); +PYQT_ENUM(QtCore, Qt::BGMode); +PYQT_ENUM(QtCore, Qt::BrushStyle); +PYQT_ENUM(QtCore, Qt::CaseSensitivity); +PYQT_ENUM(QtCore, Qt::CheckState); +PYQT_ENUM(QtCore, Qt::ChecksumType); +PYQT_ENUM(QtCore, Qt::ClipOperation); +PYQT_ENUM(QtCore, Qt::ConnectionType); +PYQT_ENUM(QtCore, Qt::ContextMenuPolicy); +PYQT_ENUM(QtCore, Qt::CoordinateSystem); +PYQT_ENUM(QtCore, Qt::Corner); +PYQT_ENUM(QtCore, Qt::CursorMoveStyle); +PYQT_ENUM(QtCore, Qt::CursorShape); +PYQT_ENUM(QtCore, Qt::DateFormat); +PYQT_ENUM(QtCore, Qt::DayOfWeek); +PYQT_ENUM(QtCore, Qt::DockWidgetArea); +PYQT_ENUM(QtCore, Qt::DropAction); +PYQT_ENUM(QtCore, Qt::Edge); +PYQT_ENUM(QtCore, Qt::EnterKeyType); +PYQT_ENUM(QtCore, Qt::EventPriority); +PYQT_ENUM(QtCore, Qt::FillRule); +PYQT_ENUM(QtCore, Qt::FindChildOption); +PYQT_ENUM(QtCore, Qt::FocusPolicy); +PYQT_ENUM(QtCore, Qt::FocusReason); +PYQT_ENUM(QtCore, Qt::GestureFlag); +PYQT_ENUM(QtCore, Qt::GestureState); +PYQT_ENUM(QtCore, Qt::GestureType); +PYQT_ENUM(QtCore, Qt::GlobalColor); +PYQT_ENUM(QtCore, Qt::HitTestAccuracy); +PYQT_ENUM(QtCore, Qt::ImageConversionFlag); +PYQT_ENUM(QtCore, Qt::InputMethodHint); +PYQT_ENUM(QtCore, Qt::InputMethodQuery); +PYQT_ENUM(QtCore, Qt::ItemDataRole); +PYQT_ENUM(QtCore, Qt::ItemFlag); +PYQT_ENUM(QtCore, Qt::ItemSelectionMode); +PYQT_ENUM(QtCore, Qt::ItemSelectionOperation); +PYQT_ENUM(QtCore, Qt::Key); +PYQT_ENUM(QtCore, Qt::KeyboardModifier); +PYQT_ENUM(QtCore, Qt::LayoutDirection); +PYQT_ENUM(QtCore, Qt::MaskMode); +PYQT_ENUM(QtCore, Qt::MatchFlag); +PYQT_ENUM(QtCore, Qt::Modifier); +PYQT_ENUM(QtCore, Qt::MouseButton); +PYQT_ENUM(QtCore, Qt::MouseEventFlag); +PYQT_ENUM(QtCore, Qt::MouseEventSource); +PYQT_ENUM(QtCore, Qt::NativeGestureType); +PYQT_ENUM(QtCore, Qt::NavigationMode); +PYQT_ENUM(QtCore, Qt::Orientation); +PYQT_ENUM(QtCore, Qt::PenCapStyle); +PYQT_ENUM(QtCore, Qt::PenJoinStyle); +PYQT_ENUM(QtCore, Qt::PenStyle); +PYQT_ENUM(QtCore, Qt::ScreenOrientation); +PYQT_ENUM(QtCore, Qt::ScrollBarPolicy); +PYQT_ENUM(QtCore, Qt::ScrollPhase); +PYQT_ENUM(QtCore, Qt::ShortcutContext); +PYQT_ENUM(QtCore, Qt::SizeHint); +PYQT_ENUM(QtCore, Qt::SizeMode); +PYQT_ENUM(QtCore, Qt::SortOrder); +PYQT_ENUM(QtCore, Qt::TabFocusBehavior); +PYQT_ENUM(QtCore, Qt::TextElideMode); +PYQT_ENUM(QtCore, Qt::TextFlag); +PYQT_ENUM(QtCore, Qt::TextFormat); +PYQT_ENUM(QtCore, Qt::TextInteractionFlag); +PYQT_ENUM(QtCore, Qt::TileRule); +PYQT_ENUM(QtCore, Qt::TimeSpec); +PYQT_ENUM(QtCore, Qt::TimerType); +PYQT_ENUM(QtCore, Qt::ToolBarArea); +PYQT_ENUM(QtCore, Qt::ToolButtonStyle); +PYQT_ENUM(QtCore, Qt::TransformationMode); +PYQT_ENUM(QtCore, Qt::WhiteSpaceMode); +PYQT_ENUM(QtCore, Qt::WidgetAttribute); +PYQT_ENUM(QtCore, Qt::WindowFrameSection); +PYQT_ENUM(QtCore, Qt::WindowModality); +PYQT_ENUM(QtCore, Qt::WindowState); +PYQT_ENUM(QtCore, Qt::WindowType); + +PYQT_ENUM(QtWidgets, QMessageBox::ButtonRole); +PYQT_ENUM(QtWidgets, QMessageBox::DialogCode); +PYQT_ENUM(QtWidgets, QMessageBox::Icon); +PYQT_ENUM(QtWidgets, QMessageBox::PaintDeviceMetric); +PYQT_ENUM(QtWidgets, QMessageBox::RenderFlag); +PYQT_ENUM(QtWidgets, QMessageBox::StandardButton); + +#undef PYQT_ENUM + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h new file mode 100644 index 00000000..39200d51 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h @@ -0,0 +1,59 @@ +#ifndef PYTHON_PYBIND11_QT_HOLDER_HPP +#define PYTHON_PYBIND11_QT_HOLDER_HPP + +#include + +#include + +namespace pybind11::detail::qt { + + class qobject_holder_impl : public QObject { + object p_; + + public: + /** + * @brief Construct a new qobject holder linked to the given QObject and + * maintaining the given python object alive. + * + * @param p Parent of this holder. + * @param o Python object to keep alive. + */ + qobject_holder_impl(QObject* p, object o) : p_{o} { setParent(p); } + + template + qobject_holder_impl(U* p) + : qobject_holder_impl{p, reinterpret_borrow(cast(p))} + { + } + + ~qobject_holder_impl() + { + gil_scoped_acquire s; + p_ = object(); + } + }; + +} // namespace pybind11::detail::qt + +namespace pybind11::qt { + + template + class qobject_holder { + using type = Type; + + type* qobj_; + + public: + qobject_holder(type* qobj) : qobj_{qobj} + { + new detail::qt::qobject_holder_impl(qobj_); + } + + type* get() { return qobj_; } + }; + +} // namespace pybind11::qt + +PYBIND11_DECLARE_HOLDER_TYPE(T, ::pybind11::qt::qobject_holder) + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h new file mode 100644 index 00000000..e16b5ea5 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h @@ -0,0 +1,62 @@ +#ifndef PYTHON_PYBIND11_QT_OBJECTS_HPP +#define PYTHON_PYBIND11_QT_OBJECTS_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "details/pybind11_qt_sip.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_CLASS(QModule, QClass) \ + namespace pybind11::detail { \ + namespace qt { \ + template <> \ + struct MetaData { \ + constexpr static const auto class_name = #QClass; \ + constexpr static const auto python_name = \ + const_name("PyQt6.") + const_name(#QModule) + const_name(".") + \ + const_name(#QClass); \ + }; \ + } \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + type_caster_generic, qt::qt_type_caster> {}; \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + qt::qt_type_caster, type_caster> {}; \ + } + +// add declarations below to create bindings - the first argument is simply +// the name of the PyQt6 package containing the class, and is only used for +// the python signature + +PYQT_CLASS(QtCore, QByteArray); +PYQT_CLASS(QtCore, QDateTime); +PYQT_CLASS(QtCore, QDir); +PYQT_CLASS(QtCore, QFileInfo); +PYQT_CLASS(QtCore, QObject); +PYQT_CLASS(QtCore, QSize); +PYQT_CLASS(QtCore, QUrl); + +PYQT_CLASS(QtGui, QColor); +PYQT_CLASS(QtGui, QIcon); +PYQT_CLASS(QtGui, QPixmap); + +PYQT_CLASS(QtWidgets, QMainWindow); +PYQT_CLASS(QtWidgets, QWidget); + +#undef METADATA + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h new file mode 100644 index 00000000..f246ed32 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h @@ -0,0 +1,56 @@ +#ifndef PYTHON_PYBIND11_QT_QFLAGS_HPP +#define PYTHON_PYBIND11_QT_QFLAGS_HPP + +#include + +#include + +namespace pybind11::detail { + + // QFlags + // + template + struct type_caster> { + PYBIND11_TYPE_CASTER(QFlags, const_name("QFlags[") + make_caster::name + + const_name("]")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool) + { + PyObject* tmp = PyNumber_Long(src.ptr()); + + if (!tmp) { + return false; + } + + // we do an intermediate extraction to T but this actually + // can contains multiple values + T flag_value = static_cast(PyLong_AsLong(tmp)); + Py_DECREF(tmp); + + value = QFlags(flag_value); + + return !PyErr_Occurred(); + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QFlags const& src, return_value_policy /* policy */, + handle /* parent */) + { + return PyLong_FromLong(static_cast(src)); + } + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/pybind11_qt_basic.cpp b/src/pybind11-qt/pybind11_qt_basic.cpp new file mode 100644 index 00000000..d82ec2ba --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_basic.cpp @@ -0,0 +1,156 @@ +#include "pybind11_qt/pybind11_qt_basic.h" + +#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" + +namespace pybind11::detail { + + template + QString qstring_from_stdstring(std::basic_string const& s) + { + if constexpr (std::is_same_v) { + return QString::fromStdString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdWString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU16String(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU32String(s); + } + } + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool type_caster::load(handle src, bool) + { + PyObject* objPtr = src.ptr(); + + if (PyBytes_Check(objPtr)) { + value = QString::fromUtf8(PyBytes_AsString(objPtr)); + return true; + } + else if (PyUnicode_Check(objPtr)) { + switch (PyUnicode_KIND(objPtr)) { + case PyUnicode_1BYTE_KIND: + value = QString::fromUtf8(PyUnicode_AsUTF8(objPtr)); + break; + case PyUnicode_2BYTE_KIND: + value = QString::fromUtf16( + reinterpret_cast(PyUnicode_2BYTE_DATA(objPtr)), + PyUnicode_GET_LENGTH(objPtr)); + break; + case PyUnicode_4BYTE_KIND: + value = QString::fromUcs4( + reinterpret_cast(PyUnicode_4BYTE_DATA(objPtr)), + PyUnicode_GET_LENGTH(objPtr)); + break; + default: + return false; + } + + return true; + } + else { + return false; + } + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + handle type_caster::cast(QString src, return_value_policy /* policy */, + handle /* parent */) + { + return PyUnicode_DecodeUTF16(reinterpret_cast(src.utf16()), + 2 * src.length(), nullptr, 0); + } + + bool type_caster::load(handle src, bool) + { + // test for string first otherwise PyList_Check also works + if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PySequence_Check(src.ptr())) { + // we could check if all the elements can be converted to QString + // and store a QStringList in the QVariant but I am not sure that is + // really useful. + value = src.cast(); + return true; + } + else if (PyMapping_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (src.is(pybind11::none())) { + value = QVariant(); + return true; + } + else if (PyDict_Check(src.ptr())) { + value = src.cast(); + return true; + } + // PyBool will also return true for PyLong_Check but not the other way + // around, so the order here is relevant. + else if (PyBool_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PyLong_Check(src.ptr())) { + // QVariant doesn't have long. It has int or long long. Given that + // on m/s, long is 32 bits for 32- and 64- bit code... + value = src.cast(); + return true; + } + else { + return false; + } + } + + handle type_caster::cast(QVariant var, return_value_policy policy, + handle parent) + { + switch (var.typeId()) { + case QMetaType::UnknownType: + return Py_None; + case QMetaType::Int: + return PyLong_FromLong(var.toInt()); + case QMetaType::UInt: + return PyLong_FromUnsignedLong(var.toUInt()); + 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: + return type_caster::cast(var.toStringList(), policy, parent); + case QMetaType::QVariantList: + return type_caster::cast(var.toList(), policy, parent); + case QMetaType::QVariantMap: + return type_caster::cast(var.toMap(), policy, parent); + default: { + PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.userType()); + throw pybind11::error_already_set(); + } + } + } + +} // namespace pybind11::detail diff --git a/src/pybind11-qt/pybind11_qt_sip.cpp b/src/pybind11-qt/pybind11_qt_sip.cpp new file mode 100644 index 00000000..69ad4b86 --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_sip.cpp @@ -0,0 +1,105 @@ +#include "pybind11_qt/details/pybind11_qt_sip.h" + +#include + +#include + +#include + +namespace py = pybind11; + +namespace pybind11::detail::qt { + + const sipAPIDef* sipAPI() + { + std::string exception; + static const sipAPIDef* sipApi = nullptr; + if (sipApi == nullptr) { + PyImport_ImportModule("PyQt6.sip"); + + { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + } + + sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt6.sip._C_API", 0); + if (sipApi == NULL) { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + } + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + } + 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-qt/pybind11_qt_utils.cpp b/src/pybind11-qt/pybind11_qt_utils.cpp new file mode 100644 index 00000000..249bed58 --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_utils.cpp @@ -0,0 +1,11 @@ +#include "pybind11_qt/details/pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + pybind11::object get_attr_rec(std::string_view package, std::string_view path) + { + + return module_::import("operator") + .attr("attrgetter")(path.data())(module_::import(package.data())); + } +} // namespace pybind11::detail::qt diff --git a/src/pybind11-utils/CMakeLists.txt b/src/pybind11-utils/CMakeLists.txt new file mode 100644 index 00000000..77895b63 --- /dev/null +++ b/src/pybind11-utils/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(pybind11-utils STATIC + ./include/pybind11_utils/functional.h + ./include/pybind11_utils/generator.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) + +add_library(pybind11::utils ALIAS pybind11-utils) diff --git a/src/pybind11-utils/README.md b/src/pybind11-utils/README.md new file mode 100644 index 00000000..46a92568 --- /dev/null +++ b/src/pybind11-utils/README.md @@ -0,0 +1,50 @@ +# pybind11-utils + +This library contains some utility stuff for `pybind11` + +## smart_variant_wrapper.h + +Expose a function `mo2::python::wrap_arguments` and a template +`mo2::python::smart_variant` that can be used to expose more interesting types Python +than the C++ one, e.g., accept `os.PathLike` and `QFileInfo` when a simple `QString` is +expected. + +A toy example can be found in the test folder at +[`tests/python/test_argument_wrapper.cpp`](../../tests/python/test_argument_wrapper.cpp). + +More concrete examples can be found in +[`mobase/pybind11_all.h`](../mobase/pybind11_all.h) for `FileWrapper` and +`DirectoryWrapper`. + +## functional.h + +TODO: updated version of `` that should check the signature +a bit more when creating `std::function` (similar to previous implementation). + +## shared_cpp_owner.h + +Expose a macro `MO2_PYBIND11_SHARED_CPP_HOLDER` that can be used to declare that +`std::shared_ptr<...>` must hold-on their associate Python object. + +```cpp +// use the macro on the type to be exposed (with a trampoline class) +MO2_PYBIND11_SHARED_CPP_HOLDER(ISaveGame) + +// use std::shared_ptr<> as the holder for the class +py::class_>(...); +``` + +Using the `MO2_PYBIND11_SHARED_CPP_HOLDER` (must be present in all files manipulating +`ISaveGame` between C++ and Python) ensure that the Python instance remains alive +alongside the C++ one. + +The `MO2_PYBIND11_SHARED_CPP_HOLDER` declares a specialization of `type_caster<>` that +alters the `std::shared_ptr` by return a `std::shared_ptr<>` that owns the Python +object (via a `pybind11::object`) but does not release the C++ one - The C++ object is +owned by the Python one, so the relation is as follows: + +- The `std::shared_ptr` manipulated in C++ maintains the `pybind11::object` alive + through a custom deleter but DOES NOT release the C++ object when the reference count + reaches 0. +- The Python object holds a standard `std::shared_ptr` that will release the object + when the reference count reaches 0. diff --git a/src/pybind11-utils/functional.cpp b/src/pybind11-utils/functional.cpp new file mode 100644 index 00000000..4cc7b6c8 --- /dev/null +++ b/src/pybind11-utils/functional.cpp @@ -0,0 +1,28 @@ +#include "pybind11_utils/functional.h" + +namespace py = pybind11; + +namespace mo2::python::detail { + + bool has_compatible_arity(py::function fn, std::size_t arity) + { + auto inspect = py::module_::import("inspect"); + auto arg_spec = inspect.attr("getfullargspec")(fn); + py::object args = arg_spec.attr("args"), varargs = arg_spec.attr("varargs"), + defaults = arg_spec.attr("defaults"); + + auto args_count = args.is(py::none()) ? 0 : py::len(args); + auto defaults_count = defaults.is(py::none()) ? 0 : py::len(defaults); + + if (inspect.attr("ismethod")(fn).cast() && py::hasattr(fn, "__self__")) { + --args_count; + } + + auto required_count = args_count - defaults_count; + + return required_count <= arity // cannot require more parameters than given, + && (args_count >= arity || + !varargs.is_none()); // must accept enough parameters. + } + +} // namespace mo2::python::detail diff --git a/src/pybind11-utils/include/pybind11_utils/functional.h b/src/pybind11-utils/include/pybind11_utils/functional.h new file mode 100644 index 00000000..8662d825 --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/functional.h @@ -0,0 +1,155 @@ +#ifndef PYTHON_PYBIND11_FUNCTIONAL_H +#define PYTHON_PYBIND11_FUNCTIONAL_H + +#include + +namespace mo2::python::detail { + + // check if the given function is valid for a C++ function with the + // given arity + // + bool has_compatible_arity(pybind11::function handle, std::size_t arity); + +} // namespace mo2::python::detail + +namespace pybind11::detail { + + // custom type_caster for std::function<> + // + // most of this is from pybind11 except that we also check arity of the function to + // allow overloaded function based on arity of argument + // + template + struct type_caster> { + using type = std::function; + using retval_type = + conditional_t::value, void_type, Return>; + using function_type = Return (*)(Args...); + + public: + bool load(handle src, bool convert) + { + if (src.is_none()) { + // Defer accepting None to other overloads (if we aren't in convert + // mode): + if (!convert) { + return false; + } + return true; + } + + if (!isinstance(src)) { + return false; + } + + auto func = reinterpret_borrow(src); + + /* + When passing a C++ function as an argument to another C++ + function via Python, every function call would normally involve + a full C++ -> Python -> C++ roundtrip, which can be prohibitive. + Here, we try to at least detect the case where the function is + stateless (i.e. function pointer or lambda function without + captured variables), in which case the roundtrip can be avoided. + */ + if (auto cfunc = func.cpp_function()) { + auto* cfunc_self = PyCFunction_GET_SELF(cfunc.ptr()); + if (isinstance(cfunc_self)) { + auto c = reinterpret_borrow(cfunc_self); + auto* rec = (function_record*)c; + + while (rec != nullptr) { + if (rec->is_stateless && + same_type(typeid(function_type), + *reinterpret_cast( + rec->data[1]))) { + struct capture { + function_type f; + }; + value = ((capture*)&rec->data)->f; + return true; + } + rec = rec->next; + } + } + // PYPY segfaults here when passing builtin function like sum. + // Raising an fail exception here works to prevent the segfault, but + // only on gcc. See PR #1413 for full details + } + + // !MO2! - check arity + + if (!mo2::python::detail::has_compatible_arity(func, sizeof...(Args))) { + return false; + } + + // !MO2! - everything below is copy/paste from pybind11 + + // ensure GIL is held during functor destruction + struct func_handle { + function f; +#if !(defined(_MSC_VER) && _MSC_VER == 1916 && defined(PYBIND11_CPP17)) + // This triggers a syntax error under very special conditions (very + // weird indeed). + explicit +#endif + func_handle(function&& f_) noexcept + : f(std::move(f_)) + { + } + func_handle(const func_handle& f_) { operator=(f_); } + func_handle& operator=(const func_handle& f_) + { + gil_scoped_acquire acq; + f = f_.f; + return *this; + } + ~func_handle() + { + gil_scoped_acquire acq; + function kill_f(std::move(f)); + } + }; + + // to emulate 'move initialization capture' in C++11 + struct func_wrapper { + func_handle hfunc; + explicit func_wrapper(func_handle&& hf) noexcept : hfunc(std::move(hf)) + { + } + Return operator()(Args... args) const + { + gil_scoped_acquire acq; + object retval(hfunc.f(std::forward(args)...)); + return retval.template cast(); + } + }; + + value = func_wrapper(func_handle(std::move(func))); + return true; + } + + template + static handle cast(Func&& f_, return_value_policy policy, handle /* parent */) + { + if (!f_) { + return none().inc_ref(); + } + + auto result = f_.template target(); + if (result) { + return cpp_function(*result, policy).release(); + } + return cpp_function(std::forward(f_), policy).release(); + } + + PYBIND11_TYPE_CASTER(type, const_name("Callable[[") + + concat(make_caster::name...) + + const_name("], ") + + make_caster::name + + const_name("]")); + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-utils/include/pybind11_utils/generator.h b/src/pybind11-utils/include/pybind11_utils/generator.h new file mode 100644 index 00000000..cbf9d186 --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/generator.h @@ -0,0 +1,57 @@ +#ifndef PYTHON_PYBIND11_GENERATOR_H +#define PYTHON_PYBIND11_GENERATOR_H + +#include + +#include + +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 + struct generator_state { + std::generator g; + decltype(g.begin()) it; + + generator_state(std::generator gen) : g(std::move(gen)), it(g.begin()) {} + }; + } // namespace detail + + // create a Python generator from a C++ generator + // + template + auto make_generator(std::generator g, Args&&... args) + { + using state = detail::generator_state; + + namespace py = pybind11; + if (!py::detail::get_type_info(typeid(state), false)) { + py::class_(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)...); + } + + return py::cast(state{std::move(g)}); + } + +} // namespace mo2::python + +#endif diff --git a/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h b/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h new file mode 100644 index 00000000..02c2111f --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h @@ -0,0 +1,90 @@ +#ifndef PYTHON_PYBIND11_SHARED_CPP_OWNER_H +#define PYTHON_PYBIND11_SHARED_CPP_OWNER_H + +#include + +// pybind11 has some issues when a Python classes extend a C++ wrapper since the Python +// object is not kept alive alongside the returned object +// +// there is a pybind11 branch called "smart_holder" that tries to solve this in a very +// complicated way (with many other features) +// +// here, we simply use a custom type_caster<> for the classes we need - see the actual +// definition in mo2::python::detail below +// +// IMPORTANT: this only works for classes that are managed by shared_ptr on the C++ +// side, not Qt object (see pybind11-qt holder for that) +// + +namespace mo2::python::detail { + + template + struct shared_cpp_owner_caster + : pybind11::detail::copyable_holder_caster { + + // note that the actual holder type might be different in term of constness + using type = Type; + using holder_type = SharedType; + + using base = pybind11::detail::copyable_holder_caster; + using base::holder; + using base::value; + + // in load, we use the default type_caster<> to extract the shared pointer, then + // we replace it by a custom one + // + // the custom shared_ptr<> holds the py::object BUT does not really manage the + // C++ object because it will ref-count but not delete it + // + // this should work because here it's how it works: + // - the Python object holds a standard shared_ptr<> for the C++ object -> the + // C++ object remains alive as long as the Python one remains alive + // - the C++ object holds a shared_ptr<> that manages the python object -> the + // Python object remains alive as-long as there is a shared_ptr<> on the C++ + // side + // + bool load(pybind11::handle src, bool convert) + { + namespace py = pybind11; + + if (!base::load(src, convert)) { + return false; + } + + holder.reset(holder.get(), [pyobj = py::reinterpret_borrow( + src)](auto*) mutable { + py::gil_scoped_acquire s; + pyobj = py::object(); + + // we do NOT delete the object here - if this was the last reference to + // the Python object, the Python object will delete it + }); + + return true; + } + + // cast simply forward to the original type_caster<> + // + static pybind11::handle cast(const holder_type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return base::cast(src, policy, parent); + } + }; + +} // namespace mo2::python::detail + +#define MO2_PYBIND11_SHARED_CPP_HOLDER(Type) \ + namespace pybind11::detail { \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster> {}; \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster< \ + Type, std::shared_ptr> {}; \ + } + +#endif diff --git a/src/pybind11-utils/include/pybind11_utils/smart_variant.h b/src/pybind11-utils/include/pybind11_utils/smart_variant.h new file mode 100644 index 00000000..bd7bd9b5 --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/smart_variant.h @@ -0,0 +1,53 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_H + +#include + +namespace mo2::python { + + namespace detail { + + // simple template class that should be specialized to expose proper fromXXX + // methods + // + template + struct smart_variant_converter { + template + static T from(U&& u) + { + return T{std::forward(u)}; + } + }; + + } // namespace detail + + // a smart_variant is a std::variant that can be automatically converted to any of + // its type via custom operator T() + // + // user should specialize detail::smart_variant_converter to provide proper + // conversions + // + template + struct smart_variant : std::variant { + using std::variant::variant; + + template ...>, int> = 0> + operator T() const + { + return std::visit( + [](auto const& t) -> T { + if constexpr (std::is_same_v, T>) { + return t; + } + else { + return detail::smart_variant_converter::from(t); + } + }, + *this); + } + }; + +} // namespace mo2::python + +#endif diff --git a/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h b/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h new file mode 100644 index 00000000..123d95ce --- /dev/null +++ b/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h @@ -0,0 +1,180 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H + +#include +#include + +#include +#include + +#include "smart_variant.h" + +namespace mo2::python { + + namespace detail { + + // simple helper class that expose a ::type attribute which is U is I is in Is, + // V otherwise + template + struct wrap_arg; + + template + struct wrap_arg> { + using type = + std::conditional_t...>, + U, V>; + }; + + // helper type for wrap_arg + template + using wrap_arg_t = typename wrap_arg::type; + + template + auto wrap_arguments_impl(std::index_sequence, Fn&& fn, R (*)(Args...), + std::index_sequence) + { + return [fn = std::forward(fn)]( + wrap_arg_t>... args) { + return std::invoke(fn, std::forward(args)...); + }; + } + + template + auto make_convertible_index_sequence(std::index_sequence) + { + return std::index_sequence<(std::is_convertible_v ? Is : -1)...>{}; + } + + template + auto wrap_arguments_impl(Fn&& fn, R (*sg)(Args...)) + { + if constexpr (sizeof...(Is) == 0) { + return wrap_arguments_impl( + make_convertible_index_sequence( + std::make_index_sequence{}), + std::forward(fn), sg, + std::make_index_sequence{}); + } + else { + return wrap_arguments_impl( + std::index_sequence{}, std::forward(fn), sg, + std::make_index_sequence{}); + } + } + + template + auto wrap_return_impl(Fn&& fn, R (*)(Args...)) + { + return [fn = std::forward(fn)](Args... args) { + return T{std::invoke(fn, std::forward(args)...)}; + }; + } + + // make_python_function_signature: return a null-pointer with the proper type + // for the given function + + template + struct function_signature { + using type = + pybind11::detail::function_signature_t>; + }; + + template + struct function_signature { + using type = R(Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + using function_signature_t = typename function_signature::type; + + template + class wrap_type_caster { + using variant_type = std::variant; + using variant_caster = pybind11::detail::make_caster; + + public: + PYBIND11_TYPE_CASTER(Type, variant_caster::name); + + bool load(pybind11::handle src, bool convert) + { + variant_caster caster; + + if (!caster.load(src, convert)) { + return false; + } + + value = std::visit( + [](auto const& fn) { + return Type(fn); + }, + static_cast(caster)); + return true; + } + + static pybind11::handle cast(const Type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return variant_caster::cast(variant_type(std::in_place_index<0>, src), + policy, parent); + } + }; + + } // namespace detail + + // wrap the given function-like object to accept T instead of the specified + // arguments at the specified positions + // + // if the list of positions is empty, replace all arguments that can be converted to + // T + // + template + auto wrap_arguments(Fn&& fn) + { + return detail::wrap_arguments_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + + // wrap the given function-like object to return T instead of the specified type + // + template + auto wrap_return(Fn&& fn) + { + return detail::wrap_return_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + +} // namespace mo2::python + +namespace pybind11::detail { + + template + struct type_caster<::mo2::python::smart_variant> + : variant_caster<::mo2::python::smart_variant> {}; + +} // namespace pybind11::detail + +#endif diff --git a/src/pythonrunner_en.ts b/src/pythonrunner_en.ts deleted file mode 100644 index 5cd08020..00000000 --- a/src/pythonrunner_en.ts +++ /dev/null @@ -1,90 +0,0 @@ - - - - - ProxyPython - - - Python Initialization failed - - - - - On a previous start the Python Plugin failed to initialize. -Either the value in Settings->Plugins->ProxyPython->plugin_dir is set incorrectly or it is empty and auto-detection doesn't work for whatever reason. -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. - - - - - Proxy Plugin to allow plugins written in python to be loaded - - - - - Python not installed or not found - - - - - Python version is incompatible - - - - - Invalid python path - - - - - Initializing Python failed - - - - - Python auto-detection failed - - - - - ModOrganizer path contains a semicolon - - - - - invalid problem key %1 - - - - - Some MO plugins require the python interpreter to be installed. These plugins will not even show up in settings-&gt;plugins.<br>If you want to use those plugins, please install the 32-bit version of Python 2.7.x from <a href="%1">%1</a>.<br>This is only required to use some extended functionality in MO, you do not need Python to play the game. - - - - - Your installed python version has a different version than 2.7. Some MO plugins may not work.<br>If you have multiple versions of python installed you may have to configure the path to 2.7 (32 bit) in the settings dialog.<br>This is only required to use some extended functionality in MO, you do not need Python to play the game. - - - - - Please set python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation. - - - - - The auto-detection of the python path failed. I don't know why this would happen but you can try to fix it by setting python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation. - - - - - Sorry, I don't know any details. Most likely your python installation is not supported. - - - - - The path to Mod Organizer (%1) contains a semicolon. <br>While this is legal on NTFS drives there is a lot of software that doesn't handle it correctly.<br>Unfortunately MO depends on libraries that seem to fall into that group.<br>As a result the python plugin can't be loaded.<br>The only solution I can offer is to remove the semicolon / move MO to a path without a semicolon. - - - - diff --git a/src/runner/CMakeLists.txt b/src/runner/CMakeLists.txt index e237e074..faee5c61 100644 --- a/src/runner/CMakeLists.txt +++ b/src/runner/CMakeLists.txt @@ -1,77 +1,27 @@ -CMAKE_MINIMUM_REQUIRED (VERSION 2.8.11) - -SET(PROJ_NAME pythonrunner) - -PROJECT(${PROJ_NAME}) - -CMAKE_POLICY(SET CMP0020 NEW) - - -FILE(GLOB ${PROJ_NAME}_SRCS *.cpp) -FILE(GLOB ${PROJ_NAME}_HDRS *.h) - -SET(CMAKE_INCLUDE_CURRENT_DIR ON) -SET(CMAKE_AUTOMOC ON) -SET(CMAKE_AUTOUIC ON) -ADD_DEFINITIONS(-DQT_NO_KEYWORDS) -FIND_PACKAGE(Qt5Widgets REQUIRED) -FIND_PACKAGE(Qt5Network REQUIRED) -#QT5_WRAP_UI(${PROJ_NAME}_UIHDRS ${${PROJ_NAME}_FORMS}) -FIND_PACKAGE(Qt5LinguistTools) -QT5_CREATE_TRANSLATION(${PROJ_NAME}_translations_qm ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src/${PROJ_NAME}_en.ts) - -SET(Boost_USE_STATIC_LIBS OFF) -SET(Boost_USE_MULTITHREADED ON) -SET(Boost_USE_STATIC_RUNTIME OFF) -FIND_PACKAGE(Boost REQUIRED) - -IF (Boost_FOUND) - INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS}) -ENDIF (Boost_FOUND) - -SET(default_project_path "${DEPENDENCIES_DIR}/modorganizer_super") -GET_FILENAME_COMPONENT(${default_project_path} ${default_project_path} REALPATH) - -SET(project_path "${default_project_path}" CACHE PATH "path to the other mo projects") -SET(lib_path "${project_path}/../../install/libs") - -INCLUDE_DIRECTORIES(${project_path}/uibase/src - ${project_path}/game_features/src - ${PYTHON_ROOT}/Include - ${PYTHON_ROOT}/PC) - -LINK_DIRECTORIES(${lib_path} - ${PYTHON_ROOT}/PCbuild - ${Boost_LIBRARY_DIRS}) - -ADD_LIBRARY(${PROJ_NAME} SHARED ${${PROJ_NAME}_HDRS} ${${PROJ_NAME}_SRCS} ${${PROJ_NAME}_translations_qm}) -TARGET_LINK_LIBRARIES(${PROJ_NAME} - Qt5::Widgets - Qt5::Network - uibase) - -ADD_DEFINITIONS(-DPYTHONRUNNER_LIBRARY) - -IF (MSVC) - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES COMPILE_FLAGS "/std:c++latest") -ENDIF() -IF (MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 4) - # 32 bits - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES LINK_FLAGS "/LARGEADDRESSAWARE") -ENDIF() - -IF (NOT "${OPTIMIZE_COMPILE_FLAGS}" STREQUAL "") - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES COMPILE_FLAGS_RELWITHDEBINFO ${OPTIMIZE_COMPILE_FLAGS}) -ENDIF() -IF (NOT "${OPTIMIZE_LINK_FLAGS}" STREQUAL "") - SET_TARGET_PROPERTIES(${PROJ_NAME} PROPERTIES LINK_FLAGS_RELWITHDEBINFO ${OPTIMIZE_LINK_FLAGS}) -ENDIF() - -############### -## Installation - -INSTALL(TARGETS ${PROJ_NAME} - RUNTIME DESTINATION bin/plugins/data - ARCHIVE DESTINATION libs) -INSTALL(FILES $ - DESTINATION pdb) +cmake_minimum_required(VERSION 3.16) + +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 +) +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 + +# force runner to build mobase +add_dependencies(runner mobase) diff --git a/src/runner/SConscript b/src/runner/SConscript deleted file mode 100644 index 0f25b4ef..00000000 --- a/src/runner/SConscript +++ /dev/null @@ -1,63 +0,0 @@ -Import('qt_env') - -env = qt_env.Clone() - -env.EnableQtModules('Core', 'Gui', 'Widgets') - -env.AppendUnique(CPPDEFINES = 'PYTHONRUNNER_LIBRARY') - -# suppress a few warnings caused by boost vs vc++ paranoia -# NB HAVE_ROUND causes a warning because it's defined in pythonrunner.cpp -env.AppendUnique(CPPDEFINES = [ - '_SCL_SECURE_NO_WARNINGS', - 'HAVE_ROUND' -]) - -# Boost produces very long names with msvc truncates. Doesn't seem to cause -# problems. -env.AppendUnique(CPPFLAGS = [ '-wd4503' ]) - -# QMAKE_CXXFLAGS += /Zi -# QMAKE_LFLAGS += /DEBUG - -env.RequireLibraries('uibase') - -# AppendUnique appears not to work with QT4 -env['CPPPATH'] += [ - '${BOOSTPATH}', - '${PYTHONPATH}\\include', -] - -env.AppendUnique(LIBS = [ - 'python27', # Could be done better -]) - -env.AppendUnique(LIBPATH = [ - '${PYTHONPATH}/libs', # Could be done better -]) - -# We have to 'persuade' moc to generate certain other targets and inject them -# into the list of cpps -targets = env.AddExtraMoc(env.Glob('*.h')) - -# pythontoolwrapper doesnt compile and isn't in the project file -cpp_files = [ - x for x in Glob('*.cpp') if x.name != 'pythontoolwrapper.cpp' -] - -lib = env.SharedLibrary('pythonRunner', cpp_files + targets) - -env.InstallModule(lib, 'plugins/data') - -# However... -# we do need boost-python and pythonvv.dll and pythonvv.zip (though the last -# isn't needed to just bring up the module) -#env.Pseudo('install') -#env.Depends('install', -# (env.Install('${INSTALL_PATH}', 'pyCfg.py'), -# env.Install(os.path.join('${INSTALL_PATH}', 'data'), -# ( ui, rc, 'settings.json' )), -# )) - -res = env['QT_USED_MODULES'] -Return('res') diff --git a/src/runner/error.cpp b/src/runner/error.cpp deleted file mode 100644 index e17cd14f..00000000 --- a/src/runner/error.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef Q_MOC_RUN -#include -#endif -#include -#include -#include "error.h" - -using namespace MOBase; -namespace bpy = boost::python; - -void reportPythonError() -{ - if (PyErr_Occurred()) { - ErrWrapper &errWrapper = ErrWrapper::instance(); - - errWrapper.startRecordingExceptionMessage(); - PyErr_Print(); - errWrapper.stopRecordingExceptionMessage(); - - QString errMsg = errWrapper.getLastExceptionMessage(); - - throw MyException(errMsg); - } else { - throw MyException("An unexpected C++ exception was thrown in python code"); - } -} - -ErrWrapper & ErrWrapper::instance() -{ - static ErrWrapper err; - return err; -} - -void ErrWrapper::write(const char * message) -{ - buffer << message; - if (buffer.tellp() != 0 && buffer.str().back() == '\n') - { - // actually put the string in a variable so it doesn't get destroyed as soon as we get a pointer to its data - std::string string = buffer.str().substr(0, buffer.str().length() - 1); - qCritical().nospace().noquote() << string.c_str(); - buffer = std::stringstream(); - } - - if (recordingExceptionMessage) - { - lastException << message; - } -} - -void ErrWrapper::startRecordingExceptionMessage() -{ - recordingExceptionMessage = true; - lastException = std::stringstream(); -} - -void ErrWrapper::stopRecordingExceptionMessage() -{ - recordingExceptionMessage = false; -} - -QString ErrWrapper::getLastExceptionMessage() -{ - return QString::fromStdString(lastException.str()); -} diff --git a/src/runner/error.h b/src/runner/error.h index 2cadb572..341c7ea3 100644 --- a/src/runner/error.h +++ b/src/runner/error.h @@ -1,26 +1,82 @@ -#ifndef ERROR_H -#define ERROR_H -#include -#include - -// turn an error from the python interpreter into an exception -void reportPythonError(); - -struct ErrWrapper -{ - static ErrWrapper & instance(); - - void write(const char * message); - - void startRecordingExceptionMessage(); - - void stopRecordingExceptionMessage(); - - QString getLastExceptionMessage(); - - std::stringstream buffer; - bool recordingExceptionMessage; - std::stringstream lastException; -}; - -#endif // ERROR_H +#ifndef ERROR_H +#define ERROR_H + +#include + +#include + +#include + +#include + +namespace pyexcept { + + /** + * @brief Exception to throw when a python implementation does not implement + * a pure virtual function. + */ + class MissingImplementation : public MOBase::Exception { + public: + MissingImplementation(std::string const& className, + std::string const& methodName) + : Exception(QString::fromStdString( + std::format("Python class implementing \"{}\" has no " + "implementation of method \"{}\".", + className, methodName))) + { + } + }; + + /** + * @brief Exception to throw when a python error occurs. + */ + class PythonError : public MOBase::Exception { + public: + /** + * @brief Create a new PythonError, fetching the error message from + * python. If the message cannot be retrieved, `defaultErrorMessage()` + * is used instead. + */ + PythonError(pybind11::error_already_set const& ex) : Exception(ex.what()) {} + + /** + * @brief Create a new PythonError with the given message. + * + * @param message Message for the exception. + */ + PythonError(QString message) : Exception(message) {} + }; + + /** + * @brief Exception to throw when an unknown error occured. This is + * typically thrown from a catch(...) block. + */ + class UnknownException : public MOBase::Exception { + public: + /** + * @brief Create a new UnknownException with the default message. + * + * @see defaultErrorMessage + */ + UnknownException() : Exception(defaultErrorMessage()) {} + + /** + * @brief Create a new UnknownException with the given message. + * + * @param message Message for the exception. + */ + UnknownException(QString message) : Exception(message) {} + + protected: + /** + * + */ + static QString defaultErrorMessage() + { + return QObject::tr("An unknown exception was thrown in python code."); + } + }; + +} // namespace pyexcept + +#endif // ERROR_H diff --git a/src/runner/gamefeatureswrappers.cpp b/src/runner/gamefeatureswrappers.cpp deleted file mode 100644 index 5642e56b..00000000 --- a/src/runner/gamefeatureswrappers.cpp +++ /dev/null @@ -1,301 +0,0 @@ -#include "gamefeatureswrappers.h" - -#include - -#include - -#include -#include -#include -#include - -#include "gilock.h" -#include "pythonwrapperutilities.h" - -///////////////////////////// -/// BSAInvalidation Wrapper - - -bool BSAInvalidationWrapper::isInvalidationBSA(const QString &bsaName) -{ - return basicWrapperFunctionImplementation(this, "isInvalidationBSA", bsaName); -} - -void BSAInvalidationWrapper::deactivate(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "deactivate", boost::python::ptr(profile)); -} - -void BSAInvalidationWrapper::activate(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "activate", boost::python::ptr(profile)); -} - -bool BSAInvalidationWrapper::prepareProfile(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); -} -/// end BSAInvalidation Wrapper -///////////////////////////// -/// DataArchives Wrapper - - -QStringList DataArchivesWrapper::vanillaArchives() const -{ - return basicWrapperFunctionImplementation(this, "vanillaArchives"); -} - -QStringList DataArchivesWrapper::archives(const MOBase::IProfile *profile) const -{ - return basicWrapperFunctionImplementation(this, "archives", boost::python::ptr(profile)); -} - -void DataArchivesWrapper::addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) -{ - return basicWrapperFunctionImplementation(this, "addArchive", boost::python::ptr(profile), index, archiveName); -} - -void DataArchivesWrapper::removeArchive(MOBase::IProfile *profile, const QString &archiveName) -{ - return basicWrapperFunctionImplementation(this, "removeArchive", boost::python::ptr(profile), archiveName); -} -/// end DataArchives Wrapper -///////////////////////////// -/// GamePlugins Wrapper - - -void GamePluginsWrapper::writePluginLists(const MOBase::IPluginList * pluginList) -{ - return basicWrapperFunctionImplementation(this, "writePluginLists", boost::python::ptr(pluginList)); -} - -void GamePluginsWrapper::readPluginLists(MOBase::IPluginList * pluginList) -{ - return basicWrapperFunctionImplementation(this, "readPluginLists", boost::python::ptr(pluginList)); -} - -void GamePluginsWrapper::getLoadOrder(QStringList &loadOrder) -{ - return basicWrapperFunctionImplementation(this, "getLoadOrder", loadOrder); -} - -bool GamePluginsWrapper::lightPluginsAreSupported() -{ - return basicWrapperFunctionImplementation(this, "lightPluginsAreSupported"); -} - -/// end GamePlugins Wrapper -///////////////////////////// -/// LocalSavegames Wrapper - - -MappingType LocalSavegamesWrapper::mappings(const QDir & profileSaveDir) const -{ - return basicWrapperFunctionImplementation(this, "mappings", profileSaveDir); -} - -bool LocalSavegamesWrapper::prepareProfile(MOBase::IProfile * profile) -{ - return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); -} - -/// end LocalSavegames Wrapper -///////////////////////////// -/// SaveGameInfo Wrapper - - -MOBase::ISaveGame const * SaveGameInfoWrapper::getSaveGameInfo(QString const & file) const -{ - return basicWrapperFunctionImplementation(this, "getSaveGameInfo", file); -} - -SaveGameInfoWrapper::MissingAssets SaveGameInfoWrapper::getMissingAssets(QString const & file) const -{ - return basicWrapperFunctionImplementation(this, "getMissingAssets", file); -} - -MOBase::ISaveGameInfoWidget * SaveGameInfoWrapper::getSaveGameWidget(QWidget * parent) const -{ - qCritical("Calling method with unimplemented from_python converter."); - return basicWrapperFunctionImplementation(this, "getSaveGameWidget", boost::python::ptr(parent)); -} - -bool SaveGameInfoWrapper::hasScriptExtenderSave(QString const & file) const -{ - return basicWrapperFunctionImplementation(this, "hasScriptExtenderSave", file); -} -/// end SaveGameInfo Wrapper -///////////////////////////// -/// ScriptExtender Wrapper - -QString ScriptExtenderWrapper::BinaryName() const -{ - return basicWrapperFunctionImplementation(this, "BinaryName"); -} - -QString ScriptExtenderWrapper::PluginPath() const -{ - return basicWrapperFunctionImplementation(this, "PluginPath"); -} - -QString ScriptExtenderWrapper::loaderName() const -{ - return basicWrapperFunctionImplementation(this, "loaderName"); -} - -QString ScriptExtenderWrapper::loaderPath() const -{ - return basicWrapperFunctionImplementation(this, "loaderPath"); -} - -QStringList ScriptExtenderWrapper::saveGameAttachmentExtensions() const -{ - return basicWrapperFunctionImplementation(this, "saveGameAttachmentExtensions"); -} - -bool ScriptExtenderWrapper::isInstalled() const -{ - return basicWrapperFunctionImplementation(this, "isInstalled"); -} - -QString ScriptExtenderWrapper::getExtenderVersion() const -{ - return basicWrapperFunctionImplementation(this, "getExtenderVersion"); -} - -WORD ScriptExtenderWrapper::getArch() const -{ - return basicWrapperFunctionImplementation(this, "getArch"); -} - -/// end ScriptExtender Wrapper -///////////////////////////// -/// UnmanagedMods Wrapper - - -QStringList UnmanagedModsWrapper::mods(bool onlyOfficial) const -{ - return basicWrapperFunctionImplementation(this, "mods", onlyOfficial); -} - -QString UnmanagedModsWrapper::displayName(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "displayName", modName); -} - -QFileInfo UnmanagedModsWrapper::referenceFile(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "referenceFile", modName); -} - -QStringList UnmanagedModsWrapper::secondaryFiles(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "secondaryFiles", modName); -} -/// end UnmanagedMods Wrapper -///////////////////////////// - -template -void insertGameFeature(std::map &map, const boost::python::object &pyObject) -{ - map[std::type_index(typeid(T))] = boost::python::extract(pyObject)(); -} - -game_features_map_from_python::game_features_map_from_python() -{ - boost::python::converter::registry::push_back(&convertible, &construct, boost::python::type_id>()); -} - -void * game_features_map_from_python::convertible(PyObject * objPtr) -{ - return PyDict_Check(objPtr) ? objPtr : nullptr; -} - -void game_features_map_from_python::construct(PyObject * objPtr, boost::python::converter::rvalue_from_python_stage1_data * data) -{ - void *storage = ((boost::python::converter::rvalue_from_python_storage>*)data)->storage.bytes; - std::map *result = new (storage) std::map(); - boost::python::dict source(boost::python::handle<>(boost::python::borrowed(objPtr))); - boost::python::list keys = source.keys(); - int len = boost::python::len(keys); - for (int i = 0; i < len; ++i) - { - boost::python::object pyKey = keys[i]; - // pyKey should be a Boost.Python.class corresponding to a game feature. - std::string className = boost::python::extract(pyKey.attr("__name__"))(); - if (className == "BSAInvalidation") - insertGameFeature(*result, source[pyKey]); - else if (className == "DataArchives") - insertGameFeature(*result, source[pyKey]); - else if (className == "GamePlugins") - insertGameFeature(*result, source[pyKey]); - else if (className == "LocalSavegames") - insertGameFeature(*result, source[pyKey]); - else if (className == "SaveGameInfo") - insertGameFeature(*result, source[pyKey]); - else if (className == "ScriptExtender") - insertGameFeature(*result, source[pyKey]); - else if (className == "UnmanagedMods") - insertGameFeature(*result, source[pyKey]); - } - - data->convertible = storage; -} - -void registerGameFeaturesPythonConverters() -{ - namespace bpy = boost::python; - - game_features_map_from_python(); - - // Features require defs for all methods as Python can access C++ features - bpy::class_("BSAInvalidation") - .def("isInvalidationBSA", bpy::pure_virtual(&BSAInvalidation::isInvalidationBSA)) - .def("deactivate", bpy::pure_virtual(&BSAInvalidation::deactivate)) - .def("activate", bpy::pure_virtual(&BSAInvalidation::activate)) - ; - - bpy::class_("DataArchives") - .def("vanillaArchives", bpy::pure_virtual(&DataArchives::vanillaArchives)) - .def("archives", bpy::pure_virtual(&DataArchives::archives)) - .def("addArchive", bpy::pure_virtual(&DataArchives::addArchive)) - .def("removeArchive", bpy::pure_virtual(&DataArchives::removeArchive)) - ; - - bpy::class_("GamePlugins") - .def("writePluginLists", bpy::pure_virtual(&GamePlugins::writePluginLists)) - .def("readPluginLists", bpy::pure_virtual(&GamePlugins::readPluginLists)) - .def("getLoadOrder", bpy::pure_virtual(&GamePlugins::getLoadOrder)) - .def("lightPluginsAreSupported", bpy::pure_virtual(&GamePlugins::lightPluginsAreSupported)) - ; - - bpy::class_("LocalSavegames") - .def("mappings", bpy::pure_virtual(&LocalSavegames::mappings)) - .def("prepareProfile", bpy::pure_virtual(&LocalSavegames::prepareProfile)) - ; - - bpy::class_("SaveGameInfo") - .def("getSaveGameInfo", bpy::pure_virtual(&SaveGameInfo::getSaveGameInfo), bpy::return_value_policy()) - .def("getMissingAssets", bpy::pure_virtual(&SaveGameInfo::getMissingAssets)) - .def("getSaveGameWidget", bpy::pure_virtual(&SaveGameInfo::getSaveGameWidget), bpy::return_value_policy()) - .def("hasScriptExtenderSave", bpy::pure_virtual(&SaveGameInfo::hasScriptExtenderSave)) - ; - - bpy::class_("ScriptExtender") - .def("BinaryName", bpy::pure_virtual(&ScriptExtender::BinaryName)) - .def("PluginPath", bpy::pure_virtual(&ScriptExtender::PluginPath)) - .def("loaderName", bpy::pure_virtual(&ScriptExtender::loaderName)) - .def("loaderPath", bpy::pure_virtual(&ScriptExtender::loaderPath)) - .def("saveGameAttachmentExtensions", bpy::pure_virtual(&ScriptExtender::saveGameAttachmentExtensions)) - .def("isInstalled", bpy::pure_virtual(&ScriptExtender::isInstalled)) - .def("getExtenderVersion", bpy::pure_virtual(&ScriptExtender::getExtenderVersion)) - .def("getArch", bpy::pure_virtual(&ScriptExtender::getArch)) - ; - - bpy::class_("UnmanagedMods") - .def("mods", bpy::pure_virtual(&UnmanagedMods::mods)) - .def("displayName", bpy::pure_virtual(&UnmanagedMods::displayName)) - .def("referenceFile", bpy::pure_virtual(&UnmanagedMods::referenceFile)) - .def("secondaryFiles", bpy::pure_virtual(&UnmanagedMods::secondaryFiles)) - ; -} diff --git a/src/runner/gamefeatureswrappers.h b/src/runner/gamefeatureswrappers.h deleted file mode 100644 index 05191eff..00000000 --- a/src/runner/gamefeatureswrappers.h +++ /dev/null @@ -1,116 +0,0 @@ -#ifndef GAMEFEATURESWRAPPERS_H -#define GAMEFEATURESWRAPPERS_H - -#include -#include -#include -#include -#include -#include -#include - -// this might need turning off if Q_MOC_RUN is defined -#include - -///////////////////////////// -/// Wrapper declarations - -class BSAInvalidationWrapper : public BSAInvalidation, public boost::python::wrapper -{ -public: - static constexpr const char* className = "BSAInvalidationWrapper"; - using boost::python::wrapper::get_override; - - virtual bool isInvalidationBSA(const QString &bsaName) override; - virtual void deactivate(MOBase::IProfile *profile) override; - virtual void activate(MOBase::IProfile *profile) override; - virtual bool prepareProfile(MOBase::IProfile *profile) override; -}; - -class DataArchivesWrapper : public DataArchives, public boost::python::wrapper -{ -public: - static constexpr const char* className = "DataArchivesWrapper"; - using boost::python::wrapper::get_override; - - virtual QStringList vanillaArchives() const override; - virtual QStringList archives(const MOBase::IProfile *profile) const override; - virtual void addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) override; - virtual void removeArchive(MOBase::IProfile *profile, const QString &archiveName) override; -}; - -class GamePluginsWrapper : public GamePlugins, public boost::python::wrapper -{ -public: - static constexpr const char* className = "GamePluginsWrapper"; - using boost::python::wrapper::get_override; - - virtual void writePluginLists(const MOBase::IPluginList *pluginList) override; - virtual void readPluginLists(MOBase::IPluginList *pluginList) override; - virtual void getLoadOrder(QStringList &loadOrder) override; - virtual bool lightPluginsAreSupported() override; -}; - -class LocalSavegamesWrapper : public LocalSavegames, public boost::python::wrapper -{ -public: - static constexpr const char* className = "LocalSavegamesWrapper"; - using boost::python::wrapper::get_override; - - virtual MappingType mappings(const QDir &profileSaveDir) const override; - virtual bool prepareProfile(MOBase::IProfile *profile) override; -}; - -class SaveGameInfoWrapper : public SaveGameInfo, public boost::python::wrapper -{ -public: - static constexpr const char* className = "SaveGameInfoWrapper"; - using boost::python::wrapper::get_override; - - virtual MOBase::ISaveGame const *getSaveGameInfo(QString const &file) const override; - virtual MissingAssets getMissingAssets(QString const &file) const override; - virtual MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *parent = 0) const override; - virtual bool hasScriptExtenderSave(QString const &file) const override; -}; - -class ScriptExtenderWrapper : public ScriptExtender, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ScriptExtenderWrapper"; - using boost::python::wrapper::get_override; - - virtual QString BinaryName() const override; - virtual QString PluginPath() const override; - virtual QString loaderName() const override; - virtual QString loaderPath() const override; - virtual QStringList saveGameAttachmentExtensions() const override; - virtual bool isInstalled() const override; - virtual QString getExtenderVersion() const override; - virtual WORD getArch() const override; -}; - -class UnmanagedModsWrapper : public UnmanagedMods, public boost::python::wrapper -{ -public: - static constexpr const char* className = "UnmanagedModsWrapper"; - using boost::python::wrapper::get_override; - - virtual QStringList mods(bool onlyOfficial) const override; - virtual QString displayName(const QString &modName) const override; - virtual QFileInfo referenceFile(const QString &modName) const override; - virtual QStringList secondaryFiles(const QString &modName) const override; -}; - -/// end Wrapper declarations -///////////////////////////// - -struct game_features_map_from_python -{ - game_features_map_from_python(); - static void *convertible(PyObject *objPtr); - static void construct(PyObject *objPtr, boost::python::converter::rvalue_from_python_stage1_data *data); -}; - -void registerGameFeaturesPythonConverters(); - -#endif // GAMEFEATURESWRAPPERS_H diff --git a/src/runner/gilock.cpp b/src/runner/gilock.cpp deleted file mode 100644 index ed5985cd..00000000 --- a/src/runner/gilock.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "gilock.h" - -GILock::GILock() -{ - m_State = PyGILState_Ensure(); -} - -GILock::~GILock() -{ - PyGILState_Release(m_State); -} diff --git a/src/runner/gilock.h b/src/runner/gilock.h deleted file mode 100644 index 560e29a6..00000000 --- a/src/runner/gilock.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef GILOCK_H -#define GILOCK_H - - -#ifndef Q_MOC_RUN -#include -#endif // Q_MOC_RUN - -class GILock { -public: - GILock(); - ~GILock(); -private: - PyGILState_STATE m_State; -}; - - -#endif // GILOCK_H diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp deleted file mode 100644 index 127835e3..00000000 --- a/src/runner/proxypluginwrappers.cpp +++ /dev/null @@ -1,426 +0,0 @@ -#include "proxypluginwrappers.h" - -#include "gilock.h" -#include -#include -#include "pythonwrapperutilities.h" -#include "sipApiAccess.h" - -namespace boost -{ - // See bug https://connect.microsoft.com/VisualStudio/Feedback/Details/2852624 -#if (_MSC_VER == 1900) - template<> const volatile MOBase::IOrganizer* get_pointer(const volatile MOBase::IOrganizer* p) { return p; } - template<> const volatile MOBase::IModInterface* get_pointer(const volatile MOBase::IModInterface* p) { return p; } - template<> const volatile MOBase::IPluginGame* get_pointer(const volatile MOBase::IPluginGame* p) { return p; } - template<> const volatile MOBase::IProfile* get_pointer(const volatile MOBase::IProfile* p) { return p; } - template<> const volatile MOBase::IModList* get_pointer(const volatile MOBase::IModList* p) { return p; } - template<> const volatile MOBase::IPluginList* get_pointer(const volatile MOBase::IPluginList* p) { return p; } - template<> const volatile MOBase::IDownloadManager* get_pointer(const volatile MOBase::IDownloadManager* p) { return p; } - template<> const volatile MOBase::IModRepositoryBridge* get_pointer(const volatile MOBase::IModRepositoryBridge* p) { return p; } -#endif -} - -using namespace MOBase; - - -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) \ -bool class_name::init(MOBase::IOrganizer *moInfo) \ -{ \ - return basicWrapperFunctionImplementation(this, "init", boost::python::ptr(moInfo)); \ -} \ - \ -QString class_name::name() const \ -{ \ - return basicWrapperFunctionImplementation(this, "name"); \ -} \ - \ -QString class_name::author() const \ -{ \ - return basicWrapperFunctionImplementation(this, "author"); \ -} \ - \ -QString class_name::description() const \ -{ \ - return basicWrapperFunctionImplementation(this, "description"); \ -} \ - \ -MOBase::VersionInfo class_name::version() const \ -{ \ - return basicWrapperFunctionImplementation(this, "version"); \ -} \ - \ -bool class_name::isActive() const \ -{ \ - return basicWrapperFunctionImplementation(this, "isActive"); \ -} \ - \ -QList class_name::settings() const \ -{ \ - return basicWrapperFunctionImplementation>(this, "settings"); \ -} - -/// end COMMON_I_PLUGIN_WRAPPER_DEFINITIONS -///////////////////////////// -/// IPlugin Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginWrapper) -/// end IPlugin Wrapper -///////////////////////////////////// -/// IPluginDiagnose Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginDiagnoseWrapper) - -std::vector IPluginDiagnoseWrapper::activeProblems() const -{ - return basicWrapperFunctionImplementation>(this, "activeProblems"); -} - -QString IPluginDiagnoseWrapper::shortDescription(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "shortDescription", key); -} - -QString IPluginDiagnoseWrapper::fullDescription(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "fullDescription", key); -} - -bool IPluginDiagnoseWrapper::hasGuidedFix(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "hasGuidedFix", key); -} - -void IPluginDiagnoseWrapper::startGuidedFix(unsigned int key) const -{ - basicWrapperFunctionImplementation(this, "startGuidedFix", key); -} - -void IPluginDiagnoseWrapper::invalidate() -{ - IPluginDiagnose::invalidate(); -} -/// end IPluginDiagnose Wrapper -///////////////////////////////////// -/// IPluginFileMapper Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginFileMapperWrapper) - -MappingType IPluginFileMapperWrapper::mappings() const -{ - return basicWrapperFunctionImplementation(this, "mappings"); -} -/// end IPluginFileMapper Wrapper -///////////////////////////////////// -/// IPluginGame Wrapper - - -QString IPluginGameWrapper::gameName() const -{ - return basicWrapperFunctionImplementation(this, "gameName"); -} - -void IPluginGameWrapper::initializeProfile(const QDir & directory, ProfileSettings settings) const -{ - basicWrapperFunctionImplementation(this, "initializeProfile", directory, settings); -} - -QString IPluginGameWrapper::savegameExtension() const -{ - return basicWrapperFunctionImplementation(this, "savegameExtension"); -} - -QString IPluginGameWrapper::savegameSEExtension() const -{ - return basicWrapperFunctionImplementation(this, "savegameSEExtension"); -} - -bool IPluginGameWrapper::isInstalled() const -{ - return basicWrapperFunctionImplementation(this, "isInstalled"); -} - -QIcon IPluginGameWrapper::gameIcon() const -{ - return basicWrapperFunctionImplementation(this, "gameIcon"); -} - -QDir IPluginGameWrapper::gameDirectory() const -{ - return basicWrapperFunctionImplementation(this, "gameDirectory"); -} - -QDir IPluginGameWrapper::dataDirectory() const -{ - return basicWrapperFunctionImplementation(this, "dataDirectory"); -} - -void IPluginGameWrapper::setGamePath(const QString & path) -{ - basicWrapperFunctionImplementation(this, "setGamePath", path); -} - -QDir IPluginGameWrapper::documentsDirectory() const -{ - return basicWrapperFunctionImplementation(this, "documentsDirectory"); -} - -QDir IPluginGameWrapper::savesDirectory() const -{ - return basicWrapperFunctionImplementation(this, "savesDirectory"); -} - -QList IPluginGameWrapper::executables() const -{ - return basicWrapperFunctionImplementation>(this, "executables"); -} - -QList IPluginGameWrapper::executableForcedLoads() const -{ - return basicWrapperFunctionImplementation>(this, "executableForcedLoads"); -} - -QString IPluginGameWrapper::steamAPPId() const -{ - return basicWrapperFunctionImplementation(this, "steamAPPId"); -} - -QStringList IPluginGameWrapper::primaryPlugins() const -{ - return basicWrapperFunctionImplementation(this, "primaryPlugins"); -} - -QStringList IPluginGameWrapper::gameVariants() const -{ - return basicWrapperFunctionImplementation(this, "gameVariants"); -} - -void IPluginGameWrapper::setGameVariant(const QString & variant) -{ - basicWrapperFunctionImplementation(this, "setGameVariant", variant); -} - -QString IPluginGameWrapper::binaryName() const -{ - return basicWrapperFunctionImplementation(this, "binaryName"); -} - -QString IPluginGameWrapper::gameShortName() const -{ - return basicWrapperFunctionImplementation(this, "gameShortName"); -} - -QStringList IPluginGameWrapper::primarySources() const -{ - return basicWrapperFunctionImplementation(this, "primarySources"); -} - -QStringList IPluginGameWrapper::validShortNames() const -{ - return basicWrapperFunctionImplementation(this, "validShortNames"); -} - -QString IPluginGameWrapper::gameNexusName() const -{ - return basicWrapperFunctionImplementation(this, "gameNexusName"); -} - -QStringList IPluginGameWrapper::iniFiles() const -{ - return basicWrapperFunctionImplementation(this, "iniFiles"); -} - -QStringList IPluginGameWrapper::DLCPlugins() const -{ - return basicWrapperFunctionImplementation(this, "DLCPlugins"); -} - -QStringList IPluginGameWrapper::CCPlugins() const -{ - return basicWrapperFunctionImplementation(this, "CCPlugins"); -} - -IPluginGame::LoadOrderMechanism IPluginGameWrapper::loadOrderMechanism() const -{ - return basicWrapperFunctionImplementation(this, "loadOrderMechanism"); -} - -IPluginGame::SortMechanism IPluginGameWrapper::sortMechanism() const -{ - return basicWrapperFunctionImplementation(this, "sortMechanism"); -} - -int IPluginGameWrapper::nexusModOrganizerID() const -{ - return basicWrapperFunctionImplementation(this, "nexusModOrganizerID"); -} - -int IPluginGameWrapper::nexusGameID() const -{ - return basicWrapperFunctionImplementation(this, "nexusGameID"); -} - -bool IPluginGameWrapper::looksValid(QDir const & dir) const -{ - return basicWrapperFunctionImplementation(this, "looksValid", dir); -} - -QString IPluginGameWrapper::gameVersion() const -{ - return basicWrapperFunctionImplementation(this, "gameVersion"); -} - -QString IPluginGameWrapper::getLauncherName() const -{ - return basicWrapperFunctionImplementation(this, "getLauncherName"); -} - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginGameWrapper) - -std::map IPluginGameWrapper::featureList() const -{ - return basicWrapperFunctionImplementation>(this, "_featureList"); -} -/// end IPluginGame Wrapper -///////////////////////////////////// -/// IPluginInstallerCustom Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginInstallerCustomWrapper) - -unsigned int IPluginInstallerCustomWrapper::priority() const -{ - return basicWrapperFunctionImplementation(this, "priority"); -} - -bool IPluginInstallerCustomWrapper::isManualInstaller() const -{ - return basicWrapperFunctionImplementation(this, "isManualInstaller"); -} - -bool IPluginInstallerCustomWrapper::isArchiveSupported(const DirectoryTree &archiveTree) const -{ - //return basicWrapperFunctionImplementation(this, "isArchiveSupported", archiveTree); - // This was a stub implementation when I got here, and a real one won't compile. - return false; -} - -bool IPluginInstallerCustomWrapper::isArchiveSupported(const QString &archiveName) const -{ - return basicWrapperFunctionImplementation(this, "isArchiveSupported", archiveName); -} - -std::set IPluginInstallerCustomWrapper::supportedExtensions() const -{ - return basicWrapperFunctionImplementation>(this, "supportedExtensions"); -} - -IPluginInstaller::EInstallResult IPluginInstallerCustomWrapper::install(GuessedValue &modName, QString gameName, const QString &archiveName, - const QString &version, int modID) -{ - return basicWrapperFunctionImplementation(this, "install", modName, gameName, archiveName, version, modID); -} - - -void IPluginInstallerCustomWrapper::setParentWidget(QWidget *parent) -{ - basicWrapperFunctionImplementation(this, "setParentWidget", parent); -} -/// end IPluginInstallerCustom Wrapper -///////////////////////////// -/// IPluginModPage Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginModPageWrapper) - -QString IPluginModPageWrapper::displayName() const -{ - return basicWrapperFunctionImplementation(this, "displayName"); -} - -QIcon IPluginModPageWrapper::icon() const -{ - return basicWrapperFunctionImplementation(this, "icon"); -} - -QUrl IPluginModPageWrapper::pageURL() const -{ - return basicWrapperFunctionImplementation(this, "pageURL"); -} - -bool IPluginModPageWrapper::useIntegratedBrowser() const -{ - return basicWrapperFunctionImplementation(this, "useIntegratedBrowser"); -} - -bool IPluginModPageWrapper::handlesDownload(const QUrl & pageURL, const QUrl & downloadURL, MOBase::ModRepositoryFileInfo & fileInfo) const -{ - return basicWrapperFunctionImplementation(this, "handlesDownload", pageURL, downloadURL, fileInfo); -} - -void IPluginModPageWrapper::setParentWidget(QWidget * widget) -{ - basicWrapperFunctionImplementation(this, "setParentWidget", widget); -} -/// end IPluginModPage Wrapper -///////////////////////////// -/// IPluginPreview Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginPreviewWrapper) - -std::set IPluginPreviewWrapper::supportedExtensions() const -{ - return basicWrapperFunctionImplementation>(this, "supportedExtensions"); -} - -QWidget *IPluginPreviewWrapper::genFilePreview(const QString &fileName, const QSize &maxSize) const -{ - // This is complicated, so we can't use the basic implementation - try { - GILock lock; - boost::python::override implementation = this->get_override("genFilePreview"); - if (!implementation) - throw MissingImplementation(this->className, "genFilePreview"); - boost::python::object pyVersion = implementation(fileName, maxSize); - // We need responsibility for deleting the QWidget to be transferred to C++ - sipAPIAccess::sipAPI()->api_transfer_to(pyVersion.ptr(), Py_None); - return boost::python::extract(pyVersion)(); - } PYCATCH; -} -/// end IPluginPreview Wrapper -///////////////////////////// -/// IPluginTool Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginToolWrapper) - -QString IPluginToolWrapper::displayName() const -{ - return basicWrapperFunctionImplementation(this, "displayName"); -} - -QString IPluginToolWrapper::tooltip() const -{ - return basicWrapperFunctionImplementation(this, "tooltip"); -} - -QIcon IPluginToolWrapper::icon() const -{ - return basicWrapperFunctionImplementation(this, "icon"); -} - -void IPluginToolWrapper::setParentWidget(QWidget *parent) -{ - basicWrapperFunctionImplementation(this, "setParentWidget", parent); -} - -void IPluginToolWrapper::display() const -{ - basicWrapperFunctionImplementation(this, "display"); -} - -/// end IPluginTool Wrapper diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h deleted file mode 100644 index 712707a5..00000000 --- a/src/runner/proxypluginwrappers.h +++ /dev/null @@ -1,213 +0,0 @@ -#ifndef PROXYPLUGINWRAPPERS_H -#define PROXYPLUGINWRAPPERS_H - - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef Q_MOC_RUN -#include -#endif - - -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS public: \ -virtual bool init(MOBase::IOrganizer *moInfo) override; \ -virtual QString name() const override; \ -virtual QString author() const override; \ -virtual QString description() const override; \ -virtual MOBase::VersionInfo version() const override; \ -virtual bool isActive() const override; \ -virtual QList settings() const override; - - -// Even though the base interface is not a QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginWrapper : public QObject, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginWrapper"; - using boost::python::wrapper::get_override; -}; - - -// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginDiagnoseWrapper : public QObject, public MOBase::IPluginDiagnose, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) - -public: - static constexpr const char* className = "IPluginDiagnoseWrapper"; - using boost::python::wrapper::get_override; - - 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; - // Other functions exist, but shouldn't need wrapping as a default implementation exists - // This was protected, but Python doesn't have that, so it needs making public - virtual void invalidate(); - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -}; - - -// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginFileMapperWrapper : public QObject, public MOBase::IPluginFileMapper, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) - -public: - static constexpr const char* className = "IPluginFileMapperWrapper"; - using boost::python::wrapper::get_override; - - virtual MappingType mappings() const override; - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -}; - - -class IPluginGameWrapper : public MOBase::IPluginGame, public boost::python::wrapper { - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame) - -public: - static constexpr const char* className = "IPluginGameWrapper"; - using boost::python::wrapper::get_override; - - virtual QString gameName() const override; - virtual void initializeProfile(const QDir &directory, ProfileSettings settings) const override; - virtual QString savegameExtension() const override; - virtual QString savegameSEExtension() const override; - virtual bool isInstalled() const override; - virtual QIcon gameIcon() const override; - virtual QDir gameDirectory() const override; - virtual QDir dataDirectory() const override; - virtual void setGamePath(const QString &path) override; - virtual QDir documentsDirectory() const override; - virtual QDir savesDirectory() const override; - virtual QList executables() const override; - virtual QList executableForcedLoads() const override; - virtual QString steamAPPId() const override; - virtual QStringList primaryPlugins() const override; - virtual QStringList gameVariants() const override; - virtual void setGameVariant(const QString &variant) override; - virtual QString binaryName() const override; - virtual QString gameShortName() const override; - virtual QStringList primarySources() const override; - virtual QStringList validShortNames() const override; - virtual QString gameNexusName() const override; - virtual QStringList iniFiles() const override; - virtual QStringList DLCPlugins() const override; - virtual QStringList CCPlugins() const override; - virtual LoadOrderMechanism loadOrderMechanism() const override; - virtual SortMechanism sortMechanism() const override; - virtual int nexusModOrganizerID() const override; - virtual int nexusGameID() const override; - virtual bool looksValid(QDir const &dir) const override; - virtual QString gameVersion() const override; - virtual QString getLauncherName() const override; - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS - -protected: - // Apparently, Python developers interpret an underscore in a function name as it being protected - virtual std::map featureList() const override; - - // Thankfully, the default implementation of the templated 'T *feature()' function should allow us to get away without overriding it. -}; - - -class IPluginInstallerCustomWrapper : public MOBase::IPluginInstallerCustom, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginInstallerCustomWrapper"; - using boost::python::wrapper::get_override; - - virtual unsigned int priority() const; - virtual bool isManualInstaller() const; - virtual bool isArchiveSupported(const MOBase::DirectoryTree &tree) const; - virtual bool isArchiveSupported(const QString &archiveName) const; - virtual std::set supportedExtensions() const; - virtual EInstallResult install(MOBase::GuessedValue &modName, QString gameName, const QString &archiveName, - const QString &version, int modID); - virtual void setParentWidget(QWidget *parent); - -}; - - -class IPluginModPageWrapper : public MOBase::IPluginModPage, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginModPageWrapper"; - using boost::python::wrapper::get_override; - - virtual QString displayName() const override; - virtual QIcon icon() const override; - virtual QUrl pageURL() const override; - virtual bool useIntegratedBrowser() const override; - virtual bool handlesDownload(const QUrl &pageURL, const QUrl &downloadURL, MOBase::ModRepositoryFileInfo &fileInfo) const override; - virtual void setParentWidget(QWidget *widget) override; -}; - - -class IPluginPreviewWrapper : public MOBase::IPluginPreview, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginPreviewWrapper"; - using boost::python::wrapper::get_override; - - virtual std::set supportedExtensions() const override; - virtual QWidget *genFilePreview(const QString &fileName, const QSize &maxSize) const override; -}; - - -class IPluginToolWrapper: public MOBase::IPluginTool, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginToolWrapper"; - using boost::python::wrapper::get_override; - - virtual QString displayName() const; - virtual QString tooltip() const; - virtual QIcon icon() const; - virtual void setParentWidget(QWidget *parent); - -public Q_SLOTS: - virtual void display() const; -}; - - - - -#endif // PROXYPLUGINWRAPPERS_H diff --git a/src/runner/pythonRunner.pro b/src/runner/pythonRunner.pro deleted file mode 100644 index 55c509b6..00000000 --- a/src/runner/pythonRunner.pro +++ /dev/null @@ -1,66 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2013-09-01T15:55:01 -# -#------------------------------------------------- - -TARGET = pythonRunner -TEMPLATE = lib - -CONFIG += dll -CONFIG += warn_on -QT += widgets - -DEFINES += PYTHONRUNNER_LIBRARY - -# suppress a few warnings caused by boost vs vc++ paranoia -DEFINES += _SCL_SECURE_NO_WARNINGS HAVE_ROUND NOMINMAX - - -!include(../LocalPaths.pri) { - message("paths to required libraries need to be set up in LocalPaths.pri") -} - -SOURCES += pythonrunner.cpp \ - gilock.cpp \ - error.cpp \ - pythonpluginwrapper.cpp \ - proxypluginwrappers.cpp - -HEADERS += pythonrunner.h \ - gilock.h \ - error.h \ - uibasewrappers.h \ - pythonpluginwrapper.h \ - proxypluginwrappers.h - -CONFIG(debug, debug|release) { - LIBS += -L$$OUT_PWD/../uibase/debug -} else { - LIBS += -L$$OUT_PWD/../uibase/release - msvc:QMAKE_CXXFLAGS += /Zi - msvc:QMAKE_LFLAGS += /DEBUG -} - -INCLUDEPATH += "$${BOOSTPATH}" "$${PYTHONPATH}/include" "$${PYTHONPATH}/Lib/site-packages/PyQt5/include" ../uibase -LIBS += -L"$${PYTHONPATH}/libs" -L"$${BOOSTPATH}/stage/lib" -LIBS += -lpython27 -LIBS += -luibase - -CONFIG(debug, debug|release) { - SRCDIR = $$OUT_PWD/debug - DSTDIR = $$PWD/../../outputd -} else { - SRCDIR = $$OUT_PWD/release - DSTDIR = $$PWD/../../output -} - -SRCDIR ~= s,/,$$QMAKE_DIR_SEP,g -DSTDIR ~= s,/,$$QMAKE_DIR_SEP,g - -QMAKE_POST_LINK += xcopy /y /s /i $$quote($$SRCDIR\\$${TARGET}*.dll) $$quote($$DSTDIR)\\plugins\\data $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /I $$quote($$SRCDIR\\$${TARGET}*.pdb) $$quote($$DSTDIR)\\plugins $$escape_expand(\\n) - -OTHER_FILES += \ - SConscript\ - CMakeLists.txt diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 3762f965..3538bb74 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -1,1464 +1,347 @@ #include "pythonrunner.h" -#pragma warning( disable : 4100 ) -#pragma warning( disable : 4996 ) - -#include -#include -#include -#include -#include "uibasewrappers.h" -#include "proxypluginwrappers.h" -#include "gamefeatureswrappers.h" -#include "sipApiAccess.h" +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) #include -#include -#include -#include -#include -#include - -// sip and qt slots seems to conflict -#include - -#ifndef Q_MOC_RUN -#include -#endif - - -MOBase::IOrganizer *s_Organizer = nullptr; - +#include -class PythonRunner : public IPythonRunner -{ - -public: - PythonRunner(const MOBase::IOrganizer *moInfo); - bool initPython(const QString &pythonDir); - QList instantiate(const QString &pluginName); - bool isPythonInstalled() const; - bool isPythonVersionSupported() const; - -private: - - void initPath(); - -private: - std::map m_PythonObjects; - const MOBase::IOrganizer *m_MOInfo; - wchar_t *m_PythonHome; -}; +#include +#include +#include +#include "pybind11_qt/pybind11_qt.h" +#include +#include +#include -IPythonRunner *CreatePythonRunner(MOBase::IOrganizer *moInfo, const QString &pythonDir) -{ - s_Organizer = moInfo; - PythonRunner *result = new PythonRunner(moInfo); - if (result->initPython(pythonDir)) { - return result; - } else { - delete result; - return nullptr; - } -} +#include +#include +#include "error.h" +#include "pythonutils.h" using namespace MOBase; +namespace py = pybind11; -namespace bpy = boost::python; - -struct QString_to_python_str -{ - static PyObject *convert(const QString &str) { - // It's safer to explicitly convert to unicode as if we don't, this can return either str or unicode without it being easy to know which to expect - bpy::object pyStr = bpy::object(qUtf8Printable(str)); - if (SIPBytes_Check(pyStr.ptr())) - pyStr = pyStr.attr("decode")("utf-8"); - return bpy::incref(pyStr.ptr()); - } -}; - -template -struct QFlags_to_int -{ - static PyObject *convert(const QFlags &flags) { - return bpy::incref(bpy::object(static_cast(flags)).ptr()); - } -}; - - -struct QString_from_python_str -{ - QString_from_python_str() { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id()); - } - - static void *convertible(PyObject *objPtr) { - return SIPBytes_Check(objPtr) || PyUnicode_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - // Ensure the string uses 8-bit characters - PyObject *strPtr = PyUnicode_Check(objPtr) ? PyUnicode_AsUTF8String(objPtr) : objPtr; - - // Extract the character data from the python string - const char* value = SIPBytes_AsString(strPtr); - assert(value != nullptr); - - // allocate storage - void *storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - // construct QString in the allocated memory - new (storage) QString(value); - - data->convertible = storage; - - // Deallocate local copy if one was made - if (strPtr != objPtr) - Py_DecRef(strPtr); - } -}; - - -struct HANDLE_converters -{ - struct HANDLE_to_python - { - static PyObject *convert(HANDLE handle) { - size_t size_t_version = (size_t)handle; - return bpy::incref(bpy::object(size_t_version).ptr()); - } - }; - - // bpy isn't keen on actually using this. - // maybe it's detecting that the function receives a pointer, and assumes that it needs to convert to the pointer's target. - // the issue can be worked around by wrapping the function to take a size_t and converting it there - struct HANDLE_from_python - { - HANDLE_from_python() { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id()); - } - - static void *convertible(PyObject *objPtr) { - return PyLong_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - void *storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - HANDLE *result = new (storage) HANDLE; - *result = (HANDLE)bpy::extract(objPtr)(); - } - }; - - HANDLE_converters() - { - HANDLE_from_python(); - bpy::to_python_converter(); - } -}; - - -template -struct GuessedValue_converters -{ - struct GuessedValue_to_python - { - static PyObject *convert(const GuessedValue &var) { - bpy::list result; - const std::set &values = var.variants(); - for (auto iter = values.begin(); iter != values.end(); ++iter) { - result.append(bpy::make_tuple(*iter, GUESS_GOOD)); - } - return bpy::incref(result.ptr()); - } - }; - - struct GuessedValue_from_python - { - GuessedValue_from_python() { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id >()); - } - - static void *convertible(PyObject *objPtr) { - if PyList_Check(objPtr) { - return objPtr; - } else { - return nullptr; - } - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - void *storage = ((bpy::converter::rvalue_from_python_storage >*)data)->storage.bytes; - GuessedValue *result = new (storage) GuessedValue(); - - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - bpy::tuple cell = bpy::extract(source[i]); - result->update(bpy::extract(cell[0]), bpy::extract(cell[1])); - } - - data->convertible = storage; - } - }; - - GuessedValue_converters() - { - GuessedValue_from_python(); - bpy::to_python_converter, GuessedValue_to_python>(); - } -}; - - -template -struct QMap_converters -{ - struct QMap_to_python - { - static PyObject *convert(const QMap &map) { - bpy::dict result; - QMapIterator iter(map); - while (iter.hasNext()) { - iter.next(); - result[bpy::object(iter.key())] = bpy::object(iter.value()); - } - return bpy::incref(result.ptr()); - } - }; +namespace mo2::python { - struct QMap_from_python - { - QMap_from_python() { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id>()); - } + /** + * + */ + class PythonRunner : public IPythonRunner { - static void *convertible(PyObject *objPtr) { - return PyDict_Check(objPtr) ? objPtr : nullptr; - } + public: + PythonRunner() = default; + ~PythonRunner() = default; - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - void *storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - QMap *result = new (storage) QMap(); - bpy::dict source(bpy::handle<>(bpy::borrowed(objPtr))); - bpy::list keys = source.keys(); - int len = bpy::len(keys); - for (int i = 0; i < len; ++i) { - bpy::object pyKey = keys[i]; - (*result)[bpy::extract(pyKey)] = bpy::extract(source[pyKey]); - } - - data->convertible = storage; - } - }; - - QMap_converters() - { - QMap_from_python(); - bpy::to_python_converter, QMap_to_python >(); - } -}; - - -struct QVariant_to_python_obj -{ - static PyObject *convert(const QVariant &var) { - switch (var.type()) { - case QVariant::Invalid: return bpy::incref(Py_None); - case QVariant::Int: return SIPLong_FromLong(var.toInt()); - case QVariant::UInt: return PyLong_FromUnsignedLong(var.toUInt()); - case QVariant::Bool: return PyBool_FromLong(var.toBool()); - case QVariant::String: return bpy::incref(bpy::object(var.toString()).ptr()); - case QVariant::List: { - return bpy::incref(bpy::object(var.toList()).ptr()); - } break; - case QVariant::Map: { - return bpy::incref(bpy::object(var.toMap()).ptr()); - } break; - default: { - PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); - throw bpy::error_already_set(); - } break; - } - } -}; + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; + bool initialize(std::vector const& pythonPaths) override; + void addDllSearchPath(std::filesystem::path const& dllPath) override; + bool isInitialized() const override; -struct QVariant_from_python_obj -{ - QVariant_from_python_obj() { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id()); - } - - static void *convertible(PyObject *objPtr) { - if (!SIPBytes_Check(objPtr) && !PyUnicode_Check(objPtr) && !PyLong_Check(objPtr) && - !PyBool_Check(objPtr) && !PyList_Check(objPtr) && !PyDict_Check(objPtr) && - objPtr != Py_None) { - return nullptr; - } - return objPtr; - } - - template - static void constructVariant(const T &value, bpy::converter::rvalue_from_python_stage1_data *data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - new (storage) QVariant(value); - - data->convertible = storage; - } - - static void constructVariant(bpy::converter::rvalue_from_python_stage1_data *data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - new (storage) QVariant(); - - data->convertible = storage; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - // PyBools will also return true for SIPLong_Check but not the other way around, so the order - // here is relevant - if (PyList_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } else if (objPtr == Py_None) { - constructVariant(data); - } else if (PyDict_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } else if (SIPBytes_Check(objPtr) || PyUnicode_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } else if (PyBool_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } else if (SIPLong_Check(objPtr)) { - //QVariant doesn't have long. It has int or long long. Given that on m/s, - //long is 32 bits for 32- and 64- bit code... - constructVariant(bpy::extract(objPtr)(), data); - } else { - PyErr_SetString(PyExc_TypeError, "type unsupported"); - throw bpy::error_already_set(); - } - } -}; - - -template -struct QList_to_python_list -{ - static PyObject *convert(const QList &list) - { - bpy::list pyList; - - try { - for (const T &item : list) { - pyList.append(item); - } - } catch (const bpy::error_already_set&) { - reportPythonError(); - } - PyObject *res = bpy::incref(pyList.ptr()); - return res; - } -}; - - -template -struct QList_from_python_obj -{ - QList_from_python_obj() { - bpy::converter::registry::push_back( - &convertible, - &construct, - bpy::type_id >()); - } - - static void* convertible(PyObject *objPtr) { - if (PyList_Check(objPtr)) return objPtr; - return nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - void *storage = ((bpy::converter::rvalue_from_python_storage >*)data)->storage.bytes; - QList *result = new (storage) QList(); - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - result->append(bpy::extract(source[i])); - } - - data->convertible = storage; - } -}; + 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; + }; -template -struct std_vector_to_python_list -{ - static PyObject *convert(const std::vector &vector) - { - bpy::list pyList; - - try { - for (const T &item : vector) - pyList.append(item); - } - catch (const bpy::error_already_set&) { - reportPythonError(); - } - - return bpy::incref(pyList.ptr()); - } -}; - - -template -struct std_vector_from_python_obj -{ - std_vector_from_python_obj() { - bpy::converter::registry::push_back( - &convertible, - &construct, - bpy::type_id >()); - } - - static void* convertible(PyObject *objPtr) { - if (PyList_Check(objPtr)) return objPtr; - return nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - void *storage = ((bpy::converter::rvalue_from_python_storage >*)data)->storage.bytes; - std::vector *result = new (storage) std::vector(); - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - result->push_back(bpy::extract(source[i])); - } - - data->convertible = storage; - } -}; - - -template -struct QFlags_from_python_obj -{ - QFlags_from_python_obj() { - bpy::converter::registry::push_back( - &convertible, - &construct, - bpy::type_id>()); - } - - static void* convertible(PyObject *objPtr) { - return SIPLong_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - int intVersion = (int)SIPLong_AsLong(objPtr); - T tVersion = (T)intVersion; - void *storage = ((bpy::converter::rvalue_from_python_storage> *)data)->storage.bytes; - new (storage) QFlags(tVersion); - - data->convertible = storage; - } -}; - - -template -struct stdset_from_python_list -{ - stdset_from_python_list() { - bpy::converter::registry::push_back( - &convertible, - &construct, - bpy::type_id >()); - } - - static void* convertible(PyObject *objPtr) { - if (PyList_Check(objPtr)) return objPtr; - return nullptr; - } - - static void construct(PyObject *objPtr, bpy::converter::rvalue_from_python_stage1_data *data) { - void *storage = ((bpy::converter::rvalue_from_python_storage >*)data)->storage.bytes; - std::set *result = new (storage) std::set(); - - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - result->insert(bpy::extract(source[i])); - } - - data->convertible = storage; - } -}; - - -struct IModRepositoryBridge_to_python -{ - static PyObject *convert(IModRepositoryBridge *bridge) - { - ModRepositoryBridgeWrapper wrapper(bridge); - - return bpy::incref(bpy::object(wrapper).ptr()); - } -}; - - - -template struct MetaData; - -template <> struct MetaData { static const char *className() { return "QObject"; } }; -template <> struct MetaData { static const char *className() { return "QObject"; } }; -template <> struct MetaData { static const char *className() { return "QObject"; } }; -template <> struct MetaData { static const char *className() { return "QWidget"; } }; -template <> struct MetaData { static const char *className() { return "QDateTime"; } }; -template <> struct MetaData { static const char *className() { return "QDir"; } }; -template <> struct MetaData { static const char *className() { return "QFileInfo"; } }; -template <> struct MetaData { static const char *className() { return "QIcon"; } }; -template <> struct MetaData { static const char *className() { return "QSize"; } }; -template <> struct MetaData { static const char *className() { return "QStringList"; } }; -template <> struct MetaData { static const char *className() { return "QUrl"; } }; -template <> struct MetaData { static const char *className() { return "QVariant"; } }; - - -template -PyObject *toPyQt(T *objPtr) -{ - if (objPtr == nullptr) { - qDebug("no input object"); - return bpy::incref(Py_None); - } - const sipTypeDef *type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - - if (type == nullptr) { - qDebug("failed to determine type: %s", MetaData::className()); - return bpy::incref(Py_None); - } - - PyObject *sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(objPtr, type, 0); - if (sipObj == nullptr) { - qDebug("failed to convert"); - return bpy::incref(Py_None); - } - return bpy::incref(sipObj); -} - - -template -struct QClass_converters -{ - struct QClass_to_PyQt - { - template - static typename std::enable_if_t, T*> getSafeCopy(T *qClass) + std::unique_ptr createPythonRunner() { - return new T(*qClass); + return std::make_unique(); } - template - static typename std::enable_if_t, T*> getSafeCopy(T *qClass) + bool PythonRunner::initialize(std::vector const& pythonPaths) { - return qClass; - } - - static PyObject *convert(const T &object) { - const sipTypeDef *type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject *sipObj = sipAPIAccess::sipAPI()->api_convert_from_type((void*)getSafeCopy((T*)&object), type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - if (std::is_copy_constructible_v) - // Ensure Python deletes the C++ component - sipAPIAccess::sipAPI()->api_transfer_back(sipObj); - - return bpy::incref(sipObj); - } - - static PyObject *convert(T *object) { - if (object == nullptr) { - return bpy::incref(Py_None); - } - - const sipTypeDef *type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject *sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(getSafeCopy(object), type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - if (std::is_copy_constructible_v) - // Ensure Python deletes the C++ component - sipAPIAccess::sipAPI()->api_transfer_back(sipObj); - - return bpy::incref(sipObj); - } - - static PyObject *convert(const T *object) { - return convert((T*)object); - } - }; - - static void *QClass_from_PyQt(PyObject *objPtr) - { - // This would transfer responsibility for deconstructing the object to C++, but Boost assumes l-value converters (such as this) don't do that - // Instead, this should be called within the wrappers for functions which return deletable pointers. - //sipAPI()->api_transfer_to(objPtr, Py_None); - if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_simplewrapper_type)) { - sipSimpleWrapper *wrapper; - wrapper = reinterpret_cast(objPtr); - return wrapper->data; - } else if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_wrapper_type)) { - sipWrapper *wrapper; - wrapper = reinterpret_cast(objPtr); - return wrapper->super.data; - } else { - if (std::is_same_v) - { - // QStringLists aren't wrapped by PyQt - regular Python string/unicode lists are used instead - bpy::extract> extractor(objPtr); - if (extractor.check()) - return new QStringList(extractor()); - } - PyErr_SetString(PyExc_TypeError, "type not wrapped"); - bpy::throw_error_already_set(); - } - return new void*; - } - - QClass_converters() - { - bpy::converter::registry::insert(&QClass_from_PyQt, bpy::type_id()); - bpy::to_python_converter(); - bpy::to_python_converter(); - bpy::to_python_converter(); - } -}; - - -template -struct QInterface_converters -{ - struct QInterface_to_PyQt - { - static PyObject *convert(const T &object) { - const sipTypeDef *type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject *sipObj = sipAPIAccess::sipAPI()->api_convert_from_type((void*)(&object), type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - return bpy::incref(sipObj); - } - - static PyObject *convert(T *object) { - if (object == nullptr) { - return bpy::incref(Py_None); - } - - const sipTypeDef *type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject *sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(object, type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - return bpy::incref(sipObj); - } - - static PyObject *convert(const T *object) { - return convert((T*)object); - } - }; - - static void *QInterface_from_PyQt(PyObject *objPtr) - { - if (!PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_wrapper_type)) { - bpy::throw_error_already_set(); - } - - // This would transfer responsibility for deconstructing the object to C++, but Boost assumes l-value converters (such as this) don't do that - // Instead, this should be called within the wrappers for functions which return deletable pointers. - //sipAPI()->api_transfer_to(objPtr, Py_None); - - sipSimpleWrapper *wrapper = reinterpret_cast(objPtr); - return wrapper->data; - } - - QInterface_converters() - { - bpy::converter::registry::insert(&QInterface_from_PyQt, bpy::type_id()); - bpy::to_python_converter(); - bpy::to_python_converter(); - } -}; - - -int getArgCount(PyObject *object) { - int result = 0; - PyObject *funcCode = PyObject_GetAttrString(object, "__code__"); - if (funcCode) { - PyObject *argCount = PyObject_GetAttrString(funcCode, "co_argcount"); - if(argCount) { - result = SIPLong_AsLong(argCount); - Py_DECREF(argCount); - } - Py_DECREF(funcCode); - } - return result; -} - -template -struct Functor0_converter -{ - - struct FunctorWrapper - { - FunctorWrapper(boost::python::object callable) : m_Callable(callable) { - } - - RET operator()() { - GILock lock; - return (RET) m_Callable(); - } - - boost::python::object m_Callable; - }; - - Functor0_converter() - { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id>()); - } - - static void *convertible(PyObject *object) - { - if (!PyCallable_Check(object) - || (getArgCount(object) != 0)) { - return nullptr; - } - return object; - } - - static void construct(PyObject *object, bpy::converter::rvalue_from_python_stage1_data *data) - { - bpy::object callable(bpy::handle<>(bpy::borrowed(object))); - void *storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - new (storage) std::function(FunctorWrapper(callable)); - data->convertible = storage; - } -}; - - -template -struct Functor1_converter -{ - - struct FunctorWrapper - { - FunctorWrapper(boost::python::object callable) : m_Callable(callable) { - } - - RET operator()(const PAR1 ¶m1) { - GILock lock; - return (RET) m_Callable(param1); - } - - boost::python::object m_Callable; - }; - - Functor1_converter() - { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id>()); - } - - static void *convertible(PyObject *object) - { - if (!PyCallable_Check(object) - || (getArgCount(object) != 1)) { - return nullptr; - } - return object; - } - - static void construct(PyObject *object, bpy::converter::rvalue_from_python_stage1_data *data) - { - bpy::object callable(bpy::handle<>(bpy::borrowed(object))); - void *storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - new (storage) std::function(FunctorWrapper(callable)); - data->convertible = storage; - } -}; - - -template -struct Functor2_converter -{ - - struct FunctorWrapper - { - FunctorWrapper(boost::python::object callable) : m_Callable(callable) { - } - - RET operator()(const PAR1 ¶m1, const PAR2 ¶m2) { - GILock lock; - return (RET) m_Callable(param1, param2); - } - - boost::python::object m_Callable; - }; - - Functor2_converter() - { - bpy::converter::registry::push_back(&convertible, &construct, bpy::type_id>()); - } - - static void *convertible(PyObject *object) - { - if (!PyCallable_Check(object) - || (getArgCount(object) != 2)) { - return nullptr; - } - return object; - } - - static void construct(PyObject *object, bpy::converter::rvalue_from_python_stage1_data *data) - { - bpy::object callable(bpy::handle<>(bpy::borrowed(object))); - void *storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - new (storage) std::function(FunctorWrapper(callable)); - data->convertible = storage; - } -}; - - -// We must wrap IOrganizer::waitForApplication to convert the out parameter to a return value and also because bpy doesn't like coverting to void* (HANDLE) even if a converter exists. -static PyObject *waitForApplication(const bpy::object &self, size_t handle) -{ - IOrganizer& organizer = bpy::extract(self)(); - DWORD returnCode; - bool result = organizer.waitForApplication((HANDLE)handle, &returnCode); - return bpy::incref(bpy::make_tuple(result, returnCode).ptr()); -} - - -BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(updateWithQuality, MOBase::GuessedValue::update, 2, 2) - - -BOOST_PYTHON_MODULE(mobase) -{ - PyEval_InitThreads(); - bpy::to_python_converter(); - QVariant_from_python_obj(); - - bpy::to_python_converter(); - QString_from_python_str(); - - //QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QClass_converters(); - QInterface_converters(); - - - bpy::def("toPyQt", &toPyQt); - bpy::def("toPyQt", &toPyQt); - - bpy::enum_("ReleaseType") - .value("final", MOBase::VersionInfo::RELEASE_FINAL) - .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE) - .value("beta", MOBase::VersionInfo::RELEASE_BETA) - .value("alpha", MOBase::VersionInfo::RELEASE_ALPHA) - .value("prealpha", MOBase::VersionInfo::RELEASE_PREALPHA) - ; - - bpy::enum_("VersionScheme") - .value("discover", MOBase::VersionInfo::SCHEME_DISCOVER) - .value("regular", MOBase::VersionInfo::SCHEME_REGULAR) - .value("decimalmark", MOBase::VersionInfo::SCHEME_DECIMALMARK) - .value("numbersandletters", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) - .value("date", MOBase::VersionInfo::SCHEME_DATE) - .value("literal", MOBase::VersionInfo::SCHEME_LITERAL) - ; - - bpy::class_("VersionInfo") - .def(bpy::init()) - .def(bpy::init()) - .def(bpy::init()) - .def(bpy::init()) - .def(bpy::init()) - .def(bpy::init()) - .def("clear", &VersionInfo::clear) - .def("parse", &VersionInfo::parse) - .def("canonicalString", &VersionInfo::canonicalString) - .def("displayString", &VersionInfo::displayString) - .def("isValid", &VersionInfo::isValid) - .def("scheme", &VersionInfo::scheme) - .def(bpy::self < bpy::self) - .def(bpy::self > bpy::self) - .def(bpy::self <= bpy::self) - .def(bpy::self >= bpy::self) - .def(bpy::self != bpy::self) - .def(bpy::self == bpy::self) - ; - - bpy::class_("PluginSetting", bpy::init()); - - bpy::class_("ExecutableInfo", bpy::init()) - .def("withArgument", &ExecutableInfo::withArgument, bpy::return_value_policy()) - .def("withWorkingDirectory", &ExecutableInfo::withWorkingDirectory, bpy::return_value_policy()) - .def("withSteamAppId", &ExecutableInfo::withSteamAppId, bpy::return_value_policy()) - .def("asCustom", &ExecutableInfo::asCustom, bpy::return_value_policy()) - .def("isValid", &ExecutableInfo::isValid) - .def("title", &ExecutableInfo::title) - .def("binary", &ExecutableInfo::binary) - .def("arguments", &ExecutableInfo::arguments) - .def("workingDirectory", &ExecutableInfo::workingDirectory) - .def("steamAppID", &ExecutableInfo::steamAppID) - .def("isCustom", &ExecutableInfo::isCustom) - ; - - bpy::class_("ISaveGame") - .def("getFilename", bpy::pure_virtual(&ISaveGame::getFilename)) - .def("getCreationTime", bpy::pure_virtual(&ISaveGame::getCreationTime)) - .def("getSaveGroupIdentifier", bpy::pure_virtual(&ISaveGame::getSaveGroupIdentifier)) - .def("allFiles", bpy::pure_virtual(&ISaveGame::allFiles)) - .def("hasScriptExtenderFile", bpy::pure_virtual(&ISaveGame::hasScriptExtenderFile)) - ; - - // TODO: ISaveGameInfoWidget bindings - - Functor1_converter(); - Functor1_converter(); - Functor1_converter(); - Functor2_converter(); - - bpy::class_("IOrganizer") - .def("createNexusBridge", bpy::pure_virtual(&IOrganizer::createNexusBridge), bpy::return_value_policy()) - .def("profileName", bpy::pure_virtual(&IOrganizer::profileName)) - .def("profilePath", bpy::pure_virtual(&IOrganizer::profilePath)) - .def("downloadsPath", bpy::pure_virtual(&IOrganizer::downloadsPath)) - .def("overwritePath", bpy::pure_virtual(&IOrganizer::overwritePath)) - .def("basePath", bpy::pure_virtual(&IOrganizer::basePath)) - .def("modsPath", bpy::pure_virtual(&IOrganizer::modsPath)) - .def("appVersion", bpy::pure_virtual(&IOrganizer::appVersion)) - .def("getMod", bpy::pure_virtual(&IOrganizer::getMod), bpy::return_value_policy()) - .def("createMod", bpy::pure_virtual(&IOrganizer::createMod), bpy::return_value_policy()) - .def("getGame", bpy::pure_virtual(&IOrganizer::getGame), bpy::return_value_policy()) - .def("removeMod", bpy::pure_virtual(&IOrganizer::removeMod)) - .def("modDataChanged", bpy::pure_virtual(&IOrganizer::modDataChanged)) - .def("pluginSetting", bpy::pure_virtual(&IOrganizer::pluginSetting)) - .def("setPluginSetting", bpy::pure_virtual(&IOrganizer::setPluginSetting)) - .def("persistent", bpy::pure_virtual(&IOrganizer::persistent)) - .def("setPersistent", bpy::pure_virtual(&IOrganizer::setPersistent)) - .def("pluginDataPath", bpy::pure_virtual(&IOrganizer::pluginDataPath)) - .def("installMod", bpy::pure_virtual(&IOrganizer::installMod),(bpy::arg("nameSuggestion")=""), bpy::return_value_policy()) - .def("resolvePath", bpy::pure_virtual(&IOrganizer::resolvePath)) - .def("listDirectories", bpy::pure_virtual(&IOrganizer::listDirectories)) - .def("findFiles", bpy::pure_virtual(&IOrganizer::findFiles)) - .def("getFileOrigins", bpy::pure_virtual(&IOrganizer::getFileOrigins)) - .def("findFileInfos", bpy::pure_virtual(&IOrganizer::findFileInfos)) - .def("downloadManager", bpy::pure_virtual(&IOrganizer::downloadManager), bpy::return_value_policy()) - .def("pluginList", bpy::pure_virtual(&IOrganizer::pluginList), bpy::return_value_policy()) - .def("modList", bpy::pure_virtual(&IOrganizer::modList), bpy::return_value_policy()) - .def("profile", bpy::pure_virtual(&IOrganizer::profile), bpy::return_value_policy()) - .def("startApplication", bpy::pure_virtual(&IOrganizer::startApplication), ((bpy::arg("args")=QStringList()), (bpy::arg("cwd")=""), (bpy::arg("profile")=""), (bpy::arg("forcedCustomOverwrite")=""), (bpy::arg("ignoreCustomOverwrite")=false)), bpy::return_value_policy()) - //.def("waitForApplication", bpy::pure_virtual(&IOrganizer::waitForApplication), (bpy::arg("exitCode")=nullptr), bpy::return_value_policy()) - // Use wrapped version - .def("waitForApplication", waitForApplication) - .def("onModInstalled", bpy::pure_virtual(&IOrganizer::onModInstalled)) - .def("onAboutToRun", bpy::pure_virtual(&IOrganizer::onAboutToRun)) - .def("onFinishedRun", bpy::pure_virtual(&IOrganizer::onFinishedRun)) - .def("refreshModList", bpy::pure_virtual(&IOrganizer::refreshModList), (bpy::arg("saveChanges")=true)) - .def("managedGame", bpy::pure_virtual(&IOrganizer::managedGame), bpy::return_value_policy()) - .def("modsSortedByProfilePriority", bpy::pure_virtual(&IOrganizer::modsSortedByProfilePriority)) - ; - - bpy::class_("IProfile") - .def("name", bpy::pure_virtual(&IProfile::name)) - .def("absolutePath", bpy::pure_virtual(&IProfile::absolutePath)) - .def("localSavesEnabled", bpy::pure_virtual(&IProfile::localSavesEnabled)) - .def("localSettingsEnabled", bpy::pure_virtual(&IProfile::localSettingsEnabled)) - .def("invalidationActive", bpy::pure_virtual(&IProfile::invalidationActive)) - ; - - bpy::class_("ModRepositoryBridge") - .def(bpy::init()) - .def("requestDescription", &ModRepositoryBridgeWrapper::requestDescription) - .def("requestFiles", &ModRepositoryBridgeWrapper::requestFiles) - .def("requestFileInfo", &ModRepositoryBridgeWrapper::requestFileInfo) - .def("requestToggleEndorsement", &ModRepositoryBridgeWrapper::requestToggleEndorsement) - .def("onFilesAvailable", &ModRepositoryBridgeWrapper::onFilesAvailable) - .def("onFileInfoAvailable", &ModRepositoryBridgeWrapper::onFileInfoAvailable) - .def("onDescriptionAvailable", &ModRepositoryBridgeWrapper::onDescriptionAvailable) - .def("onEndorsementToggled", &ModRepositoryBridgeWrapper::onEndorsementToggled) - .def("onRequestFailed", &ModRepositoryBridgeWrapper::onRequestFailed) - ; - - bpy::class_("IModRepositoryBridge") - .def("requestDescription", bpy::pure_virtual(&IModRepositoryBridge::requestDescription)) - .def("requestFiles", bpy::pure_virtual(&IModRepositoryBridge::requestFiles)) - .def("requestFileInfo", bpy::pure_virtual(&IModRepositoryBridge::requestFileInfo)) - .def("requestDownloadURL", bpy::pure_virtual(&IModRepositoryBridge::requestDownloadURL)) - .def("requestToggleEndorsement", bpy::pure_virtual(&IModRepositoryBridge::requestToggleEndorsement)) - ; - - bpy::class_("ModRepositoryFileInfo") - .def(bpy::init()) - .def(bpy::init>()) - .def("toString", &ModRepositoryFileInfo::toString) - .def("createFromJson", &ModRepositoryFileInfo::createFromJson).staticmethod("createFromJson") - .def_readwrite("name", &ModRepositoryFileInfo::name) - .def_readwrite("uri", &ModRepositoryFileInfo::uri) - .def_readwrite("description", &ModRepositoryFileInfo::description) - .def_readwrite("version", &ModRepositoryFileInfo::version) - .def_readwrite("newestVersion", &ModRepositoryFileInfo::newestVersion) - .def_readwrite("categoryID", &ModRepositoryFileInfo::categoryID) - .def_readwrite("modName", &ModRepositoryFileInfo::modName) - .def_readwrite("gameName", &ModRepositoryFileInfo::gameName) - .def_readwrite("modID", &ModRepositoryFileInfo::modID) - .def_readwrite("fileID", &ModRepositoryFileInfo::fileID) - .def_readwrite("fileSize", &ModRepositoryFileInfo::fileSize) - .def_readwrite("fileName", &ModRepositoryFileInfo::fileName) - .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory) - .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime) - .def_readwrite("repository", &ModRepositoryFileInfo::repository) - .def_readwrite("userData", &ModRepositoryFileInfo::userData) - ; - - bpy::class_("IDownloadManager") - .def("startDownloadURLs", bpy::pure_virtual(&IDownloadManager::startDownloadURLs)) - .def("startDownloadNexusFile", bpy::pure_virtual(&IDownloadManager::startDownloadNexusFile)) - .def("downloadPath", bpy::pure_virtual(&IDownloadManager::downloadPath)) - ; - - bpy::class_("IInstallationManager") - .def("extractFile", bpy::pure_virtual(&IInstallationManager::extractFile)) - .def("extractFiles", bpy::pure_virtual(&IInstallationManager::extractFiles)) - .def("installArchive", bpy::pure_virtual(&IInstallationManager::installArchive)) - .def("setURL", bpy::pure_virtual(&IInstallationManager::setURL)) - ; - - bpy::class_("IModInterface") - .def("name", bpy::pure_virtual(&IModInterface::name)) - .def("absolutePath", bpy::pure_virtual(&IModInterface::absolutePath)) - .def("setVersion", bpy::pure_virtual(&IModInterface::setVersion)) - .def("setNewestVersion", bpy::pure_virtual(&IModInterface::setNewestVersion)) - .def("setIsEndorsed", bpy::pure_virtual(&IModInterface::setIsEndorsed)) - .def("setNexusID", bpy::pure_virtual(&IModInterface::setNexusID)) - .def("addNexusCategory", bpy::pure_virtual(&IModInterface::addNexusCategory)) - .def("addCategory", bpy::pure_virtual(&IModInterface::addCategory)) - .def("removeCategory", bpy::pure_virtual(&IModInterface::removeCategory)) - .def("categories", bpy::pure_virtual(&IModInterface::categories)) - .def("setGameName", bpy::pure_virtual(&IModInterface::setGameName)) - .def("setName", bpy::pure_virtual(&IModInterface::setName)) - .def("remove", bpy::pure_virtual(&IModInterface::remove)) - ; - - bpy::enum_("GuessQuality") - .value("invalid", MOBase::GUESS_INVALID) - .value("fallback", MOBase::GUESS_FALLBACK) - .value("good", MOBase::GUESS_GOOD) - .value("meta", MOBase::GUESS_META) - .value("preset", MOBase::GUESS_PRESET) - .value("user", MOBase::GUESS_USER) - ; - - bpy::class_, boost::noncopyable>("GuessedString") - .def("update", - static_cast &(GuessedValue::*)(const QString&, EGuessQuality)>(&GuessedValue::update), - bpy::return_value_policy(), updateWithQuality()) - .def("variants", &MOBase::GuessedValue::variants, bpy::return_value_policy()) - ; - - bpy::to_python_converter>(); - QFlags_from_python_obj(); - Functor0_converter(); // converter for the onRefreshed-callback - - bpy::enum_("PluginState") - .value("missing", IPluginList::STATE_MISSING) - .value("inactive", IPluginList::STATE_INACTIVE) - .value("active", IPluginList::STATE_ACTIVE) - ; - - bpy::class_("IPluginList") - .def("state", bpy::pure_virtual(&MOBase::IPluginList::state)) - .def("priority", bpy::pure_virtual(&MOBase::IPluginList::priority)) - .def("loadOrder", bpy::pure_virtual(&MOBase::IPluginList::loadOrder)) - .def("isMaster", bpy::pure_virtual(&MOBase::IPluginList::isMaster)) - .def("masters", bpy::pure_virtual(&MOBase::IPluginList::masters)) - .def("origin", bpy::pure_virtual(&MOBase::IPluginList::origin)) - .def("onRefreshed", bpy::pure_virtual(&MOBase::IPluginList::onRefreshed)) - .def("onPluginMoved", bpy::pure_virtual(&MOBase::IPluginList::onPluginMoved)) - .def("pluginNames", bpy::pure_virtual(&MOBase::IPluginList::pluginNames)) - .def("setState", bpy::pure_virtual(&MOBase::IPluginList::setState)) - .def("setLoadOrder", bpy::pure_virtual(&MOBase::IPluginList::setLoadOrder)) - ; - - bpy::to_python_converter>(); - QFlags_from_python_obj(); - Functor2_converter(); // converter for the onModStateChanged-callback - - bpy::enum_("ModState") - .value("exists", IModList::STATE_EXISTS) - .value("active", IModList::STATE_ACTIVE) - .value("essential", IModList::STATE_ESSENTIAL) - .value("empty", IModList::STATE_EMPTY) - .value("endorsed", IModList::STATE_ENDORSED) - .value("valid", IModList::STATE_VALID) - .value("alternate", IModList::STATE_ALTERNATE) - ; - - bpy::class_("IModList") - .def("displayName", bpy::pure_virtual(&MOBase::IModList::displayName)) - .def("allMods", bpy::pure_virtual(&MOBase::IModList::allMods)) - .def("state", bpy::pure_virtual(&MOBase::IModList::state)) - .def("setActive", bpy::pure_virtual(&MOBase::IModList::setActive)) - .def("priority", bpy::pure_virtual(&MOBase::IModList::priority)) - .def("setPriority", bpy::pure_virtual(&MOBase::IModList::setPriority)) - .def("onModStateChanged", bpy::pure_virtual(&MOBase::IModList::onModStateChanged)) - .def("onModMoved", bpy::pure_virtual(&MOBase::IModList::onModMoved)) - ; - - bpy::class_("IPlugin"); - - bpy::class_("IPluginDiagnose") - .def("activeProblems", bpy::pure_virtual(&MOBase::IPluginDiagnose::activeProblems)) - .def("shortDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::shortDescription)) - .def("fullDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::fullDescription)) - .def("hasGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::hasGuidedFix)) - .def("startGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::startGuidedFix)) - .def("_invalidate", &IPluginDiagnoseWrapper::invalidate) - ; - - bpy::class_("Mapping") - .def_readwrite("source", &Mapping::source) - .def_readwrite("destination", &Mapping::destination) - .def_readwrite("isDirectory", &Mapping::isDirectory) - .def_readwrite("createTarget", &Mapping::createTarget) - ; - - bpy::class_("IPluginFileMapper") - .def("mappings", bpy::pure_virtual(&MOBase::IPluginFileMapper::mappings)) - ; - - bpy::enum_("LoadOrderMechanism") - .value("FileTime", MOBase::IPluginGame::LoadOrderMechanism::FileTime) - .value("PluginsTxt", MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) - ; - - bpy::enum_("SortMechanism") - .value("NONE", MOBase::IPluginGame::SortMechanism::NONE) - .value("MLOX", MOBase::IPluginGame::SortMechanism::MLOX) - .value("BOSS", MOBase::IPluginGame::SortMechanism::BOSS) - .value("LOOT", MOBase::IPluginGame::SortMechanism::LOOT) - ; - - // This doesn't actually do the conversion, but might be convenient for accessing the names for enum bits - bpy::enum_("ProfileSetting") - .value("mods", MOBase::IPluginGame::MODS) - .value("configuration", MOBase::IPluginGame::CONFIGURATION) - .value("savegames", MOBase::IPluginGame::SAVEGAMES) - .value("preferDefaults", MOBase::IPluginGame::PREFER_DEFAULTS) - ; - - bpy::to_python_converter>(); - QFlags_from_python_obj(); - - bpy::class_("IPluginGame") - .def("gameName", bpy::pure_virtual(&MOBase::IPluginGame::gameName)) - .def("initializeProfile", bpy::pure_virtual(&MOBase::IPluginGame::initializeProfile)) - .def("savegameExtension", bpy::pure_virtual(&MOBase::IPluginGame::savegameExtension)) - .def("savegameSEExtension", bpy::pure_virtual(&MOBase::IPluginGame::savegameSEExtension)) - .def("isInstalled", bpy::pure_virtual(&MOBase::IPluginGame::isInstalled)) - .def("gameIcon", bpy::pure_virtual(&MOBase::IPluginGame::gameIcon)) - .def("gameDirectory", bpy::pure_virtual(&MOBase::IPluginGame::gameDirectory)) - .def("dataDirectory", bpy::pure_virtual(&MOBase::IPluginGame::dataDirectory)) - .def("setGamePath", bpy::pure_virtual(&MOBase::IPluginGame::setGamePath)) - .def("documentsDirectory", bpy::pure_virtual(&MOBase::IPluginGame::documentsDirectory)) - .def("savesDirectory", bpy::pure_virtual(&MOBase::IPluginGame::savesDirectory)) - .def("executables", bpy::pure_virtual(&MOBase::IPluginGame::executables)) - .def("steamAPPId", bpy::pure_virtual(&MOBase::IPluginGame::steamAPPId)) - .def("primaryPlugins", bpy::pure_virtual(&MOBase::IPluginGame::primaryPlugins)) - .def("gameVariants", bpy::pure_virtual(&MOBase::IPluginGame::gameVariants)) - .def("setGameVariant", bpy::pure_virtual(&MOBase::IPluginGame::setGameVariant)) - .def("binaryName", bpy::pure_virtual(&MOBase::IPluginGame::binaryName)) - .def("gameShortName", bpy::pure_virtual(&MOBase::IPluginGame::gameShortName)) - .def("primarySources", bpy::pure_virtual(&MOBase::IPluginGame::primarySources)) - .def("validShortNames", bpy::pure_virtual(&MOBase::IPluginGame::validShortNames)) - .def("gameNexusName", bpy::pure_virtual(&MOBase::IPluginGame::gameNexusName)) - .def("iniFiles", bpy::pure_virtual(&MOBase::IPluginGame::iniFiles)) - .def("DLCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::DLCPlugins)) - .def("CCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::CCPlugins)) - .def("loadOrderMechanism", bpy::pure_virtual(&MOBase::IPluginGame::loadOrderMechanism)) - .def("sortMechanism", bpy::pure_virtual(&MOBase::IPluginGame::sortMechanism)) - .def("nexusModOrganizerID", bpy::pure_virtual(&MOBase::IPluginGame::nexusModOrganizerID)) - .def("nexusGameID", bpy::pure_virtual(&MOBase::IPluginGame::nexusGameID)) - .def("looksValid", bpy::pure_virtual(&MOBase::IPluginGame::looksValid)) - .def("gameVersion", bpy::pure_virtual(&MOBase::IPluginGame::gameVersion)) - .def("getLauncherName", bpy::pure_virtual(&MOBase::IPluginGame::getLauncherName)) - - //Plugin interface. - .def("init", bpy::pure_virtual(&MOBase::IPluginGame::init)) - .def("name", bpy::pure_virtual(&MOBase::IPluginGame::name)) - .def("author", bpy::pure_virtual(&MOBase::IPluginGame::author)) - .def("description", bpy::pure_virtual(&MOBase::IPluginGame::description)) - .def("version", bpy::pure_virtual(&MOBase::IPluginGame::version)) - .def("isActive", bpy::pure_virtual(&MOBase::IPluginGame::isActive)) - .def("settings", bpy::pure_virtual(&MOBase::IPluginGame::settings)) - - // The syntax has to differ slightly from C++ because these are templated - .def("featureBSAInvalidation", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureDataArchives", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureGamePlugins", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureLocalSavegames", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureSaveGameInfo", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureScriptExtender", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - .def("featureUnmanagedMods", &MOBase::IPluginGame::feature, bpy::return_value_policy()) - ; - - bpy::enum_("InstallResult") - .value("success", MOBase::IPluginInstaller::RESULT_SUCCESS) - .value("failed", MOBase::IPluginInstaller::RESULT_FAILED) - .value("canceled", MOBase::IPluginInstaller::RESULT_CANCELED) - .value("manualRequested", MOBase::IPluginInstaller::RESULT_MANUALREQUESTED) - .value("notAttempted", MOBase::IPluginInstaller::RESULT_NOTATTEMPTED) - ; - - bpy::class_("IPluginInstallerCustom") - .def("setParentWidget", bpy::pure_virtual(&MOBase::IPluginInstallerCustom::setParentWidget)) - ; - - bpy::class_("IPluginModPage") - .def("setParentWidget", bpy::pure_virtual(&MOBase::IPluginModPage::setParentWidget)) - ; - - bpy::class_("IPluginPreview") - ; - - bpy::class_, boost::noncopyable>("IPluginTool") - .def("setParentWidget", bpy::pure_virtual(&MOBase::IPluginTool::setParentWidget)) - ; - - GuessedValue_converters(); - - HANDLE_converters(); - - //bpy::to_python_converter(); - - QList_from_python_obj(); - QList_from_python_obj(); - bpy::to_python_converter, - QList_to_python_list >(); - QList_from_python_obj(); - bpy::to_python_converter, - QList_to_python_list>(); - QList_from_python_obj(); - bpy::to_python_converter, - QList_to_python_list>(); - - QMap_converters(); - QMap_converters(); - - std_vector_from_python_obj(); - std_vector_from_python_obj(); - bpy::to_python_converter, - std_vector_to_python_list>(); - - stdset_from_python_list(); - - registerGameFeaturesPythonConverters(); -} - - -PythonRunner::PythonRunner(const MOBase::IOrganizer *moInfo) - : m_MOInfo(moInfo) -{ - m_PythonHome = new wchar_t[MAX_PATH + 1]; -} - -static const char *argv0 = "ModOrganizer.exe"; - -struct PrintWrapper -{ - void write(const char * message) - { - buffer << message; - if (buffer.tellp() != 0 && buffer.str().back() == '\n') + // we only initialize Python once for the whole lifetime of the program, even if + // MO2 is restarted and the proxy or PythonRunner objects are deleted and + // recreated, Python is not re-initialized + // + // in an ideal world, we would initialize Python here (or in the constructor) + // and then finalize it in the destructor + // + // unfortunately, many library, including PyQt6, do not handle properly + // re-initializing the Python interpreter, so we cannot do that and we keep the + // interpreter alive + // + + if (Py_IsInitialized()) { + return true; + } + + try { + static const char* argv0 = "ModOrganizer.exe"; + + // set the module search paths + // + auto paths = pythonPaths; + if (paths.empty()) { + + // while it is possible to use config.pythonpath_env, it requires + // config.use_environment, which brings other stuffs in and might not be + // what we want, so simply parsing the path ourselve + // + if (auto* pythonPath = std::getenv("PYTHONPATH")) { + for (auto& path : QString::fromStdString(pythonPath).split(";")) { + paths.push_back( + std::filesystem::path{path.trimmed().toStdWString()}); + } + } + } + + PyConfig config; + PyConfig_InitIsolatedConfig(&config); + + // from PyBind11 + config.parse_argv = 0; + config.install_signal_handlers = 0; + + // from MO2 + config.site_import = 1; + config.optimization_level = 2; + + // set paths to configuration + if (!paths.empty()) { + config.module_search_paths_set = 1; + for (auto const& path : paths) { + PyWideStringList_Append(&config.module_search_paths, + absolute(path).native().c_str()); + } + } + + py::initialize_interpreter(&config, 1, &argv0, true); + + if (!Py_IsInitialized()) { + MOBase::log::error( + "failed to init python: failed to initialize interpreter."); + + if (PyGILState_Check()) { + PyEval_SaveThread(); + } + + return false; + } + + { + py::module_ mainModule = py::module_::import("__main__"); + py::object mainNamespace = mainModule.attr("__dict__"); + mainNamespace["sys"] = py::module_::import("sys"); + mainNamespace["mobase"] = py::module_::import("mobase"); + + mo2::python::configure_python_stream(); + mo2::python::configure_python_logging(mainNamespace["mobase"]); + } + + // we need to release the GIL here - which is what this does + // + // when Python is initialized, the GIl is acquired, and if it is not + // release, trying to acquire it on a different thread will deadlock + PyEval_SaveThread(); + + return true; + } + catch (const py::error_already_set& ex) { + MOBase::log::error("failed to init python: {}", ex.what()); + return false; + } + } + + void PythonRunner::addDllSearchPath(std::filesystem::path const& dllPath) { - // actually put the string in a variable so it doesn't get destroyed as soon as we get a pointer to its data - std::string string = buffer.str().substr(0, buffer.str().length() - 1); - qDebug().nospace().noquote() << string.c_str(); - buffer = std::stringstream(); - } - } - - std::stringstream buffer; -}; - -// ErrWrapper is in error.h - -BOOST_PYTHON_MODULE(moprivate) -{ - bpy::class_("PrintWrapper", bpy::init<>()) - .def("write", &PrintWrapper::write); - bpy::class_("ErrWrapper", bpy::init<>()) - .def("instance", &ErrWrapper::instance, bpy::return_value_policy()).staticmethod("instance") - .def("write", &ErrWrapper::write) - .def("startRecordingExceptionMessage", &ErrWrapper::startRecordingExceptionMessage) - .def("stopRecordingExceptionMessage", &ErrWrapper::stopRecordingExceptionMessage) - .def("getLastExceptionMessage", &ErrWrapper::getLastExceptionMessage); -} - -bool PythonRunner::initPython(const QString &pythonPath) -{ - try { - if (!pythonPath.isEmpty() && !QFile::exists(pythonPath + "/python.exe")) { - return false; - } - pythonPath.toWCharArray(m_PythonHome); - if (!pythonPath.isEmpty()) { - Py_SetPythonHome(m_PythonHome); + py::gil_scoped_acquire lock; + py::module_::import("os").attr("add_dll_directory")(absolute(dllPath)); } - wchar_t argBuffer[MAX_PATH]; - const size_t cSize = strlen(argv0) + 1; - mbstowcs(argBuffer, argv0, MAX_PATH); - - Py_SetProgramName(argBuffer); - PyImport_AppendInittab("mobase", &PyInit_mobase); - PyImport_AppendInittab("moprivate", &PyInit_moprivate); - Py_OptimizeFlag = 2; - Py_NoSiteFlag = 1; - initPath(); - Py_InitializeEx(0); - - if (!Py_IsInitialized()) { - return false; - } + void PythonRunner::ensureFolderInPath(QString folder) + { + py::module_ sys = py::module_::import("sys"); + py::list sysPath = sys.attr("path"); - PySys_SetArgv(0, (wchar_t**)&argBuffer); - - bpy::object mainModule = bpy::import("__main__"); - bpy::object mainNamespace = mainModule.attr("__dict__"); - mainNamespace["sys"] = bpy::import("sys"); - mainNamespace["moprivate"] = bpy::import("moprivate"); - bpy::import("site"); - bpy::exec("sys.stdout = moprivate.PrintWrapper()\n" - "sys.stderr = moprivate.ErrWrapper.instance()\n" - "sys.excepthook = lambda x, y, z: sys.__excepthook__(x, y, z)\n", - mainNamespace); - - return true; - } catch (const bpy::error_already_set&) { - qDebug("failed to init python"); - PyErr_Print(); - if (PyErr_Occurred()) { - PyErr_Print(); - } else { - qCritical("An unexpected C++ exception was thrown in python code"); + // 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); + } } - return false; - } -} - - -bool handled_exec_file(bpy::str filename, bpy::object globals = bpy::object(), bpy::object locals = bpy::object()) -{ - return bpy::handle_exception(std::bind(bpy::exec_file, filename, globals, locals)); -} - - -#define TRY_PLUGIN_TYPE(type, var) do { \ - bpy::extract extr(var); \ - if (extr.check()) { \ - QObject *res = extr; \ - interfaceList.append(res); \ - }\ - } while (false) - - -void PythonRunner::initPath() -{ - static QStringList paths = { - QCoreApplication::applicationDirPath() + "/pythoncore.zip", - QCoreApplication::applicationDirPath() + "/pythoncore", - m_MOInfo->pluginDataPath() - }; - Py_SetPath(paths.join(';').toStdWString().c_str()); -} - - -QList PythonRunner::instantiate(const QString &pluginName) -{ - try { - GILock lock; - bpy::object mainModule = bpy::import("__main__"); - bpy::object moduleNamespace = mainModule.attr("__dict__"); - - bpy::object sys = bpy::import("sys"); - moduleNamespace["sys"] = sys; - moduleNamespace["mobase"] = bpy::import("mobase"); - - std::string temp = ToString(pluginName); - if (handled_exec_file(temp.c_str(), moduleNamespace)) { - reportPythonError(); - return QList(); + QList PythonRunner::load(const QString& identifier) + { + 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; + + if (identifier.endsWith(".py")) { + py::object mainModule = py::module_::import("__main__"); + + // make a copy, otherwise we might end up calling the createPlugin() or + // createPlugins() function multiple time + py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")(); + + std::string temp = ToString(identifier); + py::eval_file(temp, moduleNamespace).is_none(); + moduleDict = moduleNamespace; + } + else { + // Retrieve the module name: + QStringList parts = identifier.split("/"); + std::string moduleName = ToString(parts.takeLast()); + ensureFolderInPath(parts.join("/")); + + // check if the module is already loaded + 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__"); + } + } + + if (py::len(moduleDict) == 0) { + MOBase::log::error("No plugins found in {}.", identifier); + return {}; + } + + // Create the plugins: + std::vector plugins; + + if (moduleDict.contains("createPlugin")) { + plugins.push_back(moduleDict["createPlugin"]()); + } + else if (moduleDict.contains("createPlugins")) { + py::object pyPlugins = moduleDict["createPlugins"](); + if (!py::isinstance(pyPlugins)) { + MOBase::log::error( + "Plugin {}: createPlugins must return a sequence.", identifier); + } + else { + py::sequence pyList(pyPlugins); + size_t nPlugins = pyList.size(); + for (size_t i = 0; i < nPlugins; ++i) { + plugins.push_back(pyList[i]); + } + } + } + else { + MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", + identifier); + } + + // If we have no plugins, there was an issue, and we already logged the + // problem: + if (plugins.empty()) { + return QList(); + } + + 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); + } + + // Append the plugins to the main list: + allInterfaceList.append(interfaceList); + } + + return allInterfaceList; + } + catch (const py::error_already_set& ex) { + MOBase::log::error("Failed to import plugin from {}.", identifier); + throw pyexcept::PythonError(ex); + } + } + + void PythonRunner::unload(const QString& identifier) + { + 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()); + } + } + } + } + + // 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")(); + } + + log::debug("Deleting {} python objects for {}.", it->second.size(), + identifier); + m_PythonObjects.erase(it); + } + } + + bool PythonRunner::isInitialized() const + { + return Py_IsInitialized() != 0; } - m_PythonObjects[pluginName] = moduleNamespace["createPlugin"](); - - bpy::object pluginObj = m_PythonObjects[pluginName]; - QList interfaceList; - TRY_PLUGIN_TYPE(IPluginGame, pluginObj); - // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject - TRY_PLUGIN_TYPE(IPluginDiagnoseWrapper, pluginObj); - // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject - TRY_PLUGIN_TYPE(IPluginFileMapperWrapper, pluginObj); - TRY_PLUGIN_TYPE(IPluginInstallerCustom, pluginObj); - TRY_PLUGIN_TYPE(IPluginModPage, pluginObj); - TRY_PLUGIN_TYPE(IPluginPreview, pluginObj); - TRY_PLUGIN_TYPE(IPluginTool, pluginObj); - if (interfaceList.isEmpty()) - TRY_PLUGIN_TYPE(IPluginWrapper, pluginObj); - - return interfaceList; - } catch (const bpy::error_already_set&) { - qWarning("failed to run python script \"%s\"", qUtf8Printable(pluginName)); - reportPythonError(); - } - return QList(); -} - -bool PythonRunner::isPythonInstalled() const -{ - return Py_IsInitialized() != 0; -} - - -bool PythonRunner::isPythonVersionSupported() const -{ - const char *version = Py_GetVersion(); - return strstr(version, "3.7") == version; -} +} // namespace mo2::python diff --git a/src/runner/pythonrunner.h b/src/runner/pythonrunner.h index ae5d6205..5f9751bb 100644 --- a/src/runner/pythonrunner.h +++ b/src/runner/pythonrunner.h @@ -1,29 +1,53 @@ -#ifndef PYTHONRUNNER_H -#define PYTHONRUNNER_H - -#include -#include -#include -#include -#include - - -class IPythonRunner { -public: - virtual QList instantiate(const QString &pluginName) = 0; - virtual bool isPythonInstalled() const = 0; - virtual bool isPythonVersionSupported() const = 0; -}; - - -#ifdef PYTHONRUNNER_LIBRARY -#define PYDLLEXPORT Q_DECL_EXPORT -#else // PYTHONRUNNER_LIBRARY -#define PYDLLEXPORT Q_DECL_IMPORT -#endif // PYTHONRUNNER_LIBRARY - -extern "C" PYDLLEXPORT IPythonRunner *CreatePythonRunner(MOBase::IOrganizer *moInfo, const QString &pythonDir); - - - -#endif // PYTHONRUNNER_H +#ifndef PYTHONRUNNER_H +#define PYTHONRUNNER_H + +#include +#include + +#include +#include +#include +#include + +#ifdef RUNNER_BUILD +#define RUNNER_DLL_EXPORT Q_DECL_EXPORT +#else +#define RUNNER_DLL_EXPORT Q_DECL_IMPORT +#endif + +namespace mo2::python { + + // python runner interface + // + class IPythonRunner { + public: + virtual QList load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + + // initialize Python + // + // pythonPaths contains the list of built-in paths for the Python library + // (pythonxxx.zip, etc.), an empty list uses the default Python paths (e.g., the + // PYTHONPATH environment variable) + // + virtual bool + initialize(std::vector const& pythonPaths = {}) = 0; + + // add a DLL search path + // + virtual void addDllSearchPath(std::filesystem::path const& dllPath) = 0; + + // check if the runner has been initialized, i.e., initialize() has been + // called and succeeded + virtual bool isInitialized() const = 0; + + virtual ~IPythonRunner() {} + }; + + // create the Python runner + // + RUNNER_DLL_EXPORT std::unique_ptr createPythonRunner(); + +} // namespace mo2::python + +#endif // PYTHONRUNNER_H diff --git a/src/runner/pythonutils.cpp b/src/runner/pythonutils.cpp new file mode 100644 index 00000000..c94a50b8 --- /dev/null +++ b/src/runner/pythonutils.cpp @@ -0,0 +1,155 @@ +#include "pythonutils.h" + +#include +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + +namespace mo2::python { + + class PrintWrapper { + MOBase::log::Levels level_; + std::stringstream buffer_; + + public: + PrintWrapper(MOBase::log::Levels level) : level_{level} {} + + void write(std::string_view message) + { + buffer_ << message; + if (buffer_.tellp() != 0 && buffer_.str().back() == '\n') { + const auto full_message = buffer_.str(); + MOBase::log::log(level_, "{}", + full_message.substr(0, full_message.length() - 1)); + buffer_ = std::stringstream{}; + } + } + }; + + /** + * @brief Construct a dynamic Python type. + * + */ + template + pybind11::object make_python_type(std::string_view name, + pybind11::tuple base_classes, Args&&... args) + { + // this is ugly but that's how it's done in C Python + auto type = py::reinterpret_borrow((PyObject*)&PyType_Type); + + // create the python class + return type(name, base_classes, py::dict(std::forward(args)...)); + } + + void configure_python_stream() + { + // create the "MO2Handler" python class + auto printWrapper = make_python_type( + "MO2PrintWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Debug); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + auto errorWrapper = make_python_type( + "MO2ErrorWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Error); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + py::module_ sys = py::module_::import("sys"); + sys.attr("stdout") = printWrapper(); + sys.attr("stderr") = errorWrapper(); + + // this is required to handle exception in Python code OUTSIDE of pybind11 call, + // typically on Qt classes with methods overridden on the Python side + // + // without this, the application will crash instead of properly handling the + // exception as it would do with a py::error_already_set{} + // + // IMPORTANT: sys.attr("excepthook") = sys.attr("__excepthook__") DOES NOT WORK, + // and I have no clue why since the attribute does not seem to get updated (at + // least a print does not show it) + // + sys.attr("excepthook") = + py::eval("lambda x, y, z: sys.__excepthook__(x, y, z)"); + } + + // Small structure to hold the levels - There are copy paste from + // my Python version and I assume these will not change soon: + struct PyLogLevel { + static constexpr int CRITICAL = 50; + static constexpr int ERROR = 40; + static constexpr int WARNING = 30; + static constexpr int INFO = 20; + static constexpr int DEBUG = 10; + }; + + // This is the function we are going to use as our Handler .emit + // method. + void emit_function(py::object record) + { + + // There are other parameters that could be used, but this is minimal + // for now (filename, line number, etc.). + const int level = record.attr("levelno").cast(); + const std::wstring msg = py::str(record.attr("msg")).cast(); + + switch (level) { + case PyLogLevel::CRITICAL: + case PyLogLevel::ERROR: + MOBase::log::error("{}", msg); + break; + case PyLogLevel::WARNING: + MOBase::log::warn("{}", msg); + break; + case PyLogLevel::INFO: + MOBase::log::info("{}", msg); + break; + case PyLogLevel::DEBUG: + default: // There is a "NOTSET" level in theory: + MOBase::log::debug("{}", msg); + break; + } + }; + + 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 pybind11, + // and since a lot of this would require extra register only for this. + + // see also + // https://github.com/pybind/pybind11/issues/1193#issuecomment-429451094 + + // retrieve the logging module and the Handler class. + auto logging = py::module_::import("logging"); + auto Handler = logging.attr("Handler"); + + // create the "MO2Handler" python class + auto MO2Handler = + make_python_type("LogHandler", py::make_tuple(Handler), + py::arg("emit") = py::cpp_function(emit_function)); + + // create the default logger + auto handler = MO2Handler(); + handler.attr("setLevel")(PyLogLevel::DEBUG); + auto logger = logging.attr("getLogger")(py::object(mobase.attr("__name__"))); + logger.attr("setLevel")(PyLogLevel::DEBUG); + + // set mobase attributes + mobase.attr("LogHandler") = MO2Handler; + mobase.attr("logger") = logger; + + logging.attr("root").attr("setLevel")(PyLogLevel::DEBUG); + logging.attr("root").attr("addHandler")(handler); + } + +} // namespace mo2::python diff --git a/src/runner/pythonutils.h b/src/runner/pythonutils.h new file mode 100644 index 00000000..019e7821 --- /dev/null +++ b/src/runner/pythonutils.h @@ -0,0 +1,25 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include + +namespace mo2::python { + + /** + * @brief Configure Python stdout and stderr to log to MO2. + * + */ + void configure_python_stream(); + + /** + * @brief Configure logging for MO2 python plugin. + * + * @param mobase The mobase module. + */ + void configure_python_logging(pybind11::module_ mobase); + +} // namespace mo2::python + +#endif diff --git a/src/runner/pythonwrapperutilities.h b/src/runner/pythonwrapperutilities.h deleted file mode 100644 index 86317e0f..00000000 --- a/src/runner/pythonwrapperutilities.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef PYTHONWRAPPERUTILITIES_H -#define PYTHONWRAPPERUTILITIES_H - -#include - -#include "error.h" - -class MissingImplementation : public MOBase::MyException { -public: - MissingImplementation(QString className, QString methodName) : MyException("Python class implementing \"" + - className + - "\" has no implementation of method \"" + - methodName + "\"") {} -}; - -#define PYCATCH catch (const boost::python::error_already_set &) { reportPythonError(); throw MOBase::MyException("unhandled exception"); }\ - catch (const MissingImplementation &missingImplementationException) { throw missingImplementationException; }\ - catch (...) { throw MOBase::MyException("An unknown exception was thrown in python code"); } - -template -ReturnType basicWrapperFunctionImplementation(const WrapperType *wrapper, const char *methodName, Args... args) -{ - try { - GILock lock; - boost::python::override implementation = wrapper->get_override(methodName); - if (!implementation) - throw MissingImplementation(wrapper->className, methodName); - return implementation(args...).as(); - } PYCATCH; -} - -#endif // PYTHONWRAPPERUTILITIES_H diff --git a/src/runner/sipapiaccess.cpp b/src/runner/sipapiaccess.cpp deleted file mode 100644 index b233f694..00000000 --- a/src/runner/sipapiaccess.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "sipapiaccess.h" -#include -#include -#include - -const sipAPIDef* sipAPIAccess::sipAPI() -{ - QString exception; - static const sipAPIDef* sipApi = nullptr; - if (sipApi == nullptr) { - #if defined(SIP_USE_PYCAPSULE) - PyImport_ImportModule("PyQt5.sip"); - - auto errorObj = PyErr_Occurred(); - if (errorObj != NULL) { - PyObject* type, * value, * traceback; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - if (traceback != NULL) { - boost::python::handle<> h_type(type); - boost::python::handle<> h_val(value); - boost::python::handle<> h_tb(traceback); - boost::python::object tb(boost::python::import("traceback")); - boost::python::object fmt_exp(tb.attr("format_exception")); - boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); - boost::python::object exp_str(boost::python::str("\n").join(exp_list)); - boost::python::extract returned(exp_str); - exception = QString::fromStdString(returned()); - } - PyErr_Restore(type, value, traceback); - throw MOBase::MyException(QString("Failed to load PyQt5: %1").arg(exception)); - } - - sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt5.sip._C_API", 0); - if (sipApi == NULL) { - auto errorObj = PyErr_Occurred(); - if (errorObj != NULL) { - PyObject* type, * value, * traceback; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - if (traceback != NULL) { - boost::python::handle<> h_type(type); - boost::python::handle<> h_val(value); - boost::python::handle<> h_tb(traceback); - boost::python::object tb(boost::python::import("traceback")); - boost::python::object fmt_exp(tb.attr("format_exception")); - boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); - boost::python::object exp_str(boost::python::str("\n").join(exp_list)); - boost::python::extract returned(exp_str); - exception = QString::fromStdString(returned()); - } - PyErr_Restore(type, value, traceback); - } - throw MOBase::MyException(QString("Failed to load SIP API: %1").arg(exception)); - } - #else - PyObject* sip_module; - PyObject* sip_module_dict; - PyObject* c_api; - - /* Import the SIP module. */ - sip_module = PyImport_ImportModule("PyQt5.sip"); - - if (sip_module == NULL) - return NULL; - - /* Get the module's dictionary. */ - sip_module_dict = PyModule_GetDict(sip_module); - - /* Get the "_C_API" attribute. */ - c_api = PyDict_GetItemString(sip_module_dict, "_C_API"); - - if (c_api == NULL) - return NULL; - - /* Sanity check that it is the right type. */ - if (!PyCObject_Check(c_api)) - return NULL; - - /* Get the actual pointer from the object. */ - sipApi = (const sipAPIDef*)PyCObject_AsVoidPtr(c_api); - #endif - } - - return sipApi; -} \ No newline at end of file diff --git a/src/runner/sipapiaccess.h b/src/runner/sipapiaccess.h deleted file mode 100644 index 2706ddae..00000000 --- a/src/runner/sipapiaccess.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SIPAPIACCESS_H -#define SIPAPIACCESS_H - -#include - -class sipAPIAccess -{ -public: - static const sipAPIDef* sipAPI(); -}; - -#endif // SIPAPIACCESS_H diff --git a/src/runner/uibasewrappers.h b/src/runner/uibasewrappers.h deleted file mode 100644 index b31bb608..00000000 --- a/src/runner/uibasewrappers.h +++ /dev/null @@ -1,499 +0,0 @@ -#ifndef UIBASEWRAPPERS_H -#define UIBASEWRAPPERS_H - - -#ifndef Q_MOC_RUN -#pragma warning (push, 0) -#include -#pragma warning (pop) -#endif - -#include -#include -#include -#include - -#include "iplugingame.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "error.h" -#include "gilock.h" -#include "pythonwrapperutilities.h" - -extern MOBase::IOrganizer *s_Organizer; - -using MOBase::ModRepositoryFileInfo; - -/** - * @brief Wrapper class for the bridge to a mod repository. Awkward: This may be - * unnecessary but I didn't manage to figure out how to correctly connect python - * code to C++ signals - */ -class ModRepositoryBridgeWrapper : public QObject -{ - Q_OBJECT -public: - - ModRepositoryBridgeWrapper() - : m_Wrapped(s_Organizer->createNexusBridge()) - { - } - - ModRepositoryBridgeWrapper(MOBase::IModRepositoryBridge *wrapped) - : m_Wrapped(wrapped) - { - } - - ~ModRepositoryBridgeWrapper() - { - delete m_Wrapped; - } - - void requestDescription(QString gameName, int modID, QVariant userData) - { m_Wrapped->requestDescription(gameName, modID, userData); } - void requestFiles(QString gameName, int modID, QVariant userData) - { m_Wrapped->requestFiles(gameName, modID, userData); } - void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) - { m_Wrapped->requestFileInfo(gameName, modID, fileID, userData); } - void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) - { m_Wrapped->requestToggleEndorsement(gameName, modID, modVersion, endorse, userData); } - - void onFilesAvailable(boost::python::object callback) { - m_FilesAvailableHandler = callback; - connect(m_Wrapped, SIGNAL(filesAvailable(int,QVariant,const QList&)), - this, SLOT(filesAvailable(int,QVariant,const QList&)), - Qt::UniqueConnection); - } - - void onDescriptionAvailable(boost::python::object callback) { - m_DescriptionAvailableHandler = callback; - connect(m_Wrapped, SIGNAL(descriptionAvailable(int,QVariant,QVariant)), - this, SLOT(descriptionAvailable(int,QVariant,QVariant)), - Qt::UniqueConnection); - } - - void onFileInfoAvailable(boost::python::object callback) { - m_FileInfoHandler = callback; - connect(m_Wrapped, SIGNAL(fileInfoAvailable(int,int,QVariant,QVariant)), - this, SLOT(fileInfoAvailable(int,int,QVariant,QVariant)), - Qt::UniqueConnection); - } - - void onEndorsementToggled(boost::python::object callback) { - m_EndorsementToggledHandler = callback; - connect(m_Wrapped, SIGNAL(endorsementToggled(int,QVariant,QVariant)), - this, SLOT(endorsementToggled(int,QVariant,QVariant)), - Qt::UniqueConnection); - } - - void onTrackingToggled(boost::python::object callback) { - m_TrackingToggledHandler = callback; - connect(m_Wrapped, SIGNAL(trackingToggled(int,QVariant,bool)), - this, SLOT(trackingToggled(int,QVariant,bool)), - Qt::UniqueConnection); - - } - - void onRequestFailed(boost::python::object callback) { - m_FailedHandler = callback; - connect(m_Wrapped, SIGNAL(requestFailed(int,int,QVariant,QString)), - this, SLOT(requestFailed(int,int,QVariant,QString)), - Qt::UniqueConnection); - } - -private: - - Q_DISABLE_COPY(ModRepositoryBridgeWrapper) - -private Q_SLOTS: - - void filesAvailable(int modID, QVariant userData, const QList &resultData) - { - if (m_FilesAvailableHandler.is_none()) { - qCritical("no handler connected"); - return; - } - try { - GILock lock; - m_FilesAvailableHandler(modID, userData, resultData); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } - - void descriptionAvailable(int modID, QVariant userData, const QVariant resultData) - { - try { - if (m_DescriptionAvailableHandler.is_none()) { - qCritical("no handler connected"); - return; - } - try { - GILock lock; - m_DescriptionAvailableHandler(modID, userData, resultData); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } catch (const std::exception &e) { - qCritical("failed to report event: %s", e.what()); - } catch (...) { - qCritical("failed to report event"); - } - } - - void fileInfoAvailable(int modID, int fileID, QVariant userData, const QVariant resultData) { - try { - if (m_FileInfoHandler.is_none()) { - qCritical("no handler connected"); - return; - } - try { - GILock lock; - m_FileInfoHandler(modID, fileID, userData, resultData); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } catch (const std::exception &e) { - qCritical("failed to report event: %s", e.what()); - } catch (...) { - qCritical("failed to report event"); - } - } - - void endorsementToggled(int modID, QVariant userData, const QVariant resultData) - { - try { - if (m_EndorsementToggledHandler.is_none()) { - qCritical("no handler connected"); - return; - } - try { - GILock lock; - m_EndorsementToggledHandler(modID, userData, resultData); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } catch (const std::exception &e) { - qCritical("failed to report event: %s", e.what()); - } catch (...) { - qCritical("failed to report event"); - } - } - - void trackingToggled(int modID, QVariant userData, bool tracked) - { - try { - if (m_TrackingToggledHandler.is_none()) { - qCritical("no handler connected"); - return; - } - try { - GILock lock; - m_TrackingToggledHandler(modID, userData, tracked); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } catch (const std::exception &e) { - qCritical("failed to report event: %s", e.what()); - } catch (...) { - qCritical("failed to report event"); - } - } - - void requestFailed(int modID, int fileID, QVariant userData, const QString &errorMessage) - { - try { - GILock lock; - m_FailedHandler(modID, fileID, userData, errorMessage); - } catch (const boost::python::error_already_set&) { - reportPythonError(); - } - } - -private: - - MOBase::IModRepositoryBridge *m_Wrapped; - boost::python::object m_FilesAvailableHandler; - boost::python::object m_DescriptionAvailableHandler; - boost::python::object m_FileInfoHandler; - boost::python::object m_EndorsementToggledHandler; - boost::python::object m_TrackingToggledHandler; - boost::python::object m_FailedHandler; - -}; - -// NOTE: Completely unnecessary - we're never going to override IOrganizer from within Python -struct IOrganizerWrapper : MOBase::IOrganizer, - boost::python::wrapper { - virtual MOBase::IModRepositoryBridge *createNexusBridge() const override - { - return this->get_override("createNexusBridge")(); - } - virtual QString profileName() const override - { - return this->get_override("profileName")(); - } - virtual QString profilePath() const override - { - return this->get_override("profilePath")(); - } - virtual QString downloadsPath() const override - { - return this->get_override("downloadsPath")(); - } - virtual QString overwritePath() const override - { - return this->get_override("overwritePath")(); - } - virtual QString basePath() const // override - { - return this->get_override("basePath")(); - } - virtual QString modsPath() const override - { - return this->get_override("modsPath")(); - } - virtual MOBase::VersionInfo appVersion() const override - { - return this->get_override("appVersion")(); - } - virtual MOBase::IModInterface *getMod(const QString &name) const override - { - return this->get_override("getMod")(name); - } - virtual MOBase::IModInterface * - createMod(MOBase::GuessedValue &name) override - { - return this->get_override("createMod")(name); - } - virtual MOBase::IPluginGame *getGame(const QString &gameName) const override - { - return this->get_override("getGame")(gameName); - } - virtual bool removeMod(MOBase::IModInterface *mod) override - { - return this->get_override("removeMod")(mod); - } - virtual void modDataChanged(MOBase::IModInterface *mod) override - { - this->get_override("modDataChanged")(mod); - } - virtual QVariant pluginSetting(const QString &pluginName, - const QString &key) const override - { - return this->get_override("pluginSetting")(pluginName, key).as(); - } - virtual void setPluginSetting(const QString &pluginName, const QString &key, - const QVariant &value) override - { - this->get_override("setPluginSetting")(pluginName, key, value); - } - virtual QVariant persistent(const QString &pluginName, const QString &key, - const QVariant &def = QVariant()) const override - { - return this->get_override("persistent")(pluginName, key, def).as(); - } - virtual void setPersistent(const QString &pluginName, const QString &key, - const QVariant &value, bool sync = true) override - { - this->get_override("setPersistent")(pluginName, key, value, sync); - } - virtual QString pluginDataPath() const override - { - return this->get_override("pluginDataPath")(); - } - virtual MOBase::IModInterface *installMod(const QString &fileName, - const QString &nameSuggestion - = QString()) override - { - return this->get_override("installMod")(fileName, nameSuggestion); - } - virtual MOBase::IDownloadManager *downloadManager() const override - { - return this->get_override("downloadManager")(); - } - virtual MOBase::IPluginList *pluginList() const override - { - return this->get_override("pluginList")(); - } - virtual MOBase::IModList *modList() const override - { - return this->get_override("modList")(); - } - virtual QString resolvePath(const QString &fileName) const override - { - return this->get_override("resolvePath")(fileName); - } - virtual QStringList - listDirectories(const QString &directoryName) const override - { - return this->get_override("listDirectories")(directoryName); - } - virtual QStringList - findFiles(const QString &path, - const std::function &filter) const override - { - return this->get_override("findFiles")(path, filter); - } - virtual QStringList getFileOrigins(const QString &fileName) const override - { - return this->get_override("getFileOrigins")(fileName); - } - virtual QList findFileInfos( - const QString &path, - const std::function &filter) const override - { - return this->get_override("findFileInfos")(path, filter); - } - virtual HANDLE startApplication(const QString &executable, - const QStringList &args = QStringList(), - const QString &cwd = "", - const QString &profile = "", - const QString &forcedCustomOverwrite = "", - bool ignoreCustomOverwrite = false) override - { - return reinterpret_cast(this->get_override("startApplication")(executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite).as()); - } - virtual bool waitForApplication(HANDLE handle, - LPDWORD exitCode = nullptr) const override - { - return this->get_override("waitForApplication")(reinterpret_cast(handle), exitCode); - } - virtual void refreshModList(bool saveChanges = true) override - { - this->get_override("refreshModList")(saveChanges); - } - virtual bool - onAboutToRun(const std::function &func) override - { - return this->get_override("onAboutToRun")(func); - } - virtual bool onFinishedRun( - const std::function &func) override - { - return this->get_override("onFinishedRun")(func); - } - virtual bool - onModInstalled(const std::function &func) override - { - return this->get_override("onModInstalled")(func); - } - virtual MOBase::IProfile *profile() const override - { - return this->get_override("profile")(); - } - virtual MOBase::IPluginGame const *managedGame() const override - { - return this->get_override("managedGame")(); - } - virtual QStringList modsSortedByProfilePriority() const override - { - return this->get_override("modsSortedByProfilePriority")(); - } -}; - -struct IProfileWrapper: MOBase::IProfile, boost::python::wrapper -{ - virtual QString name() const override { return this->get_override("name")(); } - virtual QString absolutePath() const override { return this->get_override("absolutePath")(); } - virtual bool localSavesEnabled() const override { return this->get_override("localSavesEnabled")(); } - virtual bool localSettingsEnabled() const override { return this->get_override("localSettingsEnabled")(); } - virtual bool invalidationActive(bool *supported) const override { return this->get_override("invalidationActive")(supported); } -}; - -struct IDownloadManagerWrapper: MOBase::IDownloadManager, boost::python::wrapper -{ - virtual int startDownloadURLs(const QStringList &urls) { return this->get_override("startDownloadURLs")(urls); } - virtual int startDownloadNexusFile(int modID, int fileID) { return this->get_override("startDownloadNexusFile")(modID, fileID); } - virtual QString downloadPath(int id) { return this->get_override("downloadPath")(id); } -}; - -struct IModRepositoryBridgeWrapper: MOBase::IModRepositoryBridge, boost::python::wrapper -{ - virtual void requestDescription(QString gameName, int modID, QVariant userData) { this->get_override("requestDescription")(gameName, modID, userData); } - virtual void requestFiles(QString gameName, int modID, QVariant userData) { this->get_override("requestFiles")(gameName, modID, userData); } - virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) { this->get_override("requestFileInfo")(gameName, modID, fileID, userData); } - virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData) { this->get_override("requestDownloadURL")(gameName, modID, fileID, userData); } - virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) { this->get_override("requestToggleEndorsement")(gameName, modID, endorse, userData); } -}; - -struct IInstallationManagerWrapper: MOBase::IInstallationManager, boost::python::wrapper -{ - virtual QString extractFile(const QString &fileName) { return this->get_override("extractFile")(fileName); } - virtual QStringList extractFiles(const QStringList &files, bool flatten) { return this->get_override("extractFiles")(files, flatten); } - virtual MOBase::IPluginInstaller::EInstallResult installArchive(MOBase::GuessedValue &modName, const QString &archiveFile, const int &modId = 0) { return this->get_override("installArchive")(modName, archiveFile, modId); } - virtual void setURL(QString const &url) { this->get_override("setURL")(url); } -}; - -struct IModInterfaceWrapper: MOBase::IModInterface, boost::python::wrapper -{ - virtual QString name() const override { return this->get_override("name")(); } - virtual QString absolutePath() const override { return this->get_override("absolutePath")(); } - virtual void setVersion(const MOBase::VersionInfo &version) override { this->get_override("setVersion")(version); } - virtual void setNewestVersion(const MOBase::VersionInfo &version) override { this->get_override("setNewestVersion")(version); } - virtual void setIsEndorsed(bool endorsed) override { this->get_override("setIsEndorsed")(endorsed); } - virtual void setNexusID(int nexusID) override { this->get_override("setNexusID")(nexusID); } - virtual void setInstallationFile(const QString &fileName) override { this->get_override("setInstallationFile")(fileName); } - virtual void addNexusCategory(int categoryID) override { this->get_override("addNexusCategory")(categoryID); } - virtual void setGameName(const QString &gameName) override { this->get_override("setGameName")(gameName); } - virtual bool setName(const QString &name) override { return this->get_override("setName")(name); } - virtual bool remove() override { return this->get_override("remove")(); } - virtual void addCategory(const QString &categoryName) override { this->get_override("addCategory")(categoryName); } - virtual bool removeCategory(const QString &categoryName) override { return this->get_override("removeCategory")(categoryName); } - virtual QStringList categories() override { return this->get_override("categories")(); } -}; - - -struct IPluginListWrapper: MOBase::IPluginList, boost::python::wrapper { - virtual PluginStates state(const QString &name) const { return this->get_override("state")(name); } - virtual int priority(const QString &name) const { return this->get_override("priority")(name); } - virtual int loadOrder(const QString &name) const { return this->get_override("loadOrder")(name); } - virtual bool isMaster(const QString &name) const { return this->get_override("isMaster")(name); } - virtual QStringList masters(const QString &name) const { return this->get_override("masters")(name); } - virtual QString origin(const QString &name) const { return this->get_override("origin")(name); } - virtual bool onRefreshed(const std::function &callback) { return this->get_override("onRefreshed")(callback); } - virtual bool onPluginMoved(const std::function &callback) { return this->get_override("onPluginMoved")(callback); } - virtual bool onPluginStateChanged(const std::function &callback) override { return this->get_override("onPluginStateChanged")(callback); } - virtual QStringList pluginNames() const override { return this->get_override("pluginNames")(); } - virtual void setState(const QString &name, PluginStates state) override { this->get_override("setState")(name, state); } - virtual void setLoadOrder(const QStringList &pluginList) override { this->get_override("setLoadOrder")(pluginList); } -}; - - -struct IModListWrapper: MOBase::IModList, boost::python::wrapper { - virtual QString displayName(const QString &internalName) const override { return this->get_override("displayName")(internalName); } - virtual QStringList allMods() const override { return this->get_override("allMods")(); } - virtual ModStates state(const QString &name) const override { return this->get_override("state")(name); } - virtual int priority(const QString &name) const override { return this->get_override("priority")(name); } - virtual bool setActive(const QString &name, bool active) override { return this->get_override("setActive")(name, active); } - virtual bool setPriority(const QString &name, int newPriority) override { return this->get_override("setPriority")(name, newPriority); } - virtual bool onModStateChanged(const std::function &func) override { return this->get_override("onModStateChanged")(func); } - virtual bool onModMoved(const std::function &func) override { return this->get_override("onModMoved")(func); } -}; - - -// This needs to be extendable in Python, so actually needs a wrapper -// Everything else probably doesn't -class ISaveGameWrapper : public MOBase::ISaveGame, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ISaveGameWrapper"; - using boost::python::wrapper::get_override; - - virtual QString getFilename() const override { return basicWrapperFunctionImplementation(this, "getFilename"); }; - virtual QDateTime getCreationTime() const override { return basicWrapperFunctionImplementation(this, "getCreationTime"); }; - virtual QString getSaveGroupIdentifier() const override { return basicWrapperFunctionImplementation(this, "getSaveGroupIdentifier"); }; - virtual QStringList allFiles() const override { return basicWrapperFunctionImplementation(this, "allFiles"); }; - virtual bool hasScriptExtenderFile() const override { return basicWrapperFunctionImplementation(this, "hasScriptExtenderFile"); }; -}; - - -#endif // UIBASEWRAPPERS_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..0141184e --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +add_subdirectory(python) +add_subdirectory(runner) diff --git a/tests/mocks/DummyFileTree.h b/tests/mocks/DummyFileTree.h new file mode 100644 index 00000000..cf81691b --- /dev/null +++ b/tests/mocks/DummyFileTree.h @@ -0,0 +1,34 @@ +#ifndef DUMMY_TREE_H +#define DUMMY_TREE_H + +#include + +// filetree implementation for testing purpose +// +class DummyFileTree : public MOBase::IFileTree { +public: + DummyFileTree(std::shared_ptr parent, QString name) + : FileTreeEntry(parent, name), IFileTree() + { + } + +protected: + std::shared_ptr makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name); + } + + bool doPopulate(std::shared_ptr, + std::vector>&) const override + { + return true; + } + + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name()); + } +}; + +#endif diff --git a/tests/mocks/MockOrganizer.h b/tests/mocks/MockOrganizer.h new file mode 100644 index 00000000..3ccfa3ca --- /dev/null +++ b/tests/mocks/MockOrganizer.h @@ -0,0 +1,65 @@ +#include +#include + +using namespace MOBase; + +class MockOrganizer : public IOrganizer { +public: + // clang-format off + MOCK_METHOD(IModRepositoryBridge*, createNexusBridge, (), (const, override)); + MOCK_METHOD(QString, instanceName, (), (const, override)); + MOCK_METHOD(QString, profileName, (), (const, override)); + MOCK_METHOD(QString, profilePath, (), (const, override)); + MOCK_METHOD(QString, downloadsPath, (), (const, override)); + MOCK_METHOD(QString, overwritePath, (), (const, override)); + 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)); + MOCK_METHOD(QVariant, pluginSetting, (const QString &pluginName, const QString &key), (const, override)); + MOCK_METHOD(void, setPluginSetting, (const QString &pluginName, const QString &key, const QVariant &value), (override)); + MOCK_METHOD(bool, isPluginEnabled, (const QString& pluginName), (const, override)); + MOCK_METHOD(bool, isPluginEnabled, (IPlugin *plugin), (const, override)); + MOCK_METHOD(QVariant, persistent, (const QString &pluginName, const QString &key, const QVariant &def), (const, override)); + MOCK_METHOD(void, setPersistent, (const QString &pluginName, const QString &key, const QVariant &value, bool sync), (override)); + MOCK_METHOD(QString, pluginDataPath, (), (const, override)); + MOCK_METHOD(IModInterface*, installMod, (const QString &fileName, const QString &nameSuggestion), (override)); + MOCK_METHOD(QString, resolvePath, (const QString &fileName), (const, override)); + MOCK_METHOD(QStringList, listDirectories, (const QString &directoryName), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(QStringList, findFiles, (const QString &path, const QStringList &filter), (const, override)); + MOCK_METHOD(QStringList, getFileOrigins, (const QString &fileName) ,(const, override)); + MOCK_METHOD(QList, findFileInfos, (const QString &path, const std::function &filter), (const, override)); + MOCK_METHOD(std::shared_ptr, virtualFileTree, (), (const, override)); + MOCK_METHOD(MOBase::IInstanceManager*, instanceManager, (), (const, override)); + MOCK_METHOD(MOBase::IDownloadManager*, downloadManager, (), (const, override)); + MOCK_METHOD(MOBase::IPluginList*, pluginList, (), (const, override)); + MOCK_METHOD(MOBase::IModList*, modList, (), (const, override)); + MOCK_METHOD(MOBase::IExecutablesList*, executablesList, (), (const, override)); + MOCK_METHOD(std::shared_ptr, profile, (), (const, override)); + MOCK_METHOD(QStringList, profileNames, (), (const, override)); + MOCK_METHOD(std::shared_ptr, getProfile, (const QString& name), (const, override)); + MOCK_METHOD(MOBase::IGameFeatures*, gameFeatures, (), (const, override)); + MOCK_METHOD(HANDLE, startApplication, (const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite), (override)); + MOCK_METHOD(bool, waitForApplication, (HANDLE handle, bool refresh, LPDWORD exitCode), (const, override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onAboutToRun, (const std::function &func), (override)); + MOCK_METHOD(bool, onFinishedRun, (const std::function &func), (override)); + MOCK_METHOD(void, refresh, (bool saveChanges), (override)); + MOCK_METHOD(MOBase::IPluginGame const *, managedGame, (), (const, override)); + MOCK_METHOD(bool, onUserInterfaceInitialized, (const std::function &), (override)); + MOCK_METHOD(bool, onNextRefresh, (const std::function&, bool), (override)); + MOCK_METHOD(bool, onProfileCreated, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRemoved, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileRenamed, (const std::function&), (override)); + MOCK_METHOD(bool, onProfileChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginSettingChanged, (const std::function &), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginEnabled, (const QString&, const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const std::function&), (override)); + MOCK_METHOD(bool, onPluginDisabled, (const QString&, const std::function&), (override)); + // clang-format on +}; diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt new file mode 100644 index 00000000..f473373d --- /dev/null +++ b/tests/python/CMakeLists.txt @@ -0,0 +1,75 @@ +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) + +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 +) + +set_tests_properties(pytest + PROPERTIES + DEPENDS python-tests + ENVIRONMENT_MODIFICATION + "PYTHONPATH=set:${PYLIB_DIR}\\;$;\ +UIBASE_PATH=set:${UIBASE_PATH}" +) + +mo2_python_pip_install(python-tests + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs + PACKAGES pytest + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_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) + +file(GLOB test_files CONFIGURE_DEPENDS "test_*.cpp") +foreach (test_file ${test_files}) + get_filename_component(target ${test_file} NAME_WLE) + + string(REPLACE "test_" "" pymodule ${target}) + pybind11_add_module(${target} EXCLUDE_FROM_ALL THIN_LTO ${test_file}) + set_target_properties(${target} + PROPERTIES + CXX_STANDARD 23 + OUTPUT_NAME ${pymodule} + FOLDER tests/python + LIBRARY_OUTPUT_DIRECTORY "${PYLIB_DIR}/mobase_tests") + + if(DEFINED CMAKE_CONFIGURATION_TYPES) + foreach(config ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${config} config) + set_target_properties(${target} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY_${config} "${PYLIB_DIR}/mobase_tests") + endforeach() + endif() + + 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) + add_dependencies(python-tests ${target}) +endforeach() diff --git a/tests/python/conftest.py b/tests/python/conftest.py new file mode 100644 index 00000000..99e7800b --- /dev/null +++ b/tests/python/conftest.py @@ -0,0 +1,12 @@ +import os +import sys + +from PyQt6.QtWidgets import QApplication + + +def pytest_configure(): + global app + + os.add_dll_directory(str(os.getenv("UIBASE_PATH"))) + + app = QApplication(sys.argv) diff --git a/tests/python/test_argument_wrapper.cpp b/tests/python/test_argument_wrapper.cpp new file mode 100644 index 00000000..e87f5163 --- /dev/null +++ b/tests/python/test_argument_wrapper.cpp @@ -0,0 +1,61 @@ +#include "pybind11_utils/smart_variant_wrapper.h" + +#include +#include + +#include + +namespace mo2::python::detail { + + template <> + struct smart_variant_converter { + static std::string from(int const& value) { return std::to_string(value); } + }; + + template <> + struct smart_variant_converter { + static int from(std::string const& value) { return std::stoi(value); } + }; + +} // namespace mo2::python::detail + +// wrapper that can be constructed from +using Wrapper = mo2::python::smart_variant; + +template +auto wrap(Fn&& fn) +{ + return mo2::python::wrap_arguments(std::forward(fn)); +} + +std::string fn1(std::string const& value) +{ + return value + "-" + value; +} + +int fn2(int value) +{ + return value * 2; +} + +std::string fn3(int value, std::vector values, std::string const& name) +{ + return name + "-" + std::to_string(value + values.size()); +} + +PYBIND11_MODULE(argument_wrapper, m) +{ + m.def("fn1_raw", &fn1); + m.def("fn1_wrap", wrap(&fn1)); + m.def("fn1_wrap_0", wrap<0>(&fn1)); + + m.def("fn2_raw", &fn2); + m.def("fn2_wrap", wrap(&fn2)); + m.def("fn2_wrap_0", wrap<0>(&fn2)); + + m.def("fn3_raw", &fn3); + m.def("fn3_wrap", wrap(&fn3)); + m.def("fn3_wrap_0", wrap<0>(&fn3)); + m.def("fn3_wrap_2", wrap<2>(&fn3)); + m.def("fn3_wrap_0_2", wrap<0, 2>(&fn3)); +} diff --git a/tests/python/test_argument_wrapper.py b/tests/python/test_argument_wrapper.py new file mode 100644 index 00000000..08b4a1bf --- /dev/null +++ b/tests/python/test_argument_wrapper.py @@ -0,0 +1,62 @@ +import pytest + +import mobase_tests.argument_wrapper as m + + +def test_argument_wrapper_fn1(): + assert m.fn1_raw("hello") == "hello-hello" + + with pytest.raises(TypeError): + m.fn1_raw(1) # pyright: ignore[reportArgumentType] + + assert m.fn1_wrap("hello") == "hello-hello" + assert m.fn1_wrap(32) == "32-32" + + assert m.fn1_wrap_0("world") == "world-world" + assert m.fn1_wrap_0(45) == "45-45" + + +def test_argument_wrapper_fn2(): + assert m.fn2_raw(33) == 66 + + with pytest.raises(TypeError): + m.fn2_raw("12") # pyright: ignore[reportArgumentType] + + assert m.fn2_wrap("15") == 30 + assert m.fn2_wrap(32) == 64 + + assert m.fn2_wrap_0("-15") == -30 + assert m.fn2_wrap_0(45) == 90 + + +def test_argument_wrapper_fn3(): + assert m.fn3_raw(33, [], "hello") == "hello-33" + assert m.fn3_raw(33, [1, 2], "hello") == "hello-35" + + with pytest.raises(TypeError): + m.fn3_raw("12", [], "hello") # pyright: ignore[reportArgumentType] + + with pytest.raises(TypeError): + m.fn3_raw(36, [], 136) # pyright: ignore[reportArgumentType] + + assert m.fn3_wrap(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap("15", [0], "woot") == "woot-16" + assert m.fn3_wrap(17, [], 33) == "33-17" + assert m.fn3_wrap("15", [], 44) == "44-15" + + assert m.fn3_wrap_0_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0_2("15", [0], "woot") == "woot-16" + assert m.fn3_wrap_0_2(17, [], 33) == "33-17" + assert m.fn3_wrap_0_2("15", [], 44) == "44-15" + + assert m.fn3_wrap_0(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_0("15", [], "w00t") == "w00t-15" + + with pytest.raises(TypeError): + m.fn3_wrap_0(14, [], 12) # pyright: ignore[reportArgumentType] + + assert m.fn3_wrap_2(14, [1, 2], "world") == "world-16" + assert m.fn3_wrap_2(15, [], 18) == "18-15" + + with pytest.raises(TypeError): + m.fn3_wrap_2("14", [], 12) # pyright: ignore[reportArgumentType] diff --git a/tests/python/test_filetree.cpp b/tests/python/test_filetree.cpp new file mode 100644 index 00000000..cce43cfb --- /dev/null +++ b/tests/python/test_filetree.cpp @@ -0,0 +1,22 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include +#include + +#include + +using namespace MOBase; + +PYBIND11_MODULE(filetree, m) +{ + // test the FileTypes stuff + m.def("is_file", [](IFileTree::FileTypes const& t) { + return t.testFlag(IFileTree::FILE); + }); + m.def("is_directory", [](IFileTree::FileTypes const& t) { + return t.testFlag(IFileTree::DIRECTORY); + }); + m.def("value", [](IFileTree::FileTypes const& t) { + return t.toInt(); + }); +} diff --git a/tests/python/test_filetree.py b/tests/python/test_filetree.py new file mode 100644 index 00000000..0322faa0 --- /dev/null +++ b/tests/python/test_filetree.py @@ -0,0 +1,86 @@ +from typing import TypeAlias, cast + +import mobase + +import mobase_tests.filetree as m + + +def test_filetype(): + FT = mobase.FileTreeEntry.FileTypes + + assert mobase.FileTreeEntry.FILE == FT.FILE + assert m.is_file(FT.FILE) + assert not m.is_directory(FT.FILE) + assert m.value(FT.FILE) == FT.FILE.value + + assert mobase.FileTreeEntry.DIRECTORY == FT.DIRECTORY + assert not m.is_file(FT.DIRECTORY) + assert m.is_directory(FT.DIRECTORY) + assert m.value(FT.DIRECTORY) == FT.DIRECTORY.value + + assert mobase.FileTreeEntry.FILE_OR_DIRECTORY == FT.FILE_OR_DIRECTORY + assert m.is_file(FT.FILE_OR_DIRECTORY) + assert m.is_directory(FT.FILE_OR_DIRECTORY) + assert m.value(FT.FILE_OR_DIRECTORY) == FT.FILE_OR_DIRECTORY.value + + assert FT.FILE_OR_DIRECTORY == FT.FILE | FT.DIRECTORY + assert m.is_file(FT.FILE | FT.DIRECTORY) + assert m.is_directory(FT.FILE | FT.DIRECTORY) + assert m.value(FT.FILE | FT.DIRECTORY) == (FT.FILE.value | FT.DIRECTORY.value) + + assert m.is_file(FT.FILE_OR_DIRECTORY & FT.FILE) + assert not m.is_directory(FT.FILE_OR_DIRECTORY & FT.FILE) + + assert not m.is_file(FT.FILE_OR_DIRECTORY & FT.DIRECTORY) + assert m.is_directory(FT.FILE_OR_DIRECTORY & FT.DIRECTORY) + + assert m.is_file(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY) + assert not m.is_directory(FT.FILE_OR_DIRECTORY & ~FT.DIRECTORY) + + +_tree_values: TypeAlias = list["str | tuple[str, _tree_values]"] + + +def make_tree( + values: _tree_values, root: mobase.IFileTree | None = None +) -> mobase.IFileTree: + if root is None: + root = cast(mobase.IFileTree, mobase.private.makeTree()) # type: ignore + + for value in values: + if isinstance(value, str): + root.addFile(value) + else: + sub_tree = root.addDirectory(value[0]) + make_tree(value[1], sub_tree) + + return root + + +def test_walk(): + tree = make_tree( + [("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])] + ) + + assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == { + e.path("/") for e in tree.walk() + } + + entries: list[str] = [] + for e in tree.walk(): + if e.name() == "e": + break + entries.append(e.path("/")) + assert {"a", "b", "b/u", "b/v"} == set(entries) + + +def test_glob(): + tree = make_tree( + [("a", []), ("b", ["u", "v"]), "c.x", "d.y", ("e", [("q", ["c.t", ("p", [])])])] + ) + + assert {"a", "b", "b/u", "b/v", "c.x", "d.y", "e", "e/q", "e/q/c.t", "e/q/p"} == { + e.path("/") for e in tree.glob("**/*") + } + + assert {"d.y"} == {e.path("/") for e in tree.glob("**/*.y")} diff --git a/tests/python/test_functional.cpp b/tests/python/test_functional.cpp new file mode 100644 index 00000000..48585162 --- /dev/null +++ b/tests/python/test_functional.cpp @@ -0,0 +1,38 @@ +#include "pybind11_utils/functional.h" + +#include + +PYBIND11_MODULE(functional, m) +{ + m.def("fn_0_arg", [](std::function const& fn) { + return fn(); + }); + + m.def("fn_1_arg", [](std::function const& fn, int a) { + return fn(a); + }); + + m.def("fn_2_arg", [](std::function const& fn, int a, int b) { + return fn(a, b); + }); + + m.def("fn_0_or_1_arg", [](std::function const& fn) { + return fn(); + }); + + m.def("fn_0_or_1_arg", [](std::function const& fn) { + return fn(1); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1, 2); + }); + + m.def("fn_1_or_2_or_3_arg", [](std::function const& fn) { + return fn(1, 2, 3); + }); +} diff --git a/tests/python/test_functional.py b/tests/python/test_functional.py new file mode 100644 index 00000000..f8da087e --- /dev/null +++ b/tests/python/test_functional.py @@ -0,0 +1,55 @@ +from typing import Any + +import pytest + +import mobase_tests.functional as m + + +def test_guessed_string(): + # available functions: + # - fn_0_arg, fn_1_arg, fn_2_arg + # - fn_0_or_1_arg, fn_1_or_2_or_3_arg + + def no_args() -> int: + return 0 + + def len_of_args(*args: int) -> int: + return len(args) + + def len_of_args_tweaked(x: int, *args: int, **kwargs: Any) -> int: + return x + len(args) + + def sum_of_args(*args: int) -> int: + return sum(args) + + assert m.fn_0_arg(lambda: 0) == 0 + assert m.fn_0_arg(lambda: 5) == 5 + assert m.fn_0_arg(lambda x=2: x) == 2 + assert m.fn_0_arg(len_of_args) == 0 + + assert m.fn_1_arg(lambda x: x, 4) == 4 + assert m.fn_1_arg(sum_of_args, 8) == 8 + assert m.fn_1_arg(lambda x=2, y=4: x + y, 3) == 7 + assert m.fn_1_arg(len_of_args_tweaked, 5) == 5 + + assert m.fn_2_arg(lambda x, y: x * y, 4, 5) == 20 + assert m.fn_2_arg(lambda x, y=3: x * y, 4, 2) == 8 + assert m.fn_2_arg(sum_of_args, 8, 9) == 17 + assert m.fn_2_arg(lambda x=2, y=4: x + y, 3, 3) == 6 + assert m.fn_2_arg(len_of_args_tweaked, 5, 2) == 6 + + assert m.fn_0_or_1_arg(lambda: 3) == 3 + assert m.fn_0_or_1_arg(lambda x: x) == 1 + + # the 0 arg is bound first, both are possible, the 0 arg is chosen + assert m.fn_0_or_1_arg(lambda x=2: x) == 2 + + with pytest.raises(TypeError): + m.fn_1_or_2_or_3_arg(no_args) # pyright: ignore[reportArgumentType, reportCallIssue] + + assert m.fn_1_or_2_or_3_arg(lambda x=4: x) == 1 + assert m.fn_1_or_2_or_3_arg(lambda x, y: x + y) == 3 # 1 + 2 + assert m.fn_1_or_2_or_3_arg(lambda x, y, z: x * y * z) == 6 # 1 * 2 * 3 + + # the 1 arg is bound first + assert m.fn_1_or_2_or_3_arg(sum_of_args) == 1 diff --git a/tests/python/test_guessed_string.cpp b/tests/python/test_guessed_string.cpp new file mode 100644 index 00000000..5e76773d --- /dev/null +++ b/tests/python/test_guessed_string.cpp @@ -0,0 +1,34 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include +#include + +#include + +using namespace MOBase; + +PYBIND11_MODULE(guessed_string, m) +{ + m.def("get_value", [](GuessedValue const& value) { + return value.operator const QString&(); + }); + m.def("get_variants", [](GuessedValue const& value) { + return value.variants(); + }); + + m.def("set_from_callback", + [](GuessedValue& value, + std::function&)> const& fn) { + fn(value); + }); + + // note: the function needs to take the guessed string by pointer if constructed + // from C++, this is to be taken into account when calling Python function (cf. + // installers) + m.def("get_from_callback", + [](std::function*)> const& fn) { + GuessedValue value; + fn(&value); + return (QString)value; + }); +} diff --git a/tests/python/test_guessed_string.py b/tests/python/test_guessed_string.py new file mode 100644 index 00000000..204e1f15 --- /dev/null +++ b/tests/python/test_guessed_string.py @@ -0,0 +1,36 @@ +import mobase + +import mobase_tests.guessed_string as m + + +def test_guessed_string(): + # empty string + gs = mobase.GuessedString() + + assert len(gs.variants()) == 0 + assert not gs.variants() + assert str(gs) == "" + + # automatic conversion from string + assert m.get_value("test") == "test" + assert m.get_variants("test") == {"test"} + + # update + gs = mobase.GuessedString("fallback", mobase.GuessQuality.FALLBACK) + assert str(gs) == "fallback" + + gs.update("good", mobase.GuessQuality.GOOD) + assert str(gs) == "good" + assert gs.variants() == {"fallback", "good"} + + # back-and-forth + gs = mobase.GuessedString() + assert str(gs) == "" + + def _update(gs: mobase.GuessedString): + gs.update("test") + + m.set_from_callback(gs, _update) + assert str(gs) == "test" + + assert m.get_from_callback(_update) == "test" diff --git a/tests/python/test_organizer.cpp b/tests/python/test_organizer.cpp new file mode 100644 index 00000000..ea54c0dd --- /dev/null +++ b/tests/python/test_organizer.cpp @@ -0,0 +1,46 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using ::testing::NiceMock; + +PYBIND11_MODULE(organizer, m) +{ + using ::testing::_; + using ::testing::Eq; + using ::testing::Return; + + 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; + } + }); + + return mock; + }); +} diff --git a/tests/python/test_organizer.py b/tests/python/test_organizer.py new file mode 100644 index 00000000..3ac42d28 --- /dev/null +++ b/tests/python/test_organizer.py @@ -0,0 +1,14 @@ +import pytest + +import mobase + +m = pytest.importorskip("mobase_tests.organizer") + + +def test_getters(): + o: mobase.IOrganizer = m.organizer() + assert o.profileName() == "profile" + assert o.startApplication("valid.exe") == 4654 + assert o.startApplication("invalid.exe") == mobase.INVALID_HANDLE_VALUE + assert o.waitForApplication(42) == (False, -1) + assert o.waitForApplication(4654) == (True, 0) diff --git a/tests/python/test_path_wrappers.py b/tests/python/test_path_wrappers.py new file mode 100644 index 00000000..5a0d7019 --- /dev/null +++ b/tests/python/test_path_wrappers.py @@ -0,0 +1,45 @@ +import sys +from pathlib import Path + +import pytest +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + + +def test_filepath_wrappers(): + # TBC that this works everywhere + version = ".".join(map(str, sys.version_info[:3])) + + # from string, ok + assert mobase.getProductVersion(sys.executable) == version + + # from path, ok + assert mobase.getProductVersion(Path(sys.executable)) == version + + # from QDir, ko + with pytest.raises(TypeError): + mobase.getProductVersion(QDir(sys.executable)) # pyright: ignore[reportArgumentType] + + +def test_executableinfo(): + info = mobase.ExecutableInfo("exe", QFileInfo(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", sys.executable) + assert info.binary() == QFileInfo(sys.executable) + + info = mobase.ExecutableInfo("exe", Path(sys.executable)) + assert info.binary() == QFileInfo(sys.executable) + + info.withWorkingDirectory(Path(__file__).parent) + assert info.workingDirectory() == QFileInfo(__file__).dir() + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(Path(".")) + assert info.workingDirectory() == QDir(".") + + info.withWorkingDirectory(".") + assert info.workingDirectory() == QDir(".") diff --git a/tests/python/test_qt.cpp b/tests/python/test_qt.cpp new file mode 100644 index 00000000..5ef1f008 --- /dev/null +++ b/tests/python/test_qt.cpp @@ -0,0 +1,155 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +PYBIND11_MODULE(qt, m) +{ + // QString + + m.def("create_qstring_with_emoji", []() { + return QString::fromUtf16(u"\U0001F600"); + }); + + m.def("consume_qstring_with_emoji", [](QString const& qstring) { + return qstring.length(); + }); + + m.def("qstring_to_stdstring", [](QString const& qstring) { + return qstring.toStdString(); + }); + + m.def("stdstring_to_qstring", [](std::string const& string) { + return QString::fromStdString(string); + }); + m.def("qstring_to_int", [](QString const& qstring) { + return qstring.toInt(); + }); + m.def("int_to_qstring", [](int value) { + return QString::number(value); + }); + + // QStringList + + m.def("qstringlist_join", [](QStringList const& values, QString const& sep) { + return values.join(sep); + }); + + m.def("qstringlist_at", [](QStringList const& values, int index) { + return values.at(index); + }); + + // QMap + + m.def("qmap_to_length", [](QMap const& map) { + QMap res; + for (auto it = map.begin(); it != map.end(); ++it) { + res[it.key()] = it.value().size(); + } + return res; + }); + + // QDateTime + + m.def( + "datetime_from_string", + [](QString const& date, Qt::DateFormat format) { + return QDateTime::fromString(date, format); + }, + "string"_a, "format"_a = Qt::DateFormat::ISODate); + + m.def( + "datetime_to_string", + [](QDateTime const& datetime, Qt::DateFormat format) { + return datetime.toString(format); + }, + "datetime"_a, "format"_a = Qt::DateFormat::ISODate); + + // QVariant + + m.def("qvariant_from_none", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::UnknownType, + variant.isValid()); + }); + m.def("qvariant_from_int", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::Int, variant.toInt()); + }); + m.def("qvariant_from_bool", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::Bool, variant.toBool()); + }); + m.def("qvariant_from_str", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QString, + variant.toString()); + }); + m.def("qvariant_from_list", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QVariantList, + variant.toList()); + }); + m.def("qvariant_from_map", [](QVariant const& variant) { + return std::make_tuple(variant.userType() == QMetaType::QVariantMap, + variant.toMap()); + }); + + m.def("qvariant_none", []() { + return QVariant(); + }); + m.def("qvariant_int", []() { + return QVariant(42); + }); + m.def("qvariant_bool", []() { + return QVariant(true); + }); + m.def("qvariant_str", []() { + return QVariant("hello world"); + }); + m.def("qvariant_list", [] { + QVariantMap subMap; + subMap["bar"] = 42; + subMap["moo"] = QVariantList{44, true}; + QVariantList list; + list.push_back(33); + list.push_back(QVariantList{4, "foo"}); + list.push_back(false); + list.push_back("hello"); + list.push_back(QVariant()); + list.push_back(subMap); + list.push_back(45); + return QVariant(list); + }); + m.def("qvariant_map", []() { + QVariantMap map; + map["bar"] = 42; + map["moo"] = true; + map["baz"] = "world hello"; + return map; + }); + + // QFlags + + enum SimpleEnum { Value0 = 0x1, Value1 = 0x2, Value2 = 0x4 }; + Q_DECLARE_FLAGS(SimpleEnumFlags, SimpleEnum); + + py::enum_(m, "SimpleEnum", py::arithmetic()) + .value("Value0", Value0) + .value("Value1", Value1) + .value("Value2", Value2); + + m.def("qflags_explode", [](SimpleEnumFlags const& flags) { + return std::make_tuple(flags.toInt(), flags.testFlag(Value0), + flags.testFlag(Value1), flags.testFlag(Value2)); + }); + m.def("qflags_create", [](bool v0, bool v1, bool v2) { + SimpleEnumFlags r; + if (v0) + r.setFlag(Value0); + if (v1) + r.setFlag(Value1); + if (v2) + r.setFlag(Value2); + return r; + }); +} diff --git a/tests/python/test_qt.py b/tests/python/test_qt.py new file mode 100644 index 00000000..991e33cf --- /dev/null +++ b/tests/python/test_qt.py @@ -0,0 +1,119 @@ +from PyQt6.QtCore import QDateTime, Qt + +import mobase_tests.qt as m + + +def test_qstring(): + assert m.qstring_to_stdstring("test") == "test" + assert m.stdstring_to_qstring("test") == "test" + + assert m.qstring_to_stdstring("éàüö") == "éàüö" + assert m.stdstring_to_qstring("éàüö") == "éàüö" + assert m.qstring_to_stdstring("خالد") == "خالد" + assert m.qstring_to_stdstring("🌎") == "🌎" + + assert m.qstring_to_int("2") == 2 + assert m.int_to_qstring(2) == "2" + + emoji = m.create_qstring_with_emoji() + + assert emoji.encode("utf-16be", "surrogatepass") == b"\xd8\x3d\xde\x00" + assert m.consume_qstring_with_emoji(emoji) == 2 + + assert m.consume_qstring_with_emoji("🌎") == 2 + + +def test_qstringlist(): + assert m.qstringlist_join([""], "--") == "" + assert m.qstringlist_join(["a", "b"], "") == "ab" + assert m.qstringlist_join(["x", "y"], ";") == "x;y" + + assert m.qstringlist_at(["x", "y"], 0) == "x" + assert m.qstringlist_at(["x", "y"], 1) == "y" + + +def test_qmap(): + assert m.qmap_to_length({"t1": "abc", "t2": "o", "t3": ""}) == { + "t1": 3, + "t2": 1, + "t3": 0, + } + + +def test_qdatetime(): + assert m.datetime_from_string("2022-03-01") == QDateTime(2022, 3, 1, 0, 0) + + date = QDateTime(1995, 5, 20, 0, 0) + assert ( + m.datetime_from_string( + date.toString(Qt.DateFormat.TextDate), Qt.DateFormat.TextDate + ) + == date + ) + + assert m.datetime_to_string(date) == "1995-05-20T00:00:00" + assert m.datetime_to_string(date, Qt.DateFormat.TextDate) == date.toString( + Qt.DateFormat.TextDate + ) + + +def test_qvariant(): + # Python -> C++ + + assert m.qvariant_from_none(None) == (True, False) + + assert m.qvariant_from_int(-52) == (True, -52) + assert m.qvariant_from_int(0) == (True, 0) + assert m.qvariant_from_int(33) == (True, 33) + + assert m.qvariant_from_bool(True) == (True, True) + assert m.qvariant_from_bool(False) == (True, False) + + assert m.qvariant_from_str("a string") == (True, "a string") + + assert m.qvariant_from_list([]) == (True, []) + assert m.qvariant_from_list([1, "hello", False]) == (True, [1, "hello", False]) + + assert m.qvariant_from_map({"a": 33, "b": False, "c": ["a", "b"]}) == ( + True, + {"a": 33, "b": False, "c": ["a", "b"]}, + ) + + # C++ -> Python (see .cpp file for the value) + + assert m.qvariant_none() is None + assert m.qvariant_int() == 42 + assert m.qvariant_bool() is True + assert m.qvariant_str() == "hello world" + + assert m.qvariant_map() == {"baz": "world hello", "bar": 42, "moo": True} + + assert m.qvariant_list() == [ + 33, + [4, "foo"], + False, + "hello", + None, + {"bar": 42, "moo": [44, True]}, + 45, + ] + + +def test_qflags(): + v0, v1, v2 = m.SimpleEnum.Value0, m.SimpleEnum.Value1, m.SimpleEnum.Value2 + + assert m.qflags_explode(v0 | v1) == (0x3, True, True, False) + assert m.qflags_explode(v0 | v2) == (0x5, True, False, True) + assert m.qflags_explode(0) == (0, False, False, False) + + assert not (m.qflags_create(False, False, False) & v0) + assert not (m.qflags_create(False, False, False) & v1) + assert not (m.qflags_create(False, False, False) & v2) + + assert m.qflags_create(True, False, False) & v0 + assert m.qflags_create(True, True, False) & v0 + assert m.qflags_create(True, True, False) & v1 + assert not (m.qflags_create(True, True, False) & v2) + + assert m.qflags_create(True, False, False) | v0 == v0 + assert m.qflags_create(True, False, False) | v0 | v2 == v0 | v2 diff --git a/tests/python/test_qt_widgets.cpp b/tests/python/test_qt_widgets.cpp new file mode 100644 index 00000000..6d8a9019 --- /dev/null +++ b/tests/python/test_qt_widgets.cpp @@ -0,0 +1,85 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +QMap s_Widgets; +QWidget* s_Parent; + +class CustomWidget : public QWidget { +public: + CustomWidget(QString const& name, QWidget* parent = nullptr) : QWidget(parent) + { + s_Widgets[name] = this; + setProperty("name", name); + } + + ~CustomWidget() { s_Widgets.remove(property("name").toString()); } +}; + +class PyCustomWidget : public CustomWidget { +public: + using CustomWidget::CustomWidget; + int heightForWidth(int value) const + { + PYBIND11_OVERRIDE(int, CustomWidget, heightForWidth, value); + } +}; + +PYBIND11_MODULE(qt_widgets, m) +{ + s_Parent = new QWidget(); + + py::class_> + pyCustomWidget(m, "CustomWidget"); + pyCustomWidget + .def(py::init(), "name"_a, "parent"_a = (QWidget*)nullptr) + .def("set_parent_cpp", [](CustomWidget* w) { + w->setParent(s_Parent); + }); + py::qt::add_qt_delegate(pyCustomWidget, "_widget"); + + m.def("is_alive", [](QString const& name) { + return s_Widgets.contains(name); + }); + + m.def("get", [](QString const& name) { + return s_Widgets.contains(name) ? s_Widgets[name] : nullptr; + }); + + m.def("set_parent", [](QWidget* widget) { + widget->setParent(s_Parent); + }); + + m.def("is_owned_cpp", [](QString const& name) { + return s_Widgets.contains(name) && s_Widgets[name]->parent() == s_Parent; + }); + + m.def("make_widget_own_cpp", [](QString const& name) -> QWidget* { + return new CustomWidget(name, s_Parent); + }); + + m.def( + "make_widget_own_py", + [](QString const& name) -> QWidget* { + return new CustomWidget(name); + }, + py::return_value_policy::take_ownership); + + // simply passing the widget gives the ownership of the Python object to C++ + m.def("send_to_cpp", [](QString const& name, QWidget* widget) { + widget->setProperty("name", name); + widget->setParent(s_Parent); + s_Widgets[name] = widget; + }); + + m.def("heightForWidth", [](QString const& name, int value) { + return s_Widgets.contains(name) ? s_Widgets[name]->heightForWidth(value) + : -1024; + }); +} diff --git a/tests/python/test_qt_widgets.py b/tests/python/test_qt_widgets.py new file mode 100644 index 00000000..057f34e3 --- /dev/null +++ b/tests/python/test_qt_widgets.py @@ -0,0 +1,62 @@ +from PyQt6.QtWidgets import QWidget + +import mobase_tests.qt_widgets as m + + +class PyWidget(QWidget): + def heightForWidth(self, a0: int) -> int: + return a0 * 3 + 4 + + +class PyCustomWidget(m.CustomWidget): + def __init__(self, name: str): + super().__init__(name) + + def heightForWidth(self, value: int) -> int: + return value * 6 - 5 + + +def test_qt_widget(): + # own cpp + w = m.make_widget_own_cpp("w1") + assert m.is_alive("w1") + assert m.is_owned_cpp("w1") + + del w + assert m.is_alive("w1") + + # own py + w = m.make_widget_own_py("w2") + assert m.is_alive("w2") + assert not m.is_owned_cpp("w2") + + del w + assert not m.is_alive("w2") + + # transfer to C++ + w = PyWidget() + m.send_to_cpp("w3", w) + + # delete the reference w - this should NOT delete the underlying object since it + # was transferred to C++ + del w + assert m.is_alive("w3") + assert m.is_owned_cpp("w3") + + # if the Python object is dead (BAD!), this will crash horrible + assert m.heightForWidth("w3", 4) == 4 * 3 + 4 + + # CustomWidget as a qholder, so the construction itself transfers the ownership + # to C++ + w = PyCustomWidget("w4") + w.set_parent_cpp() + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5 + assert w.heightForWidth(7) == 6 * 7 - 5 + + # can call function not defined and not bound through the delegate + assert not w.hasHeightForWidth() + + del w + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5 diff --git a/tests/python/test_shared_cpp_owner.cpp b/tests/python/test_shared_cpp_owner.cpp new file mode 100644 index 00000000..f24cafda --- /dev/null +++ b/tests/python/test_shared_cpp_owner.cpp @@ -0,0 +1,67 @@ +#include "pybind11_utils/shared_cpp_owner.h" + +#include + +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +class Base; +static std::unordered_map bases; + +class Base { + std::string name_; + +public: + Base(std::string const& name) : name_{name} { bases[name] = this; } + virtual std::string fn() const = 0; + virtual ~Base() { bases.erase(name_); } +}; + +MO2_PYBIND11_SHARED_CPP_HOLDER(Base); + +class CppBase : public Base { +public: + using Base::Base; + std::string fn() const override { return "CppBase::fn()"; } +}; + +class PyBase : public Base { +public: + using Base::Base; + std::string fn() const override { PYBIND11_OVERRIDE_PURE(std::string, Base, fn, ); } +}; + +PYBIND11_MODULE(shared_cpp_owner, m) +{ + static std::shared_ptr base_ptr; + + py::class_>(m, "Base") + .def(py::init()) + .def("fn", &Base::fn); + + m.def("is_alive", [](std::string const& name) { + return bases.find(name) != bases.end(); + }); + + m.def("create", [](std::string const& name) -> std::shared_ptr { + return std::make_shared(name); + }); + m.def("create_and_store", [](std::string const& name) { + base_ptr = std::make_shared(name); + return base_ptr; + }); + m.def("store", [](std::shared_ptr ptr) { + base_ptr = ptr; + }); + m.def("clear", []() { + base_ptr.reset(); + }); + + m.def("call_fn", [](std::string const& name) { + auto it = bases.find(name); + return it != bases.end() ? it->second->fn() : ""; + }); +} diff --git a/tests/python/test_shared_cpp_owner.py b/tests/python/test_shared_cpp_owner.py new file mode 100644 index 00000000..0bcecf75 --- /dev/null +++ b/tests/python/test_shared_cpp_owner.py @@ -0,0 +1,72 @@ +import mobase_tests.shared_cpp_owner as m + + +class PyBase(m.Base): + def __init__(self, name: str, value: int): + super().__init__(name) + self.value = value + + def fn(self): + return f"PyBase.fn({self.value})" + + +def test_shared_cpp_owner_1(): + # create from C++, owned by Python + + # create from C++ + p = m.create("tmp") + assert m.is_alive("tmp") + + # should delete since it's not owner by C++ + del p + assert not m.is_alive("tmp") + + +def test_shared_cpp_owner_2(): + # create from C++, owned by C++ (and Python) + + # create from C++ + p = m.create_and_store("tmp") + assert m.is_alive("tmp") + + # should not delete since it's owned by both C++ and Python + del p + assert m.is_alive("tmp") + + # clear from C++ should free it + m.clear() + assert not m.is_alive("tmp") + + +def test_shared_cpp_owner_3(): + # create from Python, owned by Python + + p = PyBase("foo", 1) + assert m.is_alive("foo") + assert m.call_fn("foo") == "PyBase.fn(1)" + + del p + assert not m.is_alive("foo") + + +def test_shared_cpp_owner_4(): + # create from Python, owned by C++ + + p = PyBase("foo", 2) + assert m.is_alive("foo") + + # send to C++ + m.store(p) + assert m.is_alive("foo") + assert m.call_fn("foo") == "PyBase.fn(2)" + + # delete in Python, should still be alived + del p + assert m.is_alive("foo") + + # should still be able to call fn() + assert m.call_fn("foo") == "PyBase.fn(2)" + + # clear in C++, should kill Python + m.clear() + assert not m.is_alive("foo") diff --git a/tests/runner/CMakeLists.txt b/tests/runner/CMakeLists.txt new file mode 100644 index 00000000..ea17438c --- /dev/null +++ b/tests/runner/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.22) + +# setting-up the tests for the runner is a bit complex because we need a tons of +# things + +# first we configure the tests as with other tests +add_executable(runner-tests EXCLUDE_FROM_ALL) +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) + +# link to runner +target_link_libraries(runner-tests PUBLIC runner) + +# linking to Python - this is not required to get proper linking but required so that +# CMake generator variables will lookup appropriate DLLs for Python and update PATH +# accordingly thanks to mo2_configure_tests +target_link_libraries(runner-tests PUBLIC Python::Python) + +# add mocks +target_include_directories(runner-tests + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mocks) + +set(PYLIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/pylibs) +mo2_python_pip_install(runner-tests + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pylibs + PACKAGES + pytest + PyQt${MO2_QT_VERSION_MAJOR}==${MO2_PYQT_VERSION} + PyQt${MO2_QT_VERSION_MAJOR}-Qt${MO2_QT_VERSION_MAJOR}==${MO2_QT_VERSION}) + +add_dependencies(runner-tests mobase) + +set(PYTHONPATH "${PYLIB_DIR}\\;$\\;${Python_DLL_DIR}\\;${Python_LIB_DIR}") + +set_tests_properties(${runner-tests_gtests} + PROPERTIES + ENVIRONMENT "PLUGIN_DIR=${CMAKE_CURRENT_SOURCE_DIR}/plugins;PYTHONPATH=${PYTHONPATH}" +) diff --git a/tests/runner/plugins/dummy-diagnose.py b/tests/runner/plugins/dummy-diagnose.py new file mode 100644 index 00000000..6f25ef0e --- /dev/null +++ b/tests/runner/plugins/dummy-diagnose.py @@ -0,0 +1,37 @@ +import mobase + + +class DummyDiagnose(mobase.IPluginDiagnose): + def activeProblems(self) -> list[int]: + return [1, 2] + + def shortDescription(self, key: int) -> str: + return f"short-{key}" + + def fullDescription(self, key: int) -> str: + return f"long-{key}" + + def hasGuidedFix(self, key: int) -> bool: + return key == 1 + + +class DummyDiagnoseAndGame(mobase.IPluginDiagnose, mobase.IPluginGame): + def __init__(self): + mobase.IPluginDiagnose.__init__(self) + mobase.IPluginGame.__init__(self) + + def activeProblems(self) -> list[int]: + return [5, 7] + + def shortDescription(self, key: int) -> str: + return f"short-{key}" + + def fullDescription(self, key: int) -> str: + return f"long-{key}" + + def hasGuidedFix(self, key: int) -> bool: + return key == 7 + + +def createPlugins() -> list[mobase.IPlugin]: + return [DummyDiagnose(), DummyDiagnoseAndGame()] # type: ignore diff --git a/tests/runner/plugins/dummy-filemapper.py b/tests/runner/plugins/dummy-filemapper.py new file mode 100644 index 00000000..1993b556 --- /dev/null +++ b/tests/runner/plugins/dummy-filemapper.py @@ -0,0 +1,32 @@ +import mobase + + +class DummyFileMapper(mobase.IPluginFileMapper): + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping( + source="the source", destination="the destination", is_directory=False + ), + mobase.Mapping( + source="the other source", + destination="the other destination", + is_directory=True, + ), + ] + + +class DummyFileMapperAndGame(mobase.IPluginFileMapper, mobase.IPluginGame): + def __init__(self): + mobase.IPluginFileMapper.__init__(self) + mobase.IPluginGame.__init__(self) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping( + source="the source", destination="the destination", is_directory=False + ), + ] + + +def createPlugins() -> list[mobase.IPlugin]: + return [DummyFileMapper(), DummyFileMapperAndGame()] # type: ignore diff --git a/tests/runner/plugins/dummy-game.py b/tests/runner/plugins/dummy-game.py new file mode 100644 index 00000000..e17c9b88 --- /dev/null +++ b/tests/runner/plugins/dummy-game.py @@ -0,0 +1,45 @@ +from collections.abc import Sequence + +from PyQt6.QtWidgets import QWidget + +import mobase + + +class DummyModDataChecker(mobase.ModDataChecker): + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + return mobase.ModDataChecker.VALID + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + return filetree + + +class DummySaveGameInfo(mobase.SaveGameInfo): + def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]: + return {} + + def getSaveGameWidget(self, parent: QWidget) -> mobase.ISaveGameInfoWidget | None: + return None + + +# we do not implement everything since this will do fine if we do not call anything +# from the C++ side +# +class DummyGame(mobase.IPluginGame): + _features: dict[type, object] + + def __init__(self): + super().__init__() + + self._features = { + mobase.ModDataChecker: DummyModDataChecker(), + mobase.SaveGameInfo: DummySaveGameInfo(), + } + + def _featureList(self) -> dict[type, object]: + return self._features + + +def createPlugin() -> mobase.IPlugin: + return DummyGame() # type: ignore diff --git a/tests/runner/plugins/dummy-installer.py b/tests/runner/plugins/dummy-installer.py new file mode 100644 index 00000000..3c4e67e6 --- /dev/null +++ b/tests/runner/plugins/dummy-installer.py @@ -0,0 +1,46 @@ +# -*- encoding: utf-8 -*- + +from typing import Union, cast + +import mobase + + +# we do not implement everything since this will do fine if we do not call anything +# from the C++ side +# +class DummyInstaller(mobase.IPluginInstallerSimple): + def isManualInstaller(self) -> bool: + return False + + def priority(self) -> int: + return 10 + + def isArchiveSupported(self, tree: mobase.IFileTree) -> bool: + return tree.find("needed-file.txt") is not None + + def install( + self, + name: mobase.GuessedString, + tree: mobase.IFileTree, + version: str, + nexus_id: int, + ) -> Union[ + mobase.InstallResult, + mobase.IFileTree, + tuple[mobase.InstallResult, mobase.IFileTree, str, int], + ]: + if tree.find("needed-file.txt") is None: + return mobase.InstallResult.FAILED + + if tree.find("extra-file.txt"): + name.update("new name") + new_tree = tree.createOrphanTree() + new_tree.move(tree, "subtree") + cast(mobase.IFileTree, new_tree.find("subtree")).remove("extra-file.txt") + return new_tree + + return (mobase.InstallResult.NOT_ATTEMPTED, tree, "2.4.5", 33) + + +def createPlugin() -> mobase.IPlugin: + return DummyInstaller() # type: ignore diff --git a/tests/runner/plugins/dummy-iplugin.py b/tests/runner/plugins/dummy-iplugin.py new file mode 100644 index 00000000..eaaf6c0a --- /dev/null +++ b/tests/runner/plugins/dummy-iplugin.py @@ -0,0 +1,29 @@ +import mobase + + +class DummyPlugin(mobase.IPlugin): + def init(self, organizer: mobase.IOrganizer) -> bool: + return True + + def author(self) -> str: + return "The Author" + + def name(self) -> str: + return "The Name" + + def description(self) -> str: + return "The Description" + + def version(self) -> mobase.VersionInfo: + return mobase.VersionInfo("1.3.0") + + def settings(self) -> list[mobase.PluginSetting]: + return [ + mobase.PluginSetting( + "a setting", "the setting description", default_value=12 + ) + ] + + +def createPlugin() -> mobase.IPlugin: + return DummyPlugin() diff --git a/tests/runner/test_diagnose.cpp b/tests/runner/test_diagnose.cpp new file mode 100644 index 00000000..467c9cb0 --- /dev/null +++ b/tests/runner/test_diagnose.cpp @@ -0,0 +1,61 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +using ::testing::ElementsAre; + +TEST(IPluginDiagnose, Simple) +{ + const auto plugins_folder = QString(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); + + // load the first IPluginDiagnose + { + IPluginDiagnose* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + ASSERT_THAT(plugin->activeProblems(), ElementsAre(1, 2)); + EXPECT_EQ(plugin->shortDescription(1), "short-1"); + EXPECT_EQ(plugin->fullDescription(1), "long-1"); + EXPECT_TRUE(plugin->hasGuidedFix(1)); + EXPECT_EQ(plugin->shortDescription(2), "short-2"); + EXPECT_EQ(plugin->fullDescription(2), "long-2"); + EXPECT_FALSE(plugin->hasGuidedFix(2)); + } + + // load the second one (this is cast before IPluginGame so should be before) + { + IPluginDiagnose* plugin = qobject_cast(objects[1]); + EXPECT_NE(plugin, nullptr); + + ASSERT_THAT(plugin->activeProblems(), ElementsAre(5, 7)); + EXPECT_EQ(plugin->shortDescription(5), "short-5"); + EXPECT_EQ(plugin->fullDescription(5), "long-5"); + EXPECT_FALSE(plugin->hasGuidedFix(5)); + EXPECT_EQ(plugin->shortDescription(7), "short-7"); + EXPECT_EQ(plugin->fullDescription(7), "long-7"); + EXPECT_TRUE(plugin->hasGuidedFix(7)); + } + + // load the game plugin + { + IPluginGame* plugin = qobject_cast(objects[2]); + EXPECT_NE(plugin, nullptr); + } +} diff --git a/tests/runner/test_filemapper.cpp b/tests/runner/test_filemapper.cpp new file mode 100644 index 00000000..5044283b --- /dev/null +++ b/tests/runner/test_filemapper.cpp @@ -0,0 +1,64 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +TEST(IPluginFileMapper, Simple) +{ + const auto plugins_folder = QString(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); + + // load the first IPluginFileMapper + { + IPluginFileMapper* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + const auto m = plugin->mappings(); + EXPECT_EQ(m.size(), 2); + + EXPECT_EQ(m[0].source, "the source"); + EXPECT_EQ(m[0].destination, "the destination"); + EXPECT_EQ(m[0].isDirectory, false); + EXPECT_EQ(m[0].createTarget, false); + + EXPECT_EQ(m[1].source, "the other source"); + EXPECT_EQ(m[1].destination, "the other destination"); + EXPECT_EQ(m[1].isDirectory, true); + EXPECT_EQ(m[1].createTarget, false); + } + + // load the second one (this is cast before IPluginGame so should be before) + { + IPluginFileMapper* plugin = qobject_cast(objects[1]); + EXPECT_NE(plugin, nullptr); + + const auto m = plugin->mappings(); + EXPECT_EQ(m.size(), 1); + + EXPECT_EQ(m[0].source, "the source"); + EXPECT_EQ(m[0].destination, "the destination"); + EXPECT_EQ(m[0].isDirectory, false); + EXPECT_EQ(m[0].createTarget, false); + } + + // load the game plugin + { + IPluginGame* plugin = qobject_cast(objects[2]); + EXPECT_NE(plugin, nullptr); + } +} diff --git a/tests/runner/test_game.cpp b/tests/runner/test_game.cpp new file mode 100644 index 00000000..0d1d7184 --- /dev/null +++ b/tests/runner/test_game.cpp @@ -0,0 +1,28 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "pythonrunner.h" + +#include + +#include + +#include "MockOrganizer.h" + +using namespace MOBase; + +TEST(IPluginGame, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-game.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + IPluginGame* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); +} diff --git a/tests/runner/test_installer.cpp b/tests/runner/test_installer.cpp new file mode 100644 index 00000000..349c904b --- /dev/null +++ b/tests/runner/test_installer.cpp @@ -0,0 +1,112 @@ +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +#include "DummyFileTree.h" + +using namespace MOBase; + +TEST(IPluginInstaller, Simple) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-installer.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + IPluginInstallerSimple* plugin = qobject_cast(objects[0]); + EXPECT_NE(plugin, nullptr); + + // basic tests + EXPECT_EQ(plugin->priority(), 10); + EXPECT_EQ(plugin->isManualInstaller(), false); + + GuessedValue name{"default name"}; + QString version = "1.0.0"; + int nexus_id = 12; + + // invalid tree + { + std::shared_ptr invalid_tree = + std::make_shared(nullptr, "root"); + invalid_tree->addFile("some file"); + invalid_tree->addDirectory("some directory")->addFile("some other file"); + EXPECT_EQ(plugin->isArchiveSupported(invalid_tree), false); + EXPECT_EQ(plugin->install(name, invalid_tree, version, nexus_id), + IPluginInstaller::RESULT_FAILED); + + // extra arguments stay the same + EXPECT_EQ(name, "default name"); + EXPECT_EQ(version, "1.0.0"); + EXPECT_EQ(nexus_id, 12); + } + + // valid simple tree + { + std::shared_ptr valid_tree = + std::make_shared(nullptr, "root"); + const auto initial_tree = valid_tree; + valid_tree->addFile("needed-file.txt"); + valid_tree->addFile("some file"); + valid_tree->addDirectory("some directory")->addFile("some other file"); + + // valid tree + EXPECT_EQ(plugin->isArchiveSupported(valid_tree), true); + EXPECT_EQ(plugin->install(name, valid_tree, version, nexus_id), + IPluginInstaller::RESULT_NOTATTEMPTED); + + // name is not modified + EXPECT_EQ(name, "default name"); + + // tree is not modified + EXPECT_EQ(valid_tree, initial_tree); + + // version and ID are modified + EXPECT_EQ(version, "2.4.5"); + EXPECT_EQ(nexus_id, 33); + } + + // "complex" tree + { + std::shared_ptr complex_tree = + std::make_shared(nullptr, "root"); + const auto initial_tree = complex_tree; + const auto needed_file = complex_tree->addFile("needed-file.txt"); + const auto extra_file = complex_tree->addFile("extra-file.txt"); + const auto some_file = complex_tree->addFile("some file"); + const auto some_directory = complex_tree->addDirectory("some directory"); + some_directory->addFile("some other file"); + + // valid tree + EXPECT_EQ(plugin->isArchiveSupported(complex_tree), true); + EXPECT_EQ(plugin->install(name, complex_tree, version, nexus_id), + IPluginInstaller::RESULT_SUCCESS); + + // name is modified + EXPECT_EQ(name, "new name"); + + // tree is modified + EXPECT_EQ(complex_tree->findDirectory("subtree")->find("needed-file.txt"), + needed_file); + EXPECT_EQ( + complex_tree->findDirectory("subtree")->findDirectory("extra-file.txt"), + nullptr); + EXPECT_EQ( + complex_tree->findDirectory("subtree")->findDirectory("some directory"), + some_directory); + + // version and ID are not modified (from previous call) + EXPECT_EQ(version, "2.4.5"); + EXPECT_EQ(nexus_id, 33); + } +} diff --git a/tests/runner/test_iplugin.cpp b/tests/runner/test_iplugin.cpp new file mode 100644 index 00000000..27c48293 --- /dev/null +++ b/tests/runner/test_iplugin.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +using namespace MOBase; + +TEST(IPlugin, Basic) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + // load objects + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + EXPECT_EQ(objects.size(), 1); + + // load the IPlugin + const IPlugin* plugin = qobject_cast(objects[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(), 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 new file mode 100644 index 00000000..bf057d3f --- /dev/null +++ b/tests/runner/test_lifetime.cpp @@ -0,0 +1,47 @@ +#include "gtest/gtest.h" + +#include "MockOrganizer.h" +#include "pythonrunner.h" + +#include + +TEST(Lifetime, Plugins) +{ + const auto plugins_folder = QString(std::getenv("PLUGIN_DIR")); + + auto runner = mo2::python::createPythonRunner(); + runner->initialize(); + + { + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + + // we found one plugin + EXPECT_EQ(objects.size(), 1); + + // check that deleting the object actually destroys it + bool destroyed = false; + QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + destroyed = true; + }); + delete objects[0]; + EXPECT_EQ(destroyed, true); + } + + // same things but with a parent + { + QObject* dummy_parent = new QObject(); + const auto objects = runner->load(plugins_folder + "/dummy-iplugin.py"); + + // we found one plugin + EXPECT_EQ(objects.size(), 1); + objects[0]->setParent(dummy_parent); + + // check that deleting the object actually destroys it + bool destroyed = false; + QObject::connect(objects[0], &QObject::destroyed, [&destroyed]() { + destroyed = true; + }); + delete dummy_parent; + EXPECT_EQ(destroyed, true); + } +} diff --git a/typings/generate.py b/typings/generate.py new file mode 100644 index 00000000..1983186e --- /dev/null +++ b/typings/generate.py @@ -0,0 +1,38 @@ +import os +import site +import sys +from pathlib import Path +from typing import cast + +import pybind11_stubgen as py11stubs + +typings_dir = Path(__file__).parent +mobase_tests_dir = Path(__file__).parent.parent.joinpath( + "vsbuild", "tests", "python", "pylibs", "mobase_tests" +) + +site.addsitedir(str(mobase_tests_dir.parent)) + +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 # noqa: E402 + +app = QApplication(sys.argv) + +args = py11stubs.arg_parser().parse_args(["dummy"], namespace=py11stubs.CLIArgs()) + +parser = py11stubs.stub_parser_from_args(args) +printer = py11stubs.Printer(invalid_expr_as_ellipses=True) # type: ignore + +for path in mobase_tests_dir.glob("*.pyd"): + name = path.name.split(".")[0] + py11stubs.run( + parser, + printer, + f"mobase_tests.{name}", + typings_dir.joinpath("mobase_tests"), + sub_dir=None, + dry_run=False, + writer=py11stubs.Writer(stub_ext="pyi"), # type: ignore + ) diff --git a/typings/mobase_tests/argument_wrapper.pyi b/typings/mobase_tests/argument_wrapper.pyi new file mode 100644 index 00000000..a96e49af --- /dev/null +++ b/typings/mobase_tests/argument_wrapper.pyi @@ -0,0 +1,27 @@ +from __future__ import annotations + +__all__ = [ + "fn1_raw", + "fn1_wrap", + "fn1_wrap_0", + "fn2_raw", + "fn2_wrap", + "fn2_wrap_0", + "fn3_raw", + "fn3_wrap", + "fn3_wrap_0", + "fn3_wrap_0_2", + "fn3_wrap_2", +] + +def fn1_raw(arg0: str) -> str: ... +def fn1_wrap(arg0: int | str) -> str: ... +def fn1_wrap_0(arg0: int | str) -> str: ... +def fn2_raw(arg0: int) -> int: ... +def fn2_wrap(arg0: int | str) -> int: ... +def fn2_wrap_0(arg0: int | str) -> int: ... +def fn3_raw(arg0: int, arg1: list[int], arg2: str) -> str: ... +def fn3_wrap(arg0: int | str, arg1: list[int], arg2: int | str) -> str: ... +def fn3_wrap_0(arg0: int | str, arg1: list[int], arg2: str) -> str: ... +def fn3_wrap_0_2(arg0: int | str, arg1: list[int], arg2: int | str) -> str: ... +def fn3_wrap_2(arg0: int, arg1: list[int], arg2: int | str) -> str: ... diff --git a/typings/mobase_tests/filetree.pyi b/typings/mobase_tests/filetree.pyi new file mode 100644 index 00000000..93eab63e --- /dev/null +++ b/typings/mobase_tests/filetree.pyi @@ -0,0 +1,9 @@ +from __future__ import annotations + +import mobase + +__all__ = ["is_directory", "is_file", "value"] + +def is_directory(arg0: mobase.IFileTree.FileTypes) -> bool: ... +def is_file(arg0: mobase.IFileTree.FileTypes) -> bool: ... +def value(arg0: mobase.IFileTree.FileTypes) -> int: ... diff --git a/typings/mobase_tests/functional.pyi b/typings/mobase_tests/functional.pyi new file mode 100644 index 00000000..7e58f0ea --- /dev/null +++ b/typings/mobase_tests/functional.pyi @@ -0,0 +1,19 @@ +from __future__ import annotations + +import typing + +__all__ = ["fn_0_arg", "fn_0_or_1_arg", "fn_1_arg", "fn_1_or_2_or_3_arg", "fn_2_arg"] + +def fn_0_arg(arg0: typing.Callable[[], int]) -> int: ... +@typing.overload +def fn_0_or_1_arg(arg0: typing.Callable[[], int]) -> int: ... +@typing.overload +def fn_0_or_1_arg(arg0: typing.Callable[[int], int]) -> int: ... +def fn_1_arg(arg0: typing.Callable[[int], int], arg1: int) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int], int]) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int, int], int]) -> int: ... +@typing.overload +def fn_1_or_2_or_3_arg(arg0: typing.Callable[[int, int, int], int]) -> int: ... +def fn_2_arg(arg0: typing.Callable[[int, int], int], arg1: int, arg2: int) -> int: ... diff --git a/typings/mobase_tests/guessed_string.pyi b/typings/mobase_tests/guessed_string.pyi new file mode 100644 index 00000000..a03d9b6e --- /dev/null +++ b/typings/mobase_tests/guessed_string.pyi @@ -0,0 +1,16 @@ +from __future__ import annotations + +import typing + +import mobase + +__all__ = ["get_from_callback", "get_value", "get_variants", "set_from_callback"] + +def get_from_callback( + arg0: typing.Callable[[mobase.GuessedString], None], +) -> str: ... +def get_value(arg0: str | mobase.GuessedString) -> str: ... +def get_variants(arg0: str | mobase.GuessedString) -> set[str]: ... +def set_from_callback( + arg0: mobase.GuessedString, arg1: typing.Callable[[mobase.GuessedString], None] +) -> None: ... diff --git a/typings/mobase_tests/organizer.pyi b/typings/mobase_tests/organizer.pyi new file mode 100644 index 00000000..dd5136a4 --- /dev/null +++ b/typings/mobase_tests/organizer.pyi @@ -0,0 +1,7 @@ +from __future__ import annotations + +import mobase + +__all__ = ["organizer"] + +def organizer() -> mobase.IOrganizer: ... diff --git a/typings/mobase_tests/qt.pyi b/typings/mobase_tests/qt.pyi new file mode 100644 index 00000000..088ef2a2 --- /dev/null +++ b/typings/mobase_tests/qt.pyi @@ -0,0 +1,109 @@ +from __future__ import annotations + +import typing + +import PyQt6.QtCore + +from mobase import MoVariant + +__all__ = [ + "SimpleEnum", + "consume_qstring_with_emoji", + "create_qstring_with_emoji", + "datetime_from_string", + "datetime_to_string", + "int_to_qstring", + "qflags_create", + "qflags_explode", + "qmap_to_length", + "qstring_to_int", + "qstring_to_stdstring", + "qstringlist_at", + "qstringlist_join", + "qvariant_bool", + "qvariant_from_bool", + "qvariant_from_int", + "qvariant_from_list", + "qvariant_from_map", + "qvariant_from_none", + "qvariant_from_str", + "qvariant_int", + "qvariant_list", + "qvariant_map", + "qvariant_none", + "qvariant_str", + "stdstring_to_qstring", +] + +class SimpleEnum: + """ + Members: + + Value0 + + Value1 + + Value2 + """ + + Value0: typing.ClassVar[SimpleEnum] # value = + Value1: typing.ClassVar[SimpleEnum] # value = + Value2: typing.ClassVar[SimpleEnum] # value = + __members__: typing.ClassVar[ + dict[str, SimpleEnum] + ] # value = {'Value0': , 'Value1': , 'Value2': } + def __and__(self, other: typing.Any) -> typing.Any: ... + def __eq__(self, other: typing.Any) -> bool: ... + def __ge__(self, other: typing.Any) -> bool: ... + def __getstate__(self) -> int: ... + def __gt__(self, other: typing.Any) -> bool: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __init__(self, value: int) -> None: ... + def __int__(self) -> int: ... + def __invert__(self) -> typing.Any: ... + def __le__(self, other: typing.Any) -> bool: ... + def __lt__(self, other: typing.Any) -> bool: ... + def __ne__(self, other: typing.Any) -> bool: ... + def __or__(self, other: typing.Any) -> typing.Any: ... + def __rand__(self, other: typing.Any) -> typing.Any: ... + def __repr__(self) -> str: ... + def __ror__(self, other: typing.Any) -> typing.Any: ... + def __rxor__(self, other: typing.Any) -> typing.Any: ... + def __setstate__(self, state: int) -> None: ... + def __str__(self) -> str: ... + def __xor__(self, other: typing.Any) -> typing.Any: ... + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... + +def consume_qstring_with_emoji(arg0: str) -> int: ... +def create_qstring_with_emoji() -> str: ... +def datetime_from_string( + string: str, format: PyQt6.QtCore.Qt.DateFormat = ... +) -> PyQt6.QtCore.QDateTime: ... +def datetime_to_string( + datetime: PyQt6.QtCore.QDateTime, format: PyQt6.QtCore.Qt.DateFormat = ... +) -> str: ... +def int_to_qstring(arg0: int) -> str: ... +def qflags_create(arg0: bool, arg1: bool, arg2: bool) -> int: ... +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 qstringlist_at(arg0: typing.Sequence[str], arg1: int) -> str: ... +def qstringlist_join(arg0: typing.Sequence[str], arg1: str) -> str: ... +def qvariant_bool() -> MoVariant: ... +def qvariant_from_bool(arg0: MoVariant) -> tuple[bool, bool]: ... +def qvariant_from_int(arg0: MoVariant) -> tuple[bool, int]: ... +def qvariant_from_list(arg0: MoVariant) -> tuple[bool, typing.Sequence[MoVariant]]: ... +def qvariant_from_map(arg0: MoVariant) -> tuple[bool, dict[str, MoVariant]]: ... +def qvariant_from_none(arg0: MoVariant) -> tuple[bool, bool]: ... +def qvariant_from_str(arg0: MoVariant) -> tuple[bool, str]: ... +def qvariant_int() -> MoVariant: ... +def qvariant_list() -> MoVariant: ... +def qvariant_map() -> dict[str, MoVariant]: ... +def qvariant_none() -> MoVariant: ... +def qvariant_str() -> MoVariant: ... +def stdstring_to_qstring(arg0: str) -> str: ... diff --git a/typings/mobase_tests/qt_widgets.pyi b/typings/mobase_tests/qt_widgets.pyi new file mode 100644 index 00000000..dfda4f9b --- /dev/null +++ b/typings/mobase_tests/qt_widgets.pyi @@ -0,0 +1,34 @@ +from __future__ import annotations + +import typing + +import PyQt6.QtWidgets + +__all__ = [ + "CustomWidget", + "get", + "heightForWidth", + "is_alive", + "is_owned_cpp", + "make_widget_own_cpp", + "make_widget_own_py", + "send_to_cpp", + "set_parent", +] + +class CustomWidget: + def __getattr__(self, arg0: str) -> typing.Any: ... + def __init__( + self, name: str, parent: PyQt6.QtWidgets.QWidget | None = None + ) -> None: ... + def _widget(self) -> PyQt6.QtWidgets.QWidget: ... + def set_parent_cpp(self) -> None: ... + +def get(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def heightForWidth(arg0: str, arg1: int) -> int: ... +def is_alive(arg0: str) -> bool: ... +def is_owned_cpp(arg0: str) -> bool: ... +def make_widget_own_cpp(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def make_widget_own_py(arg0: str) -> PyQt6.QtWidgets.QWidget: ... +def send_to_cpp(arg0: str, arg1: PyQt6.QtWidgets.QWidget) -> None: ... +def set_parent(arg0: PyQt6.QtWidgets.QWidget) -> None: ... diff --git a/typings/mobase_tests/shared_cpp_owner.pyi b/typings/mobase_tests/shared_cpp_owner.pyi new file mode 100644 index 00000000..789632ee --- /dev/null +++ b/typings/mobase_tests/shared_cpp_owner.pyi @@ -0,0 +1,22 @@ +from __future__ import annotations + +__all__ = [ + "Base", + "call_fn", + "clear", + "create", + "create_and_store", + "is_alive", + "store", +] + +class Base: + def __init__(self, arg0: str) -> None: ... + def fn(self) -> str: ... + +def call_fn(arg0: str) -> str: ... +def clear() -> None: ... +def create(arg0: str) -> Base: ... +def create_and_store(arg0: str) -> Base: ... +def is_alive(arg0: str) -> bool: ... +def store(arg0: Base) -> None: ... diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000..db9eb087 --- /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": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "294f76666c3000630d828703e675814c05a4fd43", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "228cda39fe9d1eeed789c0ef64fd1235dab3b11e", + "packages": ["mo2-*", "pybind11", "spdlog"] + } + ] + } +}