From 7f93d4f77ba040e1b45df93b05a7d3d03d07156c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 9 Jun 2024 12:26:22 +0200 Subject: [PATCH] Update following uibase change for game features. (#5) --- .github/workflows/linters.yml | 22 ++- plugin-requirements.txt | 2 +- poetry.lock | 220 +++++++++++++++++++++++++ pyproject.toml | 62 ++++++++ requirements.txt | 7 - setup.cfg | 54 ------- src/__init__.py | 3 +- src/dialog.py | 291 +++++++++++++++++++--------------- src/installer.py | 171 ++++++++++---------- src/installer_wizard_en.ts | 34 +--- src/runner.py | 60 +++---- src/utils.py | 54 +++---- 12 files changed, 595 insertions(+), 385 deletions(-) create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.cfg diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 07f015d..9299e91 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -8,17 +8,15 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.8] + python-version: [3.12] steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Test with tox - run: tox -e py38-lint + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: poetry install + - name: Lint + run: poetry run poe lint diff --git a/plugin-requirements.txt b/plugin-requirements.txt index 7ce0072..5203390 100644 --- a/plugin-requirements.txt +++ b/plugin-requirements.txt @@ -1 +1 @@ -bain-wizard-interpreter==1.0.3 +bain-wizard-interpreter==1.0.4 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3ce7787 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,220 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.1" +description = "ANTLR 4.13.1 runtime for Python 3" +optional = false +python-versions = "*" +files = [ + {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, + {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, +] + +[[package]] +name = "bain-wizard-interpreter" +version = "1.0.4" +description = "BAIN Wizard Interpreter based on wizparse." +optional = false +python-versions = "<4.0,>=3.11.1" +files = [ + {file = "bain_wizard_interpreter-1.0.4-py3-none-any.whl", hash = "sha256:c6a084349caa6fd80d8fbf222869fb63df8ede337edbb8774dff557ae2a981bc"}, + {file = "bain_wizard_interpreter-1.0.4.tar.gz", hash = "sha256:4ebd36112690775bab6c921c90422c9e5432901ca90860f36c5ba0153bb82079"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.13.1,<5.0.0" +chardet = ">=5.2.0,<6.0.0" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[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.0" +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.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, + {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, +] + +[[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 = "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 = "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-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.1" +description = "The subset of a Qt installation needed by PyQt6." +optional = false +python-versions = "*" +files = [ + {file = "PyQt6_Qt6-6.7.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:29622b5dd38740b4b6962e0c88d082d08fa10b64542ef5d911b04214aad70150"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbcfe66a57199c6a26542b8b9b2f2ee59d974db36293de335a1251f24c4fe917"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9fbab2a96d72d77d16021e259ef86a1a3c87adb0e7eebcc92df0d39f3fdf7e27"}, + {file = "PyQt6_Qt6-6.7.1-py3-none-win_amd64.whl", hash = "sha256:590a2f30d15892b5259e6a17ecc0a755675b1b49f553d964e195e90094b44120"}, +] + +[[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.365" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.365-py3-none-any.whl", hash = "sha256:194d767a039f9034376b7ec8423841880ac6efdd061f3e283b4ad9fcd484a659"}, + {file = "pyright-1.1.365.tar.gz", hash = "sha256:d7e69000939aed4bf823707086c30c84c005bdd39fac2dfb370f0e5be16c2ef2"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[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"}, +] + +[[package]] +name = "types-psutil" +version = "5.9.5.20240516" +description = "Typing stubs for psutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-psutil-5.9.5.20240516.tar.gz", hash = "sha256:bb296f59fc56458891d0feb1994717e548a1bcf89936a2877df8792b822b4696"}, + {file = "types_psutil-5.9.5.20240516-py3-none-any.whl", hash = "sha256:83146ded949a10167d9895e567b3b71e53ebc5e23fd8363eab62b3c76cce7b89"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "e3a9cd106c3a545c4913ccf1956733525b3fe61323063bece381f6834b4ac0a4" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..876285b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[tool.poetry] +name = "installer-wizard" +version = "1.0.3" +description = "" +authors = ["Mikaƫl Capelle "] +license = "MIT" +readme = "README.md" + + +[tool.poetry.dependencies] +python = "^3.12" +pyqt6 = "^6.7.0" +bain-wizard-interpreter = "1.0.4" + +[tool.poetry.group.dev.dependencies] +pyright = "^1.1.365" +ruff = "^0.2.1" +types-psutil = "^5.9.5.20240516" +poethepoet = "^0.23.0" +mobase-stubs = { version = "2.5.1a0", allow-prereleases = true } + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poe.tasks] +format-imports = "ruff check --select I . --fix" +format-ruff = "ruff format ." +format.sequence = ["format-imports", "format-ruff"] +lint-ruff = "ruff check ." +lint-ruff-format = "ruff format --check ." +lint-pyright = "pyright ." +lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"] +lint.ignore_fail = "return_non_zero" + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +extend-select = ["B", "Q", "I"] + +[tool.ruff.lint.isort.sections] +mobase = ["mobase"] +wizard = ["wizard"] + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "wizard", + "mobase", + "local-folder", +] + +[tool.pyright] +exclude = ["lib", "venv", "src/ui"] +typeCheckingMode = "strict" +reportMissingTypeStubs = true +reportMissingModuleSource = false diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2469d65..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -black -PyQt6 -mypy -flake8 -flake8-black -mobase-stubs -git+https://github.com/Holt59/bain-wizard-interpreter.git@v0.0.3 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1fa4065..0000000 --- a/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -[flake8] -# Use black line length: -max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, -exclude = lib,ui - -[isort] -profile = black -multi_line_output = 3 -known_mobase = mobase -sections=FUTURE,STDLIB,THIRDPARTY,MOBASE,FIRSTPARTY,LOCALFOLDER - -[mypy] -warn_return_any = True -warn_unused_configs = True -namespace_packages = True - -[mypy-antlr4.*] -ignore_missing_imports = True - -[mypy-.ui.*] -ignore_missing_imports = True - -[mypy-src.ui.*] -ignore_missing_imports = True - -[mypy-wizard.antlr4.*] -ignore_errors = True - -[mypy-pytest] -ignore_missing_imports = True - -[tox:tox] -skipsdist = true -envlist = py310-lint - -[testenv:py310-lint] -skip_install = true -deps = - git+https://github.com/TilmanK/PyQt6-stubs.git - mobase-stubs==2.5.0.dev5 - git+https://github.com/Holt59/bain-wizard-interpreter@v0.0.3 - black - mypy - flake8 - flake8-black - isort -commands = - black src --check --diff --exclude "lib" --exclude "ui" - isort -c src - flake8 src - mypy src diff --git a/src/__init__.py b/src/__init__.py index a0f7c91..3c977b2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,9 +1,8 @@ -# -*- encoding: utf-8 -*- - """ This file is the entry point of the module and must contain a createPlugin() or createPlugins() function. """ +from __future__ import annotations import os import site diff --git a/src/dialog.py b/src/dialog.py index 189b672..417a481 100644 --- a/src/dialog.py +++ b/src/dialog.py @@ -1,13 +1,15 @@ -# -*- encoding: utf-8 -*- +from __future__ import annotations +from collections.abc import Sequence from pathlib import Path -from typing import Any, Dict, List, Mapping, Optional, Set, Tuple +from typing import Any, cast from antlr4 import ParserRuleContext from PyQt6 import QtWidgets from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFontDatabase, QKeySequence, QPixmap, QResizeEvent, QShortcut from PyQt6.QtWidgets import QApplication + from wizard.contexts import ( WizardInterpreterContext, WizardRequireVersionsContext, @@ -37,8 +39,9 @@ WizardRunnerContext = WizardInterpreterContext[WizardRunnerState, Any] def check_version( - context: WizardRequireVersionsContext, organizer: mobase.IOrganizer -) -> Tuple[bool, bool, bool, bool]: + context: WizardRequireVersionsContext[WizardRunnerState], + organizer: mobase.IOrganizer, +) -> tuple[bool, bool, bool, bool]: """ Check if the requirements are ok. @@ -58,10 +61,10 @@ def check_version( if context.game_version: game_ok = mobase.VersionInfo(context.game_version) <= game.version() - # Script extender: + # script extender se_ok = True if context.script_extender_version: - se = game.feature(mobase.ScriptExtender) # type: ignore + se = organizer.gameFeatures().gameFeature(mobase.ScriptExtender) if not se or not se.isInstalled(): se_ok = False else: @@ -72,19 +75,18 @@ def check_version( else: se_ok = False - # Cannot check these so... + # cannot check these so... ge_ok = not context.graphics_extender_version return (game_ok, se_ok, ge_ok, True) class WizardInstallerRequiresVersionPage(QtWidgets.QWidget): - - context: WizardRequireVersionsContext + context: WizardRequireVersionsContext[WizardRunnerState] def __init__( self, - context: WizardRequireVersionsContext, + context: WizardRequireVersionsContext[WizardRunnerState], organizer: mobase.IOrganizer, parent: QtWidgets.QWidget, ): @@ -92,9 +94,9 @@ class WizardInstallerRequiresVersionPage(QtWidgets.QWidget): self.context = context - # Set the ui file: + # set the ui file self.ui = Ui_WizardInstallerRequires() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] self.ui.groupBox.setStyleSheet( 'QLabel[headercell="true"] { font-weight: bold; }' @@ -123,20 +125,20 @@ class WizardInstallerRequiresVersionPage(QtWidgets.QWidget): self.ui.labelGame.setText(game.gameName()) - # Set the required version: + # set the required version self.ui.labelGameNeed.setText(context.game_version) self.ui.labelScriptExtenderNeed.setText(context.script_extender_version) self.ui.labelGraphicsExtenderNeed.setText(context.graphics_extender_version) self.ui.labelWryeBashNeed.setText(context.wrye_bash_version) - # Set the current version: + # set the current version self.ui.labelGameHave.setText(game.version().canonicalString()) - se = game.feature(mobase.ScriptExtender) # type: ignore - if se or se.isInstalled(): + se = organizer.gameFeatures().gameFeature(mobase.ScriptExtender) + if se and se.isInstalled(): self.ui.labelScriptExtenderHave.setText(se.getExtenderVersion()) - # Cannot check these so... - game_ok, se_ok, ge_ok, _ = check_version(context, organizer) + # cannot check these so... + game_ok, se_ok, _, _ = check_version(context, organizer) self.ui.labelGameIcon.setPixmap(okIcon if game_ok else koIcon) self.ui.labelScriptExtenderIcon.setPixmap(okIcon if se_ok else koIcon) self.ui.labelGraphicsExtenderIcon.setPixmap(noIcon) @@ -144,20 +146,18 @@ class WizardInstallerRequiresVersionPage(QtWidgets.QWidget): class WizardInstallerSelectPage(QtWidgets.QWidget): - - # Signal emitted when an item is double-clicked, only for SelectOne - # context: + # signal emitted when an item is double-clicked, only for SelectOne context itemDoubleClicked = pyqtSignal() - _context: WizardSelectContext - _images: Mapping[Path, Path] + context: WizardSelectContext[WizardRunnerState, Any] + _images: dict[Path, Path] _currentImage: QPixmap def __init__( self, - context: WizardSelectContext, - images: Mapping[Path, Path], - options: Optional[List[str]], + context: WizardSelectContext[WizardRunnerState, Any], + images: dict[Path, Path], + options: Sequence[str] | None, parent: QtWidgets.QWidget, ): """ @@ -171,20 +171,22 @@ class WizardInstallerSelectPage(QtWidgets.QWidget): self._images = images - # Set the ui file: + # set the ui file self.ui = Ui_WizardInstallerPage() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] - self.ui.optionList.currentItemChanged.connect(self.onCurrentItemChanged) + self.ui.optionList.currentItemChanged.connect( # pyright: ignore[reportUnknownMemberType] + self.onCurrentItemChanged + ) - # Create list item widgets: - for option in context.options: + # create list item widgets + for _ in context.options: item = QtWidgets.QListWidgetItem() self.ui.optionList.addItem(item) self.update_context(context) - # Extract previous select options: + # extract previous select options previous_options = [] if options: previous_options = [ @@ -196,9 +198,10 @@ class WizardInstallerSelectPage(QtWidgets.QWidget): elif isinstance(context, WizardSelectOneContext): previous_options = [context.default] - # Set the default values: + # set default values for i, option in enumerate(context.options): item = self.ui.optionList.item(i) + assert item is not None if isinstance(context, WizardSelectManyContext): item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) if option in previous_options: @@ -213,25 +216,27 @@ class WizardInstallerSelectPage(QtWidgets.QWidget): self.ui.optionList.setCurrentItem(item) if isinstance(context, WizardSelectOneContext): - self.ui.optionList.doubleClicked.connect(self.itemDoubleClicked.emit) + self.ui.optionList.doubleClicked.connect( # pyright: ignore[reportUnknownMemberType] + self.itemDoubleClicked.emit + ) - def update_context(self, context: WizardSelectContext): + def update_context(self, context: WizardSelectContext[WizardRunnerState, Any]): + self.context = context - self._context = context - - options = self._context.options + options = self.context.options assert len(options) == self.ui.optionList.count() self.ui.selectDescriptionLabel.setText(context.description) self.ui.selectDescriptionLabel.setMargin(4) - # Update the content of the items: + # update the content of the items for i, option in enumerate(options): item = self.ui.optionList.item(i) + assert item is not None item.setText(option.name) item.setData(Qt.ItemDataRole.UserRole, option) - # No item selected, select the first one: + # no item selected, select the first one if not self.ui.optionList.currentItem(): self.ui.optionList.setCurrentRow(0) @@ -258,30 +263,31 @@ class WizardInstallerSelectPage(QtWidgets.QWidget): Qt.TransformationMode.SmoothTransformation, ) - def resizeEvent(self, event: QResizeEvent) -> None: - super().resizeEvent(event) + def resizeEvent(self, a0: QResizeEvent | None) -> None: + super().resizeEvent(a0) self.ui.imageLabel.setPixmap(self.getResizedImage()) - def selectedOptions(self) -> List[SelectOption]: - options = [] - if isinstance(self._context, WizardSelectOneContext): - options.append( - self.ui.optionList.currentItem().data(Qt.ItemDataRole.UserRole) - ) + def selectedOptions(self) -> list[SelectOption]: + options: list[SelectOption] = [] + if isinstance(self.context, WizardSelectOneContext): + item = self.ui.optionList.currentItem() + assert item is not None + options.append(item.data(Qt.ItemDataRole.UserRole)) else: for i in range(self.ui.optionList.count()): item = self.ui.optionList.item(i) + assert item is not None if item.checkState() == Qt.CheckState.Checked: options.append(item.data(Qt.ItemDataRole.UserRole)) return options - def selected(self) -> WizardSelectContext: - if isinstance(self._context, WizardSelectOneContext): - return self._context.select(self.selectedOptions()[0]) - elif isinstance(self._context, WizardSelectManyContext): - return self._context.select(self.selectedOptions()) + def selected(self) -> WizardSelectContext[WizardRunnerState, Any]: + if isinstance(self.context, WizardSelectOneContext): + return self.context.select(self.selectedOptions()[0]) + elif isinstance(self.context, WizardSelectManyContext): + return self.context.select(self.selectedOptions()) else: - return self._context # type: ignore + return self.context class WizardInstallerCompletePage(QtWidgets.QWidget): @@ -292,22 +298,22 @@ class WizardInstallerCompletePage(QtWidgets.QWidget): ): super().__init__(parent) - # Set the ui file: + # set the ui file self.ui = Ui_WizardInstallerComplete() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] self.setStyleSheet('QLabel[heading="true"] { font-weight: bold; }') self.context = context self.state = context.state - # Retrieve the keyword visitor: - kvisitor: WizardRunnerKeywordVisitor = context.factory.kvisitor # type: ignore + # retrieve the keyword visitor + kvisitor = cast(WizardRunnerKeywordVisitor, context.factory.kvisitor) - # The list of plugins in selected sub-packages: - plugins: Set[Plugin] = set() + # the list of plugins in selected sub-packages + plugins: set[Plugin] = set() - # SubPackages: + # sub-packages for sp in kvisitor.subpackages: item = QtWidgets.QListWidgetItem() item.setText(sp.name) @@ -319,13 +325,13 @@ class WizardInstallerCompletePage(QtWidgets.QWidget): plugins.update(kvisitor.plugins_for(sp)) self.ui.subpackagesList.addItem(item) - # Switch the renamed plugins: + # switch the renamed plugins for plugin in list(plugins): if plugin in self.state.renames: plugins.remove(plugin) plugins.add(Plugin(self.state.renames[plugin])) - # Plugins: + # lugins for plugin in sorted(plugins): item = QtWidgets.QListWidgetItem() item.setText(plugin.name) @@ -336,11 +342,13 @@ class WizardInstallerCompletePage(QtWidgets.QWidget): item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) self.ui.pluginsList.addItem(item) - # INI Tweaks: + # INI Tweaks self.ui.tweaksWidget.setVisible(bool(self.state.tweaks)) - self.ui.tweaksList.currentItemChanged.connect(self.onCurrentTweakItemChanged) + self.ui.tweaksList.currentItemChanged.connect( # pyright: ignore[reportUnknownMemberType] + self.onCurrentTweakItemChanged + ) if self.state.tweaks: - # Group the tweaks per file: + # group the tweaks per file tweaks = { file: self.state.tweaks.tweaks(file) for file in self.state.tweaks.files() @@ -357,56 +365,61 @@ class WizardInstallerCompletePage(QtWidgets.QWidget): ) self.ui.tweaksList.setCurrentRow(0) - # Notes: + # notes md = "" for note in self.state.notes: md += f"- {note}\n" - self.ui.notesTextEdit.document().setIndentWidth(10) + document = self.ui.notesTextEdit.document() + assert document is not None + document.setIndentWidth(10) self.ui.notesTextEdit.setMarkdown(md) def onCurrentTweakItemChanged( self, current: QtWidgets.QListWidgetItem, previous: QtWidgets.QListWidgetItem ): - # Clear text area and create the tweaks: + # clear text area and create the tweaks self.ui.tweaksTextEdit.clear() self.ui.tweaksTextEdit.appendPlainText( make_ini_tweaks(current.data(Qt.ItemDataRole.UserRole)) ) - def subpackages(self) -> List[str]: + def subpackages(self) -> list[str]: """ Returns: The list of subpackages selected in the UI (either automatically by the interpreter or by the user). """ - sp: List[str] = [] + sp: list[str] = [] for i in range(self.ui.subpackagesList.count()): item = self.ui.subpackagesList.item(i) + assert item is not None if item.checkState() == Qt.CheckState.Checked: sp.append(item.text()) return sp - def plugins(self) -> Dict[str, bool]: + def plugins(self) -> dict[str, bool]: """ Returns: The list of plugins selected in the UI (either automatically by the interpreter or by the user). """ - sp: Dict[str, bool] = {} + sp: dict[str, bool] = {} for i in range(self.ui.pluginsList.count()): item = self.ui.pluginsList.item(i) + assert item is not None sp[item.text()] = item.checkState() == Qt.CheckState.Checked return sp - def tweaks(self) -> Mapping[str, List[WizardINISetting]]: + def tweaks(self) -> dict[str, list[WizardINISetting]]: """ Returns: The list of tweaks created by the wizard. The returned value maps filenames to INI tweaks. """ - rets = {} + rets: dict[str, list[WizardINISetting]] = {} for i in range(self.ui.tweaksList.count()): item = self.ui.tweaksList.item(i) + assert item is not None rets[item.text()] = item.data(Qt.ItemDataRole.UserRole) return rets @@ -419,17 +432,19 @@ class WizardInstallerCancelPage(QtWidgets.QWidget): ): super().__init__(parent) - # Set the ui file (same UI file for both cancel and error): + # set the ui file (same UI file for both cancel and error) self.ui = Ui_WizardInstallerError() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] self.ui.titleLabel.setText( "The installation was cancelled by the installer with the following reason." ) + style = self.style() + assert style is not None self.ui.iconLabel.setPixmap( - self.style() - .standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning) - .pixmap(24, 24) + style.standardIcon( + QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning + ).pixmap(24, 24) ) self.ui.messageEdit.setText(context.message()) @@ -442,50 +457,51 @@ class WizardInstallerErrorPage(QtWidgets.QWidget): ): super().__init__(parent) - # Set the ui file (same UI file for both cancel and error): + # set the ui file (same UI file for both cancel and error) self.ui = Ui_WizardInstallerError() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] self.ui.titleLabel.setText( "An error occurred during the installation of the script, " "this is probably due to an incorrect script file (wizard.txt) in the " "archive." ) + style = self.style() + assert style is not None self.ui.iconLabel.setPixmap( - self.style() - .standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical) - .pixmap(24, 24) + style.standardIcon( + QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical + ).pixmap(24, 24) ) self.ui.messageEdit.setText(str(error)) class WizardInstallerDialog(QtWidgets.QDialog): - - # Flag to indicate if the user chose to do a manual installation: + # flag to indicate if the user chose to do a manual installation _manual: bool = False - # The organizer: + # organizer _organizer: mobase.IOrganizer - # The interpreter: - _interpreter: WizardInterpreter - _images: Mapping[Path, Path] - _options: Mapping[str, List[str]] + # interpreter + _interpreter: WizardInterpreter[WizardRunnerState] + _images: dict[Path, Path] + _options: dict[str, list[str]] - # The Wizard MO2 interface: - _start_context: WizardTopLevelContext + # the Wizard MO2 interface + _start_context: WizardTopLevelContext[WizardRunnerState] - # Mapping from context to selected options: - _pages: Mapping[ParserRuleContext, WizardInstallerSelectPage] + # dict from context to selected options + _pages: dict[ParserRuleContext, WizardInstallerSelectPage] def __init__( self, organizer: mobase.IOrganizer, - interpreter: WizardInterpreter, + interpreter: WizardInterpreter[WizardRunnerState], context: WizardTopLevelContext[WizardRunnerState], name: mobase.GuessedString, - images: Mapping[Path, Path], - options: Mapping[str, List[str]], + images: dict[Path, Path], + options: dict[str, list[str]], parent: QtWidgets.QWidget, ): """ @@ -506,42 +522,52 @@ class WizardInstallerDialog(QtWidgets.QDialog): self._start_context = context self._pages = {} - # Set the ui file: + # set the ui file self.ui = Ui_WizardInstallerDialog() - self.ui.setupUi(self) + self.ui.setupUi(self) # pyright: ignore[reportUnknownMemberType] self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False) self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, True) # mobase.GuessedString contains multiple names with various level of - # "guess". Using .variants() returns the list of names, and doing str(name) - # will return the most-likely value. + # "guess", using.variants() returns the list of names, and doing str(name) + # will return the most-likely value for value in name.variants(): self.ui.nameCombo.addItem(value) - self.ui.nameCombo.completer().setCaseSensitivity( - Qt.CaseSensitivity.CaseSensitive - ) + completer = self.ui.nameCombo.completer() + assert completer is not None + completer.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) self.ui.nameCombo.setCurrentIndex(self.ui.nameCombo.findText(str(name))) - # We need to connect the Cancel / Manual buttons. We can of course use - # PyQt6 signal/slot syntax: - self.ui.cancelBtn.clicked.connect(self.reject) + # we need to connect the Cancel / Manual buttons. We can of course use + # PyQt6 signal/slot syntax + self.ui.cancelBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.reject + ) def manualClicked(): self._manual = True self.reject() - self.ui.manualBtn.clicked.connect(manualClicked) + self.ui.manualBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + manualClicked + ) - self.ui.prevBtn.clicked.connect(self.previousClicked) - self.ui.nextBtn.clicked.connect(self.nextClicked) + self.ui.prevBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.previousClicked + ) + self.ui.nextBtn.clicked.connect( # pyright: ignore[reportUnknownMemberType] + self.nextClicked + ) backShortcut = QShortcut(QKeySequence(Qt.Key.Key_Backspace), self) - backShortcut.activated.connect(self.previousClicked) # type: ignore + backShortcut.activated.connect( # pyright: ignore[reportUnknownMemberType] + self.previousClicked + ) @property def scriptButtonClicked(self) -> pyqtSignal: - return self.ui.scriptBtn.clicked # type: ignore + return self.ui.scriptBtn.clicked # pyright: ignore[reportReturnType] def name(self): return self.ui.nameCombo.currentText() @@ -552,25 +578,23 @@ class WizardInstallerDialog(QtWidgets.QDialog): The list of subpackages to install. Only valid if exec() returned Accepted. """ - # Note: We cannot fetch it from the state since the user can modify it in the - # UI. + # we cannot fetch it from the state since the user can modify it in the UI widget = self.ui.stackedWidget.currentWidget() assert isinstance(widget, WizardInstallerCompletePage) return widget.subpackages() - def plugins(self) -> Dict[str, bool]: + def plugins(self) -> dict[str, bool]: """ Returns: The list of plugins to install and enable. Only valid if exec() returned Accepted. """ - # Note: We cannot fetch it from the state since the user can modify it in the - # UI. + # we cannot fetch it from the state since the user can modify it in the UI widget = self.ui.stackedWidget.currentWidget() assert isinstance(widget, WizardInstallerCompletePage) return widget.plugins() - def renames(self) -> Mapping[str, str]: + def renames(self) -> dict[str, str]: """ Returns: The mapping of renames for plugins. Only valid if exec() returned Accepted. @@ -581,7 +605,7 @@ class WizardInstallerDialog(QtWidgets.QDialog): plugin.name: new_name for plugin, new_name in widget.state.renames.items() } - def tweaks(self) -> Mapping[str, List[WizardINISetting]]: + def tweaks(self) -> dict[str, list[WizardINISetting]]: """ Returns: The list of tweaks per file. Only valid if exec() returned Accepted. @@ -590,16 +614,16 @@ class WizardInstallerDialog(QtWidgets.QDialog): assert isinstance(widget, WizardInstallerCompletePage) return widget.tweaks() - def selectedOptions(self) -> Mapping[str, List[str]]: + def selectedOptions(self) -> dict[str, list[str]]: """ Returns: The list of all currently selected options. """ - result = {} + result: dict[str, list[str]] = {} for i in range(self.ui.stackedWidget.count()): page = self.ui.stackedWidget.widget(i) if isinstance(page, WizardInstallerSelectPage): - result[page._context.description] = [ + result[page.context.description] = [ option.name for option in page.selectedOptions() ] return result @@ -632,6 +656,7 @@ class WizardInstallerDialog(QtWidgets.QDialog): if context.context in self._pages: page = self._pages[context.context] + assert isinstance(context, WizardSelectContext) page.update_context(context) else: page = self._make_page(context) @@ -660,13 +685,13 @@ class WizardInstallerDialog(QtWidgets.QDialog): name: str = self.ui.nextBtn.text() if isinstance(widget, WizardInstallerSelectPage): - name = self.tr("Next") + name = self._tr("Next") elif isinstance(widget, WizardInstallerRequiresVersionPage): - name = self.tr("Install anyway") + name = self._tr("Install anyway") elif isinstance(widget, (WizardInstallerCancelPage, WizardInstallerErrorPage)): self.ui.nextBtn.setDisabled(True) else: - name = self.tr("Install") + name = self._tr("Install") self.ui.nextBtn.setText(name) @@ -679,7 +704,7 @@ class WizardInstallerDialog(QtWidgets.QDialog): ), ) - # If all requirements are ok, skip the context: + # if all requirements are ok, skip the context if isinstance(context, WizardRequireVersionsContext): if all(check_version(context, self._organizer)): return self._exec_until(context.exec()) @@ -695,7 +720,9 @@ class WizardInstallerDialog(QtWidgets.QDialog): self._options.get(context.description, None), self, ) - page.itemDoubleClicked.connect(self.nextClicked) + page.itemDoubleClicked.connect( # pyright: ignore[reportUnknownMemberType] + self.nextClicked + ) self._pages[context.context] = page # type: ignore elif isinstance(context, WizardRequireVersionsContext): page = WizardInstallerRequiresVersionPage(context, self._organizer, self) @@ -704,6 +731,8 @@ class WizardInstallerDialog(QtWidgets.QDialog): page = WizardInstallerCancelPage(context, self) else: page = WizardInstallerCompletePage(context, self) + else: + raise NotImplementedError() # for typing purpose return page @@ -719,5 +748,5 @@ class WizardInstallerDialog(QtWidgets.QDialog): self._update_focus() return super().exec() - def tr(self, str): - return QApplication.translate("WizardInstallerDialog", str) + def _tr(self, value: str): + return QApplication.translate("WizardInstallerDialog", value) diff --git a/src/installer.py b/src/installer.py index 7870245..27cb2c3 100644 --- a/src/installer.py +++ b/src/installer.py @@ -1,16 +1,15 @@ -# -*- encoding: utf-8 -*- +from __future__ import annotations import os import re import sys from collections import defaultdict from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Sequence, Union, cast from PyQt6 import QtWidgets - -# MO2 ships with PyQt6, so you can use it in your plugins: from PyQt6.QtWidgets import QApplication + from wizard.runner import WizardRunnerState import mobase @@ -29,22 +28,19 @@ class WizardInstaller(mobase.IPluginInstallerSimple): it is valid (for this installer) and then modify it if required before extraction. """ - # Regex used to parse settings: + # regex used to parse settings RE_DESCRIPTION = re.compile(r"select([0-9]+)-description") RE_OPTION = re.compile(r"select([0-9]+)-option([0-9]+)") _organizer: mobase.IOrganizer - # List of selected options: + # list of selected options _installerOptions: Dict[str, List[str]] _installerUsed: bool def __init__(self): super().__init__() - # Method for IPlugin - I will not details these here since those are quite - # self-explanatory and are common to all plugins: - def init(self, organizer: mobase.IOrganizer): self._organizer = organizer return True @@ -53,13 +49,13 @@ class WizardInstaller(mobase.IPluginInstallerSimple): return "BAIN Wizard Installer" def localizedName(self) -> str: - return self.tr("BAIN Wizard Installer") + return self._tr("BAIN Wizard Installer") def author(self): return "Holt59" def description(self): - return self.tr("Installer for BAIN archive containing wizard scripts.") + return self._tr("Installer for BAIN archive containing wizard scripts.") def version(self): return mobase.VersionInfo(1, 0, 2) @@ -80,28 +76,31 @@ class WizardInstaller(mobase.IPluginInstallerSimple): "prefer OMOD installer over this one when possible", False, ), - # Above FOMOD: + # above FOMOD? mobase.PluginSetting("priority", "priority of this installer", 120), ] - # Method for IPluginInstallerSimple: + # method for IPluginInstallerSimple - def priority(self): - return self._organizer.pluginSetting(self.name(), "priority") + def priority(self) -> int: + return cast(int, self._organizer.pluginSetting(self.name(), "priority")) def isManualInstaller(self) -> bool: return False def onInstallationStart( - self, archive: str, reinstallation: bool, mod: Optional[mobase.IModInterface] + self, + archive: str, + reinstallation: bool, + current_mod: Optional[mobase.IModInterface], ): self._installerUsed = False self._installerOptions = {} - if mod: - settings = mod.pluginSettings(self.name()) + if current_mod: + settings = current_mod.pluginSettings(self.name()) - # First extract the description: + # first extract the description descriptions: Dict[int, str] = {} options: Dict[int, Dict[int, str]] = defaultdict(dict) for setting, value in settings.items(): @@ -123,19 +122,23 @@ class WizardInstaller(mobase.IPluginInstallerSimple): self._installerOptions[desc].append(options[kdesc][index]) def onInstallationEnd( - self, result: mobase.InstallResult, mod: Optional[mobase.IModInterface] + self, result: mobase.InstallResult, new_mod: Optional[mobase.IModInterface] ): - if result != mobase.InstallResult.SUCCESS or not self._installerUsed or not mod: + if ( + result != mobase.InstallResult.SUCCESS + or not self._installerUsed + or not new_mod + ): return - mod.clearPluginSettings(self.name()) + new_mod.clearPluginSettings(self.name()) for i, desc in enumerate(self._installerOptions): - mod.setPluginSetting(self.name(), f"select{i}-description", desc) + new_mod.setPluginSetting(self.name(), f"select{i}-description", desc) for iopt, opt in enumerate(self._installerOptions[desc]): - mod.setPluginSetting(self.name(), f"select{i}-option{iopt}", opt) + new_mod.setPluginSetting(self.name(), f"select{i}-option{iopt}", opt) def _hasFomodInstaller(self) -> bool: - # Do not consider the NCC installer. + # do not consider the NCC installer return self._organizer.isPluginEnabled("Fomod Installer") def _hasOmodInstaller(self) -> bool: @@ -161,16 +164,16 @@ class WizardInstaller(mobase.IPluginInstallerSimple): if entry: return tree - if len(tree) == 1 and isinstance(tree[0], mobase.IFileTree): - return self._getWizardArchiveBase(tree[0], data_name, checker) + if len(tree) == 1 and isinstance((root := tree[0]), mobase.IFileTree): + return self._getWizardArchiveBase(root, data_name, checker) return None def _getEntriesToExtract( self, tree: mobase.IFileTree, - extensions: List[str] = ["png", "jpg", "jpeg", "gif", "bmp", "ini"], - ) -> List[mobase.FileTreeEntry]: + extensions: Sequence[str] = ["png", "jpg", "jpeg", "gif", "bmp", "ini"], + ) -> list[mobase.FileTreeEntry]: """ Retrieve all the entries to extract from the given tree. @@ -181,7 +184,7 @@ class WizardInstaller(mobase.IPluginInstallerSimple): Returns: A list of entries corresponding to files with the given extensions. """ - entries = [] + entries: list[mobase.FileTreeEntry] = [] def fn(path: str, entry: mobase.FileTreeEntry): if entry.isFile() and entry.hasSuffix(extensions): @@ -204,21 +207,19 @@ class WizardInstaller(mobase.IPluginInstallerSimple): True if the file-tree can be installed, false otherwise. """ - # Retrieve the name of the "data" folder: + # retrieve the name of the "data" folder data_name = self._organizer.managedGame().dataDirectory().dirName() - # Retrieve the mod-data-checker: - checker: mobase.ModDataChecker = self._organizer.managedGame().feature( - mobase.ModDataChecker # type: ignore - ) + # retrieve the mod-data-checker + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) - # Retrieve the base: + # retrieve the base base = self._getWizardArchiveBase(tree, data_name, checker) if not base: return False - # Check FOMOD: + # check FOMOD for priority fomod = base.exists("fomod/ModuleConfig.xml") if ( fomod @@ -234,9 +235,9 @@ class WizardInstaller(mobase.IPluginInstallerSimple): def install( self, name: mobase.GuessedString, - otree: mobase.IFileTree, + tree: mobase.IFileTree, version: str, - modId: int, + nexus_id: int, ) -> Union[mobase.InstallResult, mobase.IFileTree]: """ Perform the actual installation. @@ -244,9 +245,9 @@ class WizardInstaller(mobase.IPluginInstallerSimple): Args: name: The "name" of the mod. This can be updated to change the name of the mod. - otree: The original archive tree. + tree: The original archive tree. version: The original version of the mod. - modId: The original ID of the mod. + nexus_id: The original ID of the mod. Returns: We either return the modified file-tree (if the installation was successful), or a InstallResult otherwise. @@ -256,16 +257,14 @@ class WizardInstaller(mobase.IPluginInstallerSimple): of the mod, in case those were updated by the installer. """ - # Retrieve the name of the "data" folder: + # retrieve the name of the "data" folder data_name = self._organizer.managedGame().dataDirectory().dirName() - # Retrieve the mod-data-checker: - checker: mobase.ModDataChecker = self._organizer.managedGame().feature( - mobase.ModDataChecker # type: ignore - ) + # retrieve the mod-data-checker + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) - # Retrive the "base" folder: - base = self._getWizardArchiveBase(otree, data_name, checker) + # retrieve the "base" folder + base = self._getWizardArchiveBase(tree, data_name, checker) if not base or not checker: return mobase.InstallResult.NOT_ATTEMPTED @@ -273,9 +272,9 @@ class WizardInstaller(mobase.IPluginInstallerSimple): if wizard is None: return mobase.InstallResult.NOT_ATTEMPTED - to_extract = self._getEntriesToExtract(otree) + to_extract = self._getEntriesToExtract(tree) - # Extract the script: + # extract the script paths = self._manager().extractFiles([wizard] + to_extract, silent=False) if len(paths) != len(to_extract) + 1: return mobase.InstallResult.FAILED @@ -291,29 +290,30 @@ class WizardInstaller(mobase.IPluginInstallerSimple): name, { Path(entry.path()): Path(path) - for entry, path in zip(to_extract, paths[1:]) + for entry, path in zip(to_extract, paths[1:], strict=True) if not path.endswith(".ini") }, self._installerOptions, self._parentWidget(), ) - dialog.scriptButtonClicked.connect(lambda: os.startfile(script)) # type: ignore + dialog.scriptButtonClicked.connect( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] + lambda: os.startfile(script) + ) - # Note: Unlike the official installer, we do not have a "silent" setting, - # but it is really simple to add it. + # unlike the official installer, we do not have a "silent" setting, but it is + # really simple to add it if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted: - - # We update the name with the user specified one: + # we update the name with the user specified one name.update(dialog.name(), mobase.GuessQuality.USER) - # Create the tree with all the sub-packages: - tree = otree.createOrphanTree() + # create the tree with all the sub-packages + new_tree = tree.createOrphanTree() for subpackage in dialog.subpackages(): entry = base.find(subpackage) - # Should never happens since we fetch the subpackage for the archive: + # should never happens since we fetch the subpackage for the archive if not entry or not isinstance(entry, mobase.IFileTree): print( f"SubPackage {subpackage} not found in the archive.", @@ -321,50 +321,48 @@ class WizardInstaller(mobase.IPluginInstallerSimple): ) continue - tree.merge(entry) + new_tree.merge(entry) - # Handle renames: + # handle renames for original, new in dialog.renames().items(): - # Entry should be at the root: - entry = tree.find(original) + # entry should be at the root + entry = new_tree.find(original) if not entry: print(f"Plugin {original} not found, cannot rename.") continue - tree.move(entry, new) + new_tree.move(entry, new) - # Move not selected plugins to optional: + # move not selected plugins to optional for plugin, enabled in dialog.plugins().items(): if not enabled: - entry = tree.find(plugin) + entry = new_tree.find(plugin) if not entry: continue # silently fail since the plugin should be disabled - tree.addDirectory("optional").insert(entry) + new_tree.addDirectory("optional").insert(entry) # TODO: INI Tweaks: alltweaks = dialog.tweaks() for filename, tweaks in alltweaks.items(): - - # Find the original file (if any): - o_entry = tree.find(filename) + # find the original file (if any) + o_entry = new_tree.find(filename) o_filename: Optional[str] = None if o_entry: - # Find the filepath from the list of extracted files: + # find the filepath from the list of extracted files index = to_extract.index(o_entry) - # +1 because the first one is the script. + # +1 because the first one is the script o_filename = paths[index + 1] - # If the file existed before, we keep the new one at the same - # place: + # if the file existed before, we keep the new one at the same place if o_entry or Path(filename).parts[0].lower() == "ini tweaks": - entry = tree.addFile(filename, replace_if_exists=True) + entry = new_tree.addFile(filename, replace_if_exists=True) - # Otherwise we create it in INI Tweaks + # otherwise we create it in INI Tweaks else: - entry = tree.addFile( + entry = new_tree.addFile( os.path.join("INI Tweaks", filename), replace_if_exists=True ) @@ -378,24 +376,23 @@ class WizardInstaller(mobase.IPluginInstallerSimple): with open(filepath, "w") as fp: fp.write(data) - # Mark stuff for saving: + # mark stuff for saving self._installerUsed = True self._installerOptions = dict(dialog.selectedOptions()) - # Return the tree: - return tree + return new_tree - # If user requested a manual installation, we update the name (to keep it - # in the manual installation dialog) and just notify the installation manager: + # if user requested a manual installation, we update the name (to keep it + # in the manual installation dialog) and just notify the installation manager elif dialog.isManualRequested(): name.update(dialog.name(), mobase.GuessQuality.USER) return mobase.InstallResult.MANUAL_REQUESTED - # If user canceled, we simply notify the installation manager: + # if user canceled, we simply notify the installation manager else: return mobase.InstallResult.CANCELED - def tr(self, str) -> str: - # We need this to translate string in Python. Check the common documentation - # for more details: - return QApplication.translate("WizardInstaller", str) + def _tr(self, value: str) -> str: + # we need this to translate string in Python. Check the common documentation + # for more details + return QApplication.translate("WizardInstaller", value) diff --git a/src/installer_wizard_en.ts b/src/installer_wizard_en.ts index 4011aba..7e3e9df 100644 --- a/src/installer_wizard_en.ts +++ b/src/installer_wizard_en.ts @@ -1,19 +1,6 @@ - - WizardInstaller - - - BAIN Wizard Installer - - - - - Installer for BAIN archive containing wizard scripts. - - - WizardInstallerComplete @@ -44,22 +31,6 @@ WizardInstallerDialog - - - - Next - - - - - Install anyway - - - - - Install - - BAIN Wizard Installer @@ -85,6 +56,11 @@ Back + + + Next + + Cancel diff --git a/src/runner.py b/src/runner.py index 5c5ec83..e098e11 100644 --- a/src/runner.py +++ b/src/runner.py @@ -1,8 +1,8 @@ -# -*- encoding: utf-8 -*- +from __future__ import annotations import sys from pathlib import Path -from typing import Iterable, List, Optional +from typing import Any, Iterable, List, Optional from wizard.interpreter import WizardInterpreter from wizard.manager import ManagerModInterface @@ -14,7 +14,6 @@ import mobase class MO2SubPackage(SubPackage): - _tree: mobase.IFileTree _files: List[str] @@ -22,12 +21,11 @@ class MO2SubPackage(SubPackage): super().__init__(tree.name()) self._tree = tree - # We cannot perform lazy iteration on the tree in a Python way so we - # have to list the files: - + # we cannot perform lazy iteration on the tree in a Python way so we + # have to list the files self._files = [] - def fn(folder, entry) -> mobase.IFileTree.WalkReturn: + def fn(folder: str, entry: mobase.FileTreeEntry) -> mobase.IFileTree.WalkReturn: self._files.append(entry.path()) return mobase.IFileTree.CONTINUE @@ -39,7 +37,6 @@ class MO2SubPackage(SubPackage): class MO2SeverityContext(SeverityContext): - _organizer: mobase.IOrganizer def __init__(self, organizer: mobase.IOrganizer): @@ -51,21 +48,17 @@ class MO2SeverityContext(SeverityContext): class MO2ManagerModInterface(ManagerModInterface): - _organizer: mobase.IOrganizer _game: mobase.IPluginGame _subpackages: SubPackages def __init__(self, tree: mobase.IFileTree, organizer: mobase.IOrganizer): - self._organizer = organizer self._game = organizer.managedGame() - checker: mobase.ModDataChecker = self._game.feature( - mobase.ModDataChecker # type: ignore - ) + checker = self._organizer.gameFeatures().gameFeature(mobase.ModDataChecker) - # Read the subpackages: + # read the sub-packages self._subpackages = SubPackages() for entry in tree: if isinstance(entry, mobase.IFileTree): @@ -74,12 +67,12 @@ class MO2ManagerModInterface(ManagerModInterface): self._subpackages.append(MO2SubPackage(entry)) continue - # Add entry with INI tweaks: + # add entry with INI tweaks if entry.exists("INI Tweaks") or entry.exists("INI"): self._subpackages.append(MO2SubPackage(entry)) continue - # We add folder with format "XXX Docs" where "XXX" is a number. + # we add folder with format "XXX Docs" where "XXX" is a number parts = entry.name().split() if ( len(parts) >= 2 @@ -103,7 +96,7 @@ class MO2ManagerModInterface(ManagerModInterface): return 0 def compareSEVersion(self, version: str) -> int: - se = self._game.feature(mobase.ScriptExtender) # type: ignore + se = self._organizer.gameFeatures().gameFeature(mobase.ScriptExtender) if not se: return 1 v1 = mobase.VersionInfo(version) @@ -116,11 +109,11 @@ class MO2ManagerModInterface(ManagerModInterface): return 0 def compareGEVersion(self, version: str) -> int: - # Cannot do th is in MO2. + # cannot do th is in MO2 return 1 def compareWBVersion(self, version: str) -> int: - # Cannot do this in MO2. + # cannot do this in MO2 return 1 def _resolve(self, filepath: str) -> Optional[Path]: @@ -134,8 +127,8 @@ class MO2ManagerModInterface(ManagerModInterface): The path to the given file on the disk, or one of the file mapping to it in the VFS, or None if the file does not exists. """ - # TODO: This does not handle weird path that go back (..) and - # then in data again, e.g. ../data/xxx.esp. + # TODO: This does not handle weird path that go back (..) and then in data + # again, e.g. ../data/xxx.esp. path: Optional[Path] if filepath.startswith(".."): path = Path(self._game.dataDirectory().absoluteFilePath(filepath)) @@ -161,7 +154,7 @@ class MO2ManagerModInterface(ManagerModInterface): def getPluginLoadOrder(self, filename: str, fallback: int = -1) -> int: return self._organizer.pluginList().loadOrder(filename) - def getPluginStatus(self, filename) -> int: + def getPluginStatus(self, filename: str) -> int: state = self._organizer.pluginList().state(filename) if state == mobase.PluginState.ACTIVE: @@ -170,25 +163,24 @@ class MO2ManagerModInterface(ManagerModInterface): return 0 # Or 1? return -1 - def getFilename(self, filepath: str) -> str: - path = self._resolve(filepath) - if path: - if path.is_file(): - return path.name + def getFilename(self, path: str) -> str: + path_ = self._resolve(path) + if path_: + if path_.is_file(): + return path_.name return "" - def getFolder(self, filepath: str) -> str: - path = self._resolve(filepath) - if path: - if path.is_dir(): - return path.name + def getFolder(self, path: str) -> str: + path_ = self._resolve(path) + if path_: + if path_.is_dir(): + return path_.name return "" def make_interpreter( base: mobase.IFileTree, organizer: mobase.IOrganizer -) -> WizardInterpreter: - +) -> WizardInterpreter[Any]: manager = MO2ManagerModInterface(base, organizer) severity = MO2SeverityContext(organizer) diff --git a/src/utils.py b/src/utils.py index 173535a..da85c48 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,15 +1,15 @@ -# -*- encoding: utf-8 -*- +from __future__ import annotations import re +from collections.abc import Sequence from pathlib import Path -from typing import Dict, List, Mapping +from typing import cast from wizard.tweaks import WizardINISetting, WizardINISettingEdit -def make_obscript_ini_tweaks(tweaks: List[WizardINISetting]) -> str: - - lines = [] +def make_obscript_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: + lines: list[str] = [] for tweak in tweaks: if not isinstance(tweak, WizardINISettingEdit): @@ -23,14 +23,13 @@ def make_obscript_ini_tweaks(tweaks: List[WizardINISetting]) -> str: return "\n".join(["; Generated by Mod Organizer 2 via Wizard"] + sorted(lines)) -def make_standard_ini_tweaks(tweaks: List[WizardINISetting]) -> str: - - # Group Tweaks per section: - sections: Mapping[str, List[WizardINISetting]] = {m.section: [] for m in tweaks} +def make_standard_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: + # group Tweaks per section + sections: dict[str, list[WizardINISetting]] = {m.section: [] for m in tweaks} for m in tweaks: sections[m.section].append(m) - lines = [] + lines: list[str] = [] for k in sorted(sections): s_tweaks = sorted(sections[k], key=lambda m: m.setting) @@ -48,16 +47,15 @@ def make_standard_ini_tweaks(tweaks: List[WizardINISetting]) -> str: return "\n".join(lines[:-1]) -def merge_standard_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str: +def merge_standard_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: import sys print(f"Cannot merge INI Tweaks for {file.name}.", file=sys.stderr) return make_standard_ini_tweaks(tweaks) -def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str: - - # This is from Wrye Bash (and the function is inspired from Wrye Bash): +def merge_obscript_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: + # this is from Wrye Bash (and the function is inspired from Wrye Bash) reComment = re.compile(";.*", re.U) reDeleted = re.compile("" r";-(\w.*?)$", re.U) reSet = re.compile("" r"\s*set\s+(.+?)\s+to\s+(.*)", re.I | re.U) @@ -70,20 +68,20 @@ def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str (reSetNGS, "setnumericgamesetting", "SetNumericGameSetting {} {}"), ) - def _parse_obse_line(line): + def _parse_obse_line(line: str): for regex, sectionKey, format_string in _regex_tuples: match = regex.match(line) if match: return match, sectionKey, format_string return None, None, None - # Read the original file: + # read the original file with open(file, "r") as fp: olines = fp.readlines() - # Map setting name to value: - settings: Dict[str, Dict[str, WizardINISettingEdit]] = {} - deleted: Dict[str, Dict[str, WizardINISetting]] = {} + # map setting name to value + settings: dict[str, dict[str, WizardINISettingEdit]] = {} + deleted: dict[str, dict[str, WizardINISetting]] = {} for tweak in tweaks: if isinstance(tweak, WizardINISettingEdit): if tweak.section not in settings: @@ -94,8 +92,8 @@ def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str deleted[tweak.section.lower()] = {} deleted[tweak.section.lower()][tweak.setting] = tweak - # Create the lines: - lines = [] + # create the lines + lines: list[str] = [] for line in olines: line = line.rstrip() maDeleted = reDeleted.match(line) @@ -108,13 +106,14 @@ def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str match, section_key, format_string = _parse_obse_line(stripped) if match: + assert format_string is not None setting = match.group(1) if section_key in settings and setting in settings[section_key]: value = settings[section_key][setting].value line = format_string.format(setting, value) comment = "" if settings[section_key][setting].comment: - comment = settings[section_key][setting].comment # type: ignore + comment = cast(str, settings[section_key][setting].comment) comment += " " comment += f"(set by MO2 via Wizard, was {match.group(2)})" line = f"{line} ; {comment}" @@ -129,6 +128,7 @@ def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str lines.append(line) for section in settings.values(): + line = "" for setting in section.values(): if setting.section.lower() == "set": line = f"{setting.section} {setting.setting} to {setting.value}" @@ -146,26 +146,24 @@ def merge_obscript_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str return "\n".join(lines) -def make_ini_tweaks(tweaks: List[WizardINISetting]) -> str: - +def make_ini_tweaks(tweaks: Sequence[WizardINISetting]) -> str: is_set = [ tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks ] - # Assume there is no mix. + # assume there is no mix if all(is_set): return make_obscript_ini_tweaks(tweaks) else: return make_standard_ini_tweaks(tweaks) -def merge_ini_tweaks(tweaks: List[WizardINISetting], file: Path) -> str: - +def merge_ini_tweaks(tweaks: Sequence[WizardINISetting], file: Path) -> str: is_set = [ tw.section.lower() in ("set", "setgs", "setnumericgamesetting") for tw in tweaks ] - # Assume there is no mix. + # assume there is no mix if all(is_set): return merge_obscript_ini_tweaks(tweaks, file) else: