mirror of
https://github.com/ModOrganizer2/pystubs-generation.git
synced 2026-07-27 14:07:13 -07:00
Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966a84a59a | ||
|
|
3a5c16af21 | ||
|
|
3eb1daa053 | ||
|
|
0eca0f7644 | ||
|
|
7467a2b11c | ||
|
|
eb420fcd79 | ||
|
|
156b827389 | ||
|
|
104e8612d8 | ||
|
|
4f24d363ec | ||
|
|
2e4fd7c1a8 | ||
|
|
ee3faa0c87 | ||
|
|
c5ee7f8a43 | ||
|
|
33b7222b13 | ||
|
|
e083e0832e | ||
|
|
478af17c58 | ||
|
|
6ca51655aa | ||
|
|
0ec04f4e9e | ||
|
|
d43d067c84 | ||
|
|
06e0fe1cca | ||
|
|
c57280578d | ||
|
|
178b1d111d | ||
|
|
a5b374f3f3 | ||
|
|
14fa117b2d | ||
|
|
6cab2a555b | ||
|
|
b555debad7 | ||
|
|
4f9e212b93 | ||
|
|
1388c1113a | ||
|
|
15dae1d2bc | ||
|
|
0aa1e2d24f | ||
|
|
df29edf5a5 | ||
|
|
15d43de37e | ||
|
|
7b9c6ab32c | ||
|
|
318a4acfb1 | ||
|
|
820bc3e0fe | ||
|
|
c980dd024b | ||
|
|
c7d8442398 | ||
|
|
ae09efbab3 | ||
|
|
a203ee8215 | ||
|
|
9fa09d1b88 | ||
|
|
4d40644aa5 | ||
|
|
fa0facabe8 | ||
|
|
02be57db1d | ||
|
|
233c8f51e8 | ||
|
|
6f390a4748 | ||
|
|
afd3751266 | ||
|
|
c813d6e86a | ||
|
|
822dfc161e | ||
|
|
c61c13e469 | ||
|
|
5d99ba470b | ||
|
|
6d48f0504b | ||
|
|
c60d1ac92f | ||
|
|
3f0da6f9a9 | ||
|
|
6e9912d1ef | ||
|
|
2a06eab0a6 | ||
|
|
d477f0849c | ||
|
|
9ca265736f | ||
|
|
3a5afaf690 |
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Build Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,32 +11,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Standard drop-in approach that should work for most people.
|
||||
- uses: ammaraskar/sphinx-action@master
|
||||
env:
|
||||
PYTHONPATH: .
|
||||
with:
|
||||
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.3.0/mobase.pyi docs/mobase.py"
|
||||
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py"
|
||||
docs-folder: "docs/"
|
||||
|
||||
# Publish built docs to gh-pages branch.
|
||||
# ===============================
|
||||
- name: Commit documentation changes
|
||||
run: |
|
||||
git clone https://github.com/Holt59/mo2-pystubs-generation.git --branch gh-pages --single-branch gh-pages
|
||||
cp -r docs/build/html/* gh-pages/
|
||||
cd gh-pages
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
git commit -m "Update documentation." -a || true
|
||||
# The above command will fail if no changes were present, so we ignore
|
||||
# that.
|
||||
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
- name: Install SSH Client 🔑
|
||||
uses: webfactory/ssh-agent@v0.4.1
|
||||
with:
|
||||
branch: gh-pages
|
||||
directory: gh-pages
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
SSH: true
|
||||
REPOSITORY_NAME: ModOrganizer2/python-plugins-doc
|
||||
BRANCH: master
|
||||
FOLDER: docs/build/html
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Check Linting
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Check Documentation
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
@@ -14,5 +14,5 @@ jobs:
|
||||
env:
|
||||
PYTHONPATH: .
|
||||
with:
|
||||
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.3.0/mobase.pyi docs/mobase.py"
|
||||
docs-folder: "docs/"
|
||||
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py"
|
||||
docs-folder: "docs/"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# This workflows will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Replace string
|
||||
uses: frabert/replace-string-action@v1.1
|
||||
id: version
|
||||
with:
|
||||
string: ${{ github.ref_name }}
|
||||
pattern: "v?([0-9][.][0-9][.][0-9]).*"
|
||||
replace-with: "$1"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
cd stubs/setup
|
||||
cp -r ../${{ steps.version.outputs.replaced }}/mobase-stubs/* mobase-stubs/
|
||||
sed -i 's/__version__ = ".*"/__version__ = "${{ github.ref_name }}"/' mobase-stubs/__init__.pyi
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
+2
-1
@@ -2,9 +2,10 @@
|
||||
.mypy_cache
|
||||
__pycache__
|
||||
.vscode
|
||||
**/*.egg-info
|
||||
|
||||
# The 'bin/' directory:
|
||||
bin
|
||||
docs/build
|
||||
docs/mobase.py
|
||||
docs/source/api
|
||||
docs/source/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mod Organizer 2 - Python stubs generation
|
||||
|
||||
This little project can be used to generate python stubs (`.pyi` file) for the MO2 python
|
||||
This project can be used to generate python stubs (`.pyi` file) for the MO2 python
|
||||
interface `mobase`.
|
||||
|
||||
## Using the stubs
|
||||
@@ -19,116 +19,101 @@ You can install stubs for a specific version of MO2:
|
||||
pip install mobase-stubs==2.3.2.*
|
||||
```
|
||||
|
||||
If you want development stubs, you can install them this way:
|
||||
```bash
|
||||
# Clone this repository:
|
||||
git clone https://github.com/ModOrganizer2/pystubs-generation.git
|
||||
|
||||
# Install the stubs:
|
||||
cd pystubs-generation/stubs/setup
|
||||
pip install .
|
||||
```
|
||||
|
||||
Some words of warning:
|
||||
|
||||
- The stubs are as correct as possible, but some errors are expected.
|
||||
- If you see a `InterfaceNotImplemented` class anywhere in the stubs, it means that a proper interface is
|
||||
currently not available.
|
||||
- Some classes are said (in the stubs) to inherit `QWidget` or `QObject`. This is true on the C++ side but NOT
|
||||
on the python side. The inheritance is only added to help with auto-completion since these classes also
|
||||
override `__getattr__` to dispatch to the underlying `QWidget` or `QObject`. Some things might not work as
|
||||
expected with these class (e.g., `isintance(myObject, QObject)` will return `False`), which is why a
|
||||
`_object()` and `_widget()` method is also provided.
|
||||
- Some classes are said (in the stubs) to inherit `QWidget` or `QObject`. This is true
|
||||
on the C++ side but NOT on the python side. The inheritance is only added to help with
|
||||
auto-completion since these classes also override `__getattr__` to dispatch to the
|
||||
underlying `QWidget` or `QObject`. Some things might not work as expected with these
|
||||
class (e.g., `isinstance(myObject, QObject)` will return `False`), which is why a
|
||||
`_object()` and `_widget()` method is also provided.
|
||||
|
||||
## Generating the stubs
|
||||
|
||||
The stubs are generated using python by parsing the `mobase` module.
|
||||
You need the version of python that matches your current MO2 installation: e.g., if you have a `python38.dll` in
|
||||
your MO2 installation path, then you need **python 3.8**.
|
||||
You need the version of python that matches your current MO2 installation: e.g., if you
|
||||
have a `python310.dll` in your MO2 installation path, then you need **Python 3.10**.
|
||||
|
||||
To generate the stubs, you can run:
|
||||
|
||||
```
|
||||
# Change the output folder to whatever you want:
|
||||
python main.py -c configs\config-2.3.yml -o stubs\setup\mobase-stubs\__init__.pyi ${MO2_INSTALL_PATH}
|
||||
```bash
|
||||
# install the package (-e if you want editable mode)
|
||||
pip install [-e] .
|
||||
|
||||
# change the output folder to whatever you want
|
||||
mo2-stubs-generator -c configs/config-2.4.yml -o mobase-stubs ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
Where `${MO2_INSTALL_PATH}` is the path to your MO2 installation (the one containing `ModOrganizer.exe`).
|
||||
Where `${MO2_INSTALL_PATH}` is the path to your MO2 installation (the one
|
||||
containing `ModOrganizer.exe`).
|
||||
|
||||
The latest stubs are kept under `stubs/setup/mobase-stubs/__init__.pyi`, and when a new version is released,
|
||||
the stubs are backed-up under `stubs/x.y.z/mobase.pyi`.
|
||||
The stubs are generated under `stubs/setup/mobase-stubs` by default, you
|
||||
can change the output file by using the `-o` option.
|
||||
The stubs under `stubs/setup/mobase-stubs` should not be committed as these are
|
||||
generated from the version stubs under `stubs/${VERSION}/mobase-stubs`.
|
||||
|
||||
A few options are available for `mo2-stubs-generator`:
|
||||
|
||||
If you do not specify a `-o` option, output will go to `stdout`, so you should redirect.
|
||||
Warning and "critical" messages are printed to `stderr`.
|
||||
|
||||
A few options are available for `main.py`:
|
||||
|
||||
```
|
||||
usage: Stubs generator for the MO2 python interface [-h] [-o OUTPUT] [-v] [-c CONFIG] INSTALL_DIR
|
||||
```bash
|
||||
$ mo2-stubs-generator --help
|
||||
usage: stubs generator for the MO2 python interface [-h] [-o OUTPUT] [-v] [-c CONFIG] INSTALL_DIR
|
||||
|
||||
positional arguments:
|
||||
INSTALL_DIR installation directory of Mod Organizer 2
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-o OUTPUT, --output OUTPUT
|
||||
output file (output to stdout if not specified)
|
||||
output folder (default stubs/setup/mobase-stubs)
|
||||
-v, --verbose verbose mode (all logs go to stderr)
|
||||
-c CONFIG, --config CONFIG
|
||||
configuration file
|
||||
```
|
||||
|
||||
The stubs generator will try hard to find a valid stubs for all classes and methods of `mobase`.
|
||||
A lot of information is available through the `-v` options. Without it, only conversions or fixes
|
||||
considered "strange" will be outputed.
|
||||
For instance, here is the output with the current `config-2.3.yml` file:
|
||||
|
||||
```
|
||||
WARNING: Replacing IOrganizer::FileInfo with FileInfo.
|
||||
WARNING: Replacing IOrganizer::FileInfo with FileInfo.
|
||||
WARNING: Replacing IPluginInstaller::EInstallResult with InstallResult.
|
||||
WARNING: Replacing IPluginInstaller::EInstallResult with InstallResult.
|
||||
```
|
||||
|
||||
As you can see, only a few types were manually fixed (specified in `config-2.3.yml`).
|
||||
The stubs generator will try hard to find a valid stubs for all classes
|
||||
and methods of `mobase`.
|
||||
A lot of information is available through the `-v` options. Without it,
|
||||
only conversions or fixes considered "strange" will be shown.
|
||||
|
||||
## Configuration file
|
||||
|
||||
The configuration file contains information for the stubs that cannot be deduced by `main` (or are too
|
||||
complex to deduce), and the documentation for everything.
|
||||
The configuration file contains information for the stubs that cannot be
|
||||
deduced by `main` (or are too complex to deduce), and the documentation for everything.
|
||||
|
||||
## Uploading the stubs to pypi
|
||||
|
||||
The upload of the stubs to https://pypi.org/project/mobase-stubs/ has to be done manually. Here
|
||||
are the steps:
|
||||
The upload of the stubs to [https://pypi.org/project/mobase-stubs/](https://pypi.org/project/mobase-stubs/)
|
||||
should be done automatically when a new Github tag is pushed.
|
||||
|
||||
1. Check the version of the stubs:
|
||||
- The version is specified in the configuration file. If you need to modify the stubs of the
|
||||
current version, you need to add a `.postX` after the version since PyPi does not allow
|
||||
re-upload of the same release.
|
||||
2. Generate the stubs using the procedure above. You should generate the stubs under `stubs/setup/mobase-stubs/__init__.pyi`.
|
||||
3. If necessary, update the dependencies in `setup.py` (`PyQt5-stubs` version and python version).
|
||||
4. Go to `stubs/setup` and run:
|
||||
## Extras — Using `mobase` in a Python interpreter
|
||||
|
||||
It is possible to start a (i)python interpreter with `mobase` imported by running
|
||||
|
||||
```bash
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
python -i -m mo2.stubs.generator.loader ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
You need to set the environment variables `TWINE_USERNAME` and `TWINE_PASSWORD` to appropriate values
|
||||
before running `twine upload`.
|
||||
You can also import `mobase` in your code using the following (after installing
|
||||
this package):
|
||||
|
||||
## Extras — Starts a python interpreter with `mobase`
|
||||
```python
|
||||
from mo2.stubs.generator import load_mobase
|
||||
|
||||
It is possible to start a (i)python interpret with `mobase` imported by running:
|
||||
mobase = load_mobase(MO2_INSTALL_PATH)
|
||||
|
||||
```
|
||||
python -im generator.loader ${MO2_INSTALL_PATH}
|
||||
# the above will probably not give you type-completion in your IDE or typing, so
|
||||
# you can use the following (if the stubs are installed)
|
||||
load_mobase(MO2_INSTALL_PATH)
|
||||
import mobase
|
||||
import mobase.widgets
|
||||
```
|
||||
|
||||
This has no real usage except for MO2 developers since most classes from the `mobase` module cannot be instantiated.
|
||||
|
||||
# License
|
||||
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
|
||||
for MO2 developers.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -138,4 +123,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
See [LICENSE](LICENSE).
|
||||
|
||||
+600
-158
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -13,7 +13,7 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../stubs/2.3rc1/"))
|
||||
sys.path.insert(0, os.path.abspath("../../stubs/2.4.0/"))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
@@ -64,7 +64,10 @@ html_sidebars = {
|
||||
"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]
|
||||
}
|
||||
|
||||
html_favicon = "mo2.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
html_extra_path = [".nojekyll"]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+82
-25
@@ -15,7 +15,7 @@ This guide assumes that:
|
||||
`Python extension <https://marketplace.visualstudio.com/items?itemName=ms-python.python>`_.
|
||||
- You have Python installed: https://www.python.org/downloads/.
|
||||
|
||||
- It is recommended but not mandatory to use the Python version that is used by MO2.
|
||||
- You must use the Python version that used by MO2.
|
||||
You can check the ``pythonXX.dll`` in the MO2 installation folder to find the Python version used by MO2 (``python38.dll`` means Python 3.8).
|
||||
- You obviously need a valid MO2 installation: https://github.com/modorganizer2/modorganizer/releases
|
||||
|
||||
@@ -28,8 +28,8 @@ Preparation
|
||||
-----------
|
||||
|
||||
**Note:** This part is optional but highly recommended if you want a proper environment to work with.
|
||||
Everything here is written to be as simple as possible but you can of course adapt it to your preferences: use a python virtual
|
||||
environment, use workspace settings instead of global ones, etc.
|
||||
Everything here is written to be as simple as possible but you can of course adapt it to your preferences:
|
||||
use a python virtual environment, use workspace settings instead of global ones, etc.
|
||||
|
||||
1. Get the ``mobase`` stubs
|
||||
...........................
|
||||
@@ -39,11 +39,14 @@ as ``flake8`` or ``mypy``.
|
||||
Instead, we provide `stubs <https://stackoverflow.com/questions/59051631/what-is-the-use-of-stub-files-pyi-in-python>`_
|
||||
which can be used for auto-completion or type-checking.
|
||||
|
||||
The stubs for ``mobase`` are available at https://github.com/ModOrganizer2/pystubs-generation/tree/master/stubs.
|
||||
You want to download the ``mobase.pyi`` file in the folder corresponding to your MO2 version and put it under ``$MO2DIR/plugins/data``.
|
||||
You can install the stubs for ``mobase`` using ``pip``:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install mobase-stubs
|
||||
|
||||
This will install the stubs for ``mobase`` but also for PyQt5, which is heavily used by MO2.
|
||||
|
||||
**Note:** It is possible to put the stubs in a different location, but we are going to use ``$MO2DIR/plugins/data`` for PyQt5,
|
||||
so we might as well use it for the stubs.
|
||||
|
||||
2. Configure Visual Studio Code for ``mobase``
|
||||
..............................................
|
||||
@@ -57,32 +60,86 @@ Open ``settings.json`` (Ctrl+Shift+P, then "Open Settings (JSON)"), and add the
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.autoComplete.extraPaths": [
|
||||
"$MO2DIR\\plugins\\data",
|
||||
]
|
||||
|
||||
3. Configure ``mypy`` to find the ``mobase`` stubs
|
||||
..................................................
|
||||
3. [Optional] Configure ``black`` to auto-format your source files
|
||||
..................................................................
|
||||
|
||||
There are multiply way to configure ``mypy``:
|
||||
This step is optional for your own plugin but recent MO2 plugins use ``black``
|
||||
to get consistent formatting.
|
||||
|
||||
1. You can create a ``mypy.ini`` file somewhere containing:
|
||||
You can install ``black`` with ``pip``:
|
||||
|
||||
.. code-block:: ini
|
||||
.. code::
|
||||
|
||||
[mypy]
|
||||
mypy_path = $MO2DIR\plugins\data
|
||||
pip install black flake8-black
|
||||
|
||||
And then add the following to ``settings.json`` (with the correct path):
|
||||
To configure Visual Studio Code to auto-format your code with ``black`` when saving, open ``settings.json``
|
||||
(Ctrl+Shift+P, then "Open Settings (JSON)"), and add the following entries:
|
||||
|
||||
.. code-block:: json-object
|
||||
|
||||
"python.linting.mypyArgs": [
|
||||
"--config-file=path-to-mypy.ini",
|
||||
]
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"python.formatting.provider": "black",
|
||||
|
||||
2. You can set the ``MYPYPATH`` environment variable to ``$MO2DIR\plugins\data`` (this requires
|
||||
restarting VS code).
|
||||
|
||||
4. [Optional] Automatically reload plugins during development
|
||||
.............................................................
|
||||
|
||||
This section is optional and requires you to already have written a "working"
|
||||
plugin (a plugin that MO2 can load).
|
||||
|
||||
Since MO2 2.4 alpha 6, a new command has been added to ``ModOrganizer.exe`` to
|
||||
reload plugins during execution.
|
||||
If your plugin is named "My Plugin", you can use the following command to reload
|
||||
it while MO2 is running:
|
||||
|
||||
.. code::
|
||||
|
||||
$MO2DIR\ModOrganizer.exe reload-plugin "My Plugin"
|
||||
|
||||
If you are using Visual Studio Code, you can send this command to MO2 automatically
|
||||
after saving files from your project.
|
||||
|
||||
1. Create a "reload plugin" task in Visual Studio Code (Ctrl+Shift+P then
|
||||
``Tasks: Configure task`` or open ``.vscode/tasks.json``) using the following
|
||||
snippet (replace the name and directory as needed):
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
// .vscode/tasks.json
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "reload plugin",
|
||||
"type": "shell",
|
||||
"command": "$MO2DIR/ModOrganizer.exe",
|
||||
"args": [
|
||||
"reload-plugin", "My Plugin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2. Install the `Trigger Task on Save <https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.triggertaskonsave>`_
|
||||
extension from Visual Studio Code marketplace.
|
||||
|
||||
3. Add the following to your Visual Studio Code settings (``.vscode/settings.json``)
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"triggerTaskOnSave.on": true,
|
||||
"triggerTaskOnSave.tasks": {
|
||||
"reload plugin": [
|
||||
"*.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Testing the setup
|
||||
-----------------
|
||||
@@ -128,8 +185,8 @@ If your setup is valid, here is what you should have.
|
||||
|
||||
.. image:: images/check-setup-3.png
|
||||
|
||||
If everything is as above, you can delete the test file and move on to writting
|
||||
If everything is as above, you can delete the test file and move on to writing
|
||||
your own plugin!
|
||||
|
||||
|
||||
.. |error-window| image:: images/error-window.png
|
||||
.. |error-window| image:: images/error-window.png
|
||||
|
||||
@@ -185,7 +185,7 @@ Most of these are pretty simple to understand:
|
||||
enable the plugin. You can also use a plugin setting to allow users to disable
|
||||
your plugins.
|
||||
- ``settings``: Returns the list of settings (that user can modify) for this plugin.
|
||||
Settings can be ``int```, ``bool``, ``str`` or list of ``str``. Here we indicate
|
||||
Settings can be ``int``, ``bool``, ``str`` or list of ``str``. Here we indicate
|
||||
that we have a "enabled" setting that user could use to disable the plugin (and
|
||||
we use it in ``isActive``).
|
||||
|
||||
@@ -198,4 +198,125 @@ a "bug" in ``boost.python``).
|
||||
The ``init`` method is called by MO2 to initialize the plugin. The given argument, ``organizer``,
|
||||
is an instance of :class:`IOrganizer<mobase.IOrganizer>` which is the class used to interface with MO2.
|
||||
Here, we use it in the ``isActive()`` method to retrieve the "enabled" setting for our plugin.
|
||||
See :class:`IOrganizer<mobase.IOrganizer>` for more details.
|
||||
See :class:`IOrganizer<mobase.IOrganizer>` for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
This section contains (links to) examples of MO2 Python plugins.
|
||||
Some of these plugins have been created for educational purpose and are thus very detailed and
|
||||
easy to understand or get started from.
|
||||
|
||||
Tutorial Plugins
|
||||
................
|
||||
|
||||
`This repository <https://github.com/Holt59/modorganizer-python_plugins>`_ contains examples of Python
|
||||
plugins that were written only to help users write their own plugins.
|
||||
If you want to start somewhere, this is the place to go.
|
||||
|
||||
Official Plugins
|
||||
................
|
||||
|
||||
These plugins are (or will be) included in MO2 releases and are usually maintain by some members of
|
||||
the MO2 development teams.
|
||||
These plugins are not as well documented as the ones in the repository above.
|
||||
|
||||
- `Basic Games <https://github.com/ModOrganizer2/modorganizer-basic_games>`_ [``IPluginGame``]
|
||||
This is the meta-plugin for "basic" games. It is a complex
|
||||
plugins and should mostly be investigated if you want to add a game to it.
|
||||
- `FNIS Tool <https://github.com/ModOrganizer2/modorganizer-fnistool>`_ [``IPluginTool``]:
|
||||
Plugin to integrate FNIS into MO2.
|
||||
- `Installer Wizard <https://github.com/ModOrganizer2/modorganizer-installer_wizard>`_ [``IPluginInstaller``]:
|
||||
Installer for BAIN archives containing wizard scripts.
|
||||
- `Preview DDS <https://github.com/ModOrganizer2/modorganizer-preview_dds>`_ [``IPluginPreview``]:
|
||||
Plugin to preview DDS files. Quite complex due to the use
|
||||
of OpenGL for display.
|
||||
- `Form 43 Checker <https://github.com/ModOrganizer2/modorganizer-form43_checker>`_ [``IPluginDiagnose``]:
|
||||
Plugin that warn users if there are form 43 ESPs (Skyrim ESPs)
|
||||
enabled when managing a Skyrim SE instance.
|
||||
- `Tool Configurator <https://github.com/ModOrganizer2/modorganizer-tool_configurator>`_ [``IPluginTool``]:
|
||||
Plugin that allows easier modifications of game settings.
|
||||
Mostly contains a complex GUI for managing INI files.
|
||||
- `Script Extender Plugin Checker <https://github.com/ModOrganizer2/modorganizer-script_extender_plugin_checker>`_ [``IPluginDiagnose``]:
|
||||
Plugin that checks Script Extender logs to see
|
||||
if some plugins have failed to load and display information to the user if possible.
|
||||
|
||||
|
||||
Unofficial Plugins
|
||||
..................
|
||||
|
||||
These plugins have been created by developers for MO2 and are usually distributed on Nexus.
|
||||
|
||||
- `Merge Plugins Hide <https://github.com/deorder/mo2-plugins>`_ [``IPluginTool``]:
|
||||
Hide / unhide plugins that were merged using ``Merge Plugins`` or ``zMerge``.
|
||||
- `OpenMW Exporter <https://github.com/AnyOldName3/ModOrganizer-to-OpenMW>`_ [``IPluginTool``]:
|
||||
A Mod Organizer plugin to export your VFS, plugin selection and load order to OpenMW.
|
||||
- `Orphaned Script Extender Save Deleter <https://github.com/AnyOldName3/modorganizer-orphaned_script_extender_save_deleter>`_ [``IPluginTool``]:
|
||||
Mod Organizer plugin to delete orphaned script extender co-saves.
|
||||
- `Sync Mod Order <https://github.com/deorder/mo2-plugins>`_ [``IPluginTool``]:
|
||||
Synchronize mod order from current profile to another while keeping the (enabled/disabled) state intact.
|
||||
|
||||
*Feel free to open an issue or a pull-request if you want to add your own plugin to the list.*
|
||||
|
||||
Internationalization
|
||||
--------------------
|
||||
|
||||
If you plan to distribute your plugin, it is often a good idea to provide translations for it.
|
||||
|
||||
Adding translation code
|
||||
.......................
|
||||
|
||||
Mod Organizer uses Qt translation system, so you need to adapt your plugin code to provide
|
||||
translation strings.
|
||||
To do this, you need two things:
|
||||
|
||||
1. In every class containing strings you need to translate, you must add a ``__tr`` function
|
||||
that takes a ``str`` input and call `QApplication.translate` on it (see example below).
|
||||
2. You need to wrap all translatable strings in a call to ``self.__str("My String")`` (see
|
||||
example below).
|
||||
|
||||
.. code:: python
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
class MyPlugin(...):
|
||||
|
||||
def localizedName(self) -> str:
|
||||
# Use self.__tr to wrap string you want translatable.
|
||||
return self.__tr("My Plugin Name")
|
||||
|
||||
def __tr(self, txt: str) -> str:
|
||||
# The first argument must EXACTLY match the class name:
|
||||
return QApplication.translate("MyPlugin", txt)
|
||||
|
||||
Generating Qt translation files
|
||||
...............................
|
||||
|
||||
Once your code is updated, you need to generate the Qt translation file ``.ts``.
|
||||
You can use ``PyQt5.lupdate_main`` for this:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
PyQt5.lupdate_main mysourcefile.py -ts mysourcefile.ts
|
||||
|
||||
You should generate a single translation file for your whole plugin even if it contains
|
||||
multiple files by passing all Python file and Qt UI (``.ui``) file to the command above.
|
||||
|
||||
Translating
|
||||
...........
|
||||
|
||||
Now that you have the original ``.ts`` file, you need to translate it in order to obtain
|
||||
translation files for other languages.
|
||||
To do so, you can use online services such as `Transifex <https://www.transifex.com>`_ or
|
||||
simply Qt Linguistic tools.
|
||||
|
||||
Distributing translations
|
||||
.........................
|
||||
|
||||
Once you have obtained translation files for another language, e.g. French, you need to
|
||||
compile it into a ``.qm`` file and then ship it.
|
||||
|
||||
- If you are using a single Python file plugin ``myplugin.py``, the name of the compiled
|
||||
translation must be ``myplugin_fr.qm``.
|
||||
- If you are shipping a module ``mymoduleplugin``, the name of the compiled translation
|
||||
must be ``mymoduleplugin``.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stderr, format="%(levelname)s: %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.WARNING)
|
||||
@@ -1,75 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import importlib.machinery
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_mobase(path: Path, moprivate: bool = False):
|
||||
""" Load the mobase from the given MO2 installation path and
|
||||
returns it.
|
||||
|
||||
Args:
|
||||
path: Path to the MO2 installation (folder containg the ModOrganizer.exe).
|
||||
moprivate: If True, the moprivate module will also be loaded and returned
|
||||
alongisde mobase.
|
||||
|
||||
Returns: The mobase module. """
|
||||
|
||||
# We need absolute path for loading DLL and modules:
|
||||
path = path.resolve()
|
||||
|
||||
# Adding to PATH environment variable for python < 3.8 and
|
||||
# via os.add_dll_directory (python >= 3.8).
|
||||
# See: https://stackoverflow.com/a/58632354/2666289
|
||||
if sys.version_info < (3, 8):
|
||||
os.environ["PATH"] = os.pathsep.join(
|
||||
[str(path), str(path.joinpath("dlls")), os.environ.get("PATH", "")]
|
||||
)
|
||||
else:
|
||||
os.add_dll_directory(str(path))
|
||||
os.add_dll_directory(str(path.joinpath("dlls")))
|
||||
|
||||
# We need to add plugins/data to sys.path, mainly for PyQt5
|
||||
sys.path.insert(1, path.joinpath("plugins", "data").as_posix())
|
||||
|
||||
mobase = importlib.machinery.ExtensionFileLoader(
|
||||
"mobase", path.joinpath("plugins", "data", "pythonrunner.dll").as_posix()
|
||||
).load_module()
|
||||
|
||||
if not moprivate:
|
||||
return mobase
|
||||
|
||||
moprivate = importlib.machinery.ExtensionFileLoader(
|
||||
"moprivate", path.joinpath("plugins", "data", "pythonrunner.dll").as_posix()
|
||||
).load_module()
|
||||
|
||||
return mobase, moprivate
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"Load mobase python module from MO2 installation directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"install_dir",
|
||||
metavar="INSTALL_DIR",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="installation directory of Mod Organizer 2",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--private", action="store_true", help="also load the moprivate module"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.private:
|
||||
mobase, moprivate = load_mobase(args.install_dir, moprivate=True)
|
||||
else:
|
||||
mobase = load_mobase(args.install_dir)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, Union, List
|
||||
|
||||
from . import logger
|
||||
from .mtypes import Class, Type, CType, Function
|
||||
|
||||
|
||||
class MobaseRegister:
|
||||
""" Class that register class. """
|
||||
|
||||
objects: Dict[str, Union[Class, List[Function]]]
|
||||
|
||||
def __init__(self):
|
||||
""" Create a new register with the list of objects. """
|
||||
self.raw_objects: Dict[str, Union[type]] = OrderedDict()
|
||||
self.objects = {}
|
||||
|
||||
self._cpptypes = {}
|
||||
self.cpp2py = {}
|
||||
|
||||
def add_object(self, name, object):
|
||||
self.raw_objects[name] = object
|
||||
|
||||
def make_object(
|
||||
self, name: str, e: Optional[type] = None
|
||||
) -> Union["Class", List["Function"]]:
|
||||
"""Construct a Function, Class or Enum for the given object.
|
||||
|
||||
Args:
|
||||
name: The name of the object to inspect.
|
||||
e: The object to inspect, or None to fetch it from the underlying list.
|
||||
|
||||
Returns: A Class object for the given type, or a list of function overloads.
|
||||
"""
|
||||
from .parser import make_enum, make_class, is_enum, make_functions
|
||||
|
||||
if e is None:
|
||||
e = self.raw_objects[name]
|
||||
|
||||
if name not in self.raw_objects:
|
||||
self.raw_objects[name] = e
|
||||
|
||||
if name not in self.objects:
|
||||
if is_enum(e):
|
||||
self.objects[name] = make_enum(name, e)
|
||||
elif isinstance(e, type):
|
||||
self.objects[name] = make_class(name, e, self)
|
||||
elif callable(e):
|
||||
self.objects[name] = make_functions(name, e)
|
||||
|
||||
return self.objects[name]
|
||||
|
||||
def get_object(self, name: str):
|
||||
"""
|
||||
Retrieve the object if the given name. Fails if no object with this
|
||||
name exists (if `make_object(name, ...)` has never been called).
|
||||
|
||||
Args:
|
||||
name: Name of the object to retrieve.
|
||||
|
||||
Returns:
|
||||
The object with the given name.
|
||||
"""
|
||||
return self.objects[name]
|
||||
|
||||
def register_type(self, ptype: "Type", ctype: "CType"):
|
||||
"""Register an equivalence between a python name and a C++ name.
|
||||
|
||||
Args:
|
||||
python_name: Name of the Python class.
|
||||
cpp_name: Name of the C++ class.
|
||||
"""
|
||||
# Register the const equivalent for smart pointers:
|
||||
cname = ctype.name
|
||||
if ctype.is_smart_pointer():
|
||||
if cname.find(" const >") != -1:
|
||||
c2name = cname.replace(" const >", ">")
|
||||
if c2name in self._cpptypes:
|
||||
ptype = self.cpp2py[c2name]
|
||||
# Not the const, replace the const one:
|
||||
else:
|
||||
c2name = cname.replace(">", " const >")
|
||||
if c2name in self._cpptypes and self.cpp2py[c2name].is_object():
|
||||
self._cpptypes[c2name] = ctype
|
||||
self.cpp2py[c2name] = ptype
|
||||
logger.warning(
|
||||
"Replace registration {} [c++] with {} [python] using {}"
|
||||
" information.".format(c2name, ptype.name, cname) # noqa: E501
|
||||
)
|
||||
|
||||
if cname not in MOBASE_REGISTER.cpp2py:
|
||||
self._cpptypes[cname] = ctype
|
||||
self.cpp2py[cname] = ptype
|
||||
logger.info("Registered {} [c++] as {} [python].".format(cname, ptype.name))
|
||||
|
||||
@property
|
||||
def py2cpp(self):
|
||||
result = {v.name: [] for v in self.cpp2py.values()}
|
||||
for k in self.cpp2py:
|
||||
result[self.cpp2py[k].name].append(self._cpptypes[k])
|
||||
return result
|
||||
|
||||
|
||||
MOBASE_REGISTER = MobaseRegister()
|
||||
@@ -1,470 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from typing import List, Dict, Any, Tuple, TextIO, Optional, Union, NamedTuple
|
||||
|
||||
from . import logger
|
||||
from . import register
|
||||
from . import mtypes
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Settings:
|
||||
class FunctionSettings(NamedTuple):
|
||||
|
||||
doc: str
|
||||
args: Optional[List["mtypes.Arg"]] = None
|
||||
ret: Optional["mtypes.Ret"] = None
|
||||
raises: List["mtypes.Exc"] = []
|
||||
static: Optional[bool] = None
|
||||
abstract: Optional[bool] = None
|
||||
deprecated: bool = False
|
||||
|
||||
register: "register.MobaseRegister"
|
||||
|
||||
# Name to ignore:
|
||||
_ignore_names: List[str]
|
||||
|
||||
# Extra replacements (with warnings):
|
||||
_replacements: Dict[str, str]
|
||||
|
||||
# Content of mobase:
|
||||
_mobase: Dict[str, Dict[str, Any]]
|
||||
|
||||
def __init__(
|
||||
self, register: "register.MobaseRegister", fp: Optional[TextIO] = None
|
||||
):
|
||||
|
||||
self.register = register
|
||||
|
||||
if fp is None:
|
||||
self._ignore_names = []
|
||||
self._replacements = {}
|
||||
self._mobase = {}
|
||||
else:
|
||||
data = yaml.load(fp, yaml.FullLoader)
|
||||
assert data["version"] == 1
|
||||
|
||||
self._ignore_names = data.get("ignores", [])
|
||||
self._replacements = data.get("replacements", {})
|
||||
self._mobase = data.get("mobase", {})
|
||||
|
||||
@property
|
||||
def ignore_names(self) -> List[str]:
|
||||
return self._ignore_names
|
||||
|
||||
@property
|
||||
def replacements(self) -> Dict[str, str]:
|
||||
return self._replacements
|
||||
|
||||
@property
|
||||
def mobase(self) -> Dict[str, Dict[str, Any]]:
|
||||
return self._mobase
|
||||
|
||||
def _get_class_settings(self, canonical_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Retrieve the settings for the given class.
|
||||
|
||||
Args:
|
||||
canonical_name: Canonical name of the class.
|
||||
|
||||
Returns:
|
||||
The settings for the corresponding class, or None if the
|
||||
settings where not found.
|
||||
"""
|
||||
parts = canonical_name.split(".")
|
||||
base: Dict[str, Any] = self._mobase
|
||||
for part in parts:
|
||||
if part in base:
|
||||
base = base[part]
|
||||
else:
|
||||
return None
|
||||
return base
|
||||
|
||||
def _parse_function_settings(
|
||||
self, settings: Union[str, Dict[str, Any]]
|
||||
) -> "FunctionSettings":
|
||||
"""Parse settings for a function or method.
|
||||
|
||||
Args:
|
||||
settings: Settings corresponding to a function.
|
||||
|
||||
Returns:
|
||||
The parsed settings for the function.
|
||||
"""
|
||||
FunctionSettings = Settings.FunctionSettings
|
||||
|
||||
if settings is None:
|
||||
return FunctionSettings("")
|
||||
|
||||
if isinstance(settings, str):
|
||||
return FunctionSettings(settings)
|
||||
|
||||
doc = settings.get("__doc__", "")
|
||||
static = settings.get("static", None)
|
||||
abstract = settings.get("abstract", None)
|
||||
deprecated = settings.get("deprecated", False)
|
||||
|
||||
# Arguments:
|
||||
args: Optional[List[mtypes.Arg]] = None
|
||||
|
||||
# If "args" is in settings, it can either be None (args: ) or
|
||||
# a list of args:
|
||||
if "args" in settings:
|
||||
args = []
|
||||
if settings["args"] is not None:
|
||||
|
||||
# For each argument, we either have a None value (name: ),
|
||||
# or a string (name: Description) or a dictionary that can contain
|
||||
# __doc__ and type.
|
||||
for aname, avalue in settings["args"].items():
|
||||
t: mtypes.Type = mtypes.Type("None")
|
||||
d: str = ""
|
||||
if avalue is None:
|
||||
pass
|
||||
elif isinstance(avalue, str):
|
||||
d = avalue
|
||||
else:
|
||||
d = avalue.get("__doc__", "")
|
||||
t = mtypes.Type(avalue.get("type", "None"))
|
||||
args.append(mtypes.Arg(aname, t, doc=d))
|
||||
|
||||
ret: Optional[mtypes.Ret] = None
|
||||
if "returns" in settings and settings["returns"] is not None:
|
||||
if isinstance(settings["returns"], str):
|
||||
ret = mtypes.Ret(mtypes.Type("None"), settings["returns"])
|
||||
else:
|
||||
ret = mtypes.Ret(
|
||||
mtypes.Type(settings["returns"].get("type", "None")),
|
||||
settings["returns"].get("__doc__", ""),
|
||||
)
|
||||
|
||||
excs: List[mtypes.Exc] = []
|
||||
if "raises" in settings and settings["raises"] is not None:
|
||||
for r, v in settings["raises"].items():
|
||||
if v is None:
|
||||
v = ""
|
||||
excs.append(mtypes.Exc(mtypes.Type(r), v))
|
||||
|
||||
return FunctionSettings(doc, args, ret, excs, static, abstract, deprecated)
|
||||
|
||||
def patch_functions(self, fns: List["mtypes.Function"]):
|
||||
for i, fn in enumerate(fns):
|
||||
|
||||
# Find the name in settings:
|
||||
if fn.has_overloads():
|
||||
sname = "{}.{}".format(fn.name, i + 1)
|
||||
else:
|
||||
sname = fn.name
|
||||
|
||||
# If the name is in the settings:
|
||||
if sname in self._mobase:
|
||||
fsettings = self._parse_function_settings(self._mobase[sname])
|
||||
|
||||
# Force raises:
|
||||
fn.raises = fsettings.raises
|
||||
|
||||
# Check the args:
|
||||
if fsettings.args is not None:
|
||||
if len(fsettings.args) != len(fn.args):
|
||||
logger.warn(
|
||||
"Mismatch number of arguments for function mobase.{}."
|
||||
.format(sname) # noqa
|
||||
)
|
||||
|
||||
for sarg, marg in zip(fsettings.args, fn.args):
|
||||
marg.doc = sarg.doc
|
||||
if not sarg.type.is_none():
|
||||
marg.type = sarg.type
|
||||
|
||||
# Check the return type:
|
||||
if fsettings.ret is not None:
|
||||
|
||||
# Force the doc anyway:
|
||||
fn.ret.doc = fsettings.ret.doc
|
||||
|
||||
# Update the type if specified:
|
||||
if not fsettings.ret.type.is_none():
|
||||
fn.ret.type = fsettings.ret.type
|
||||
|
||||
# Force the doc:
|
||||
fn.doc = fsettings.doc
|
||||
|
||||
# Force depreciation:
|
||||
fn.deprecated = fsettings.deprecated
|
||||
|
||||
else:
|
||||
logger.warn("Missing settings for function mobase.{}.".format(sname))
|
||||
|
||||
def patch_class(self, cls: "mtypes.Class"):
|
||||
"""Patch the given class using the given overwrites.
|
||||
|
||||
See config.json for some examples of valid overwrites.
|
||||
|
||||
Args:
|
||||
cls: The class to patch.
|
||||
settings: The settings.
|
||||
"""
|
||||
|
||||
logger.info("Patching class {}.".format(cls.name))
|
||||
|
||||
# Find the class in mobase:
|
||||
csettings = self._get_class_settings(cls.canonical_name)
|
||||
|
||||
if csettings is None:
|
||||
logger.warn("Class {} not found in settings.".format(cls.canonical_name))
|
||||
return
|
||||
|
||||
if "__doc__" in csettings and csettings["__doc__"] is not None:
|
||||
cls.doc = csettings["__doc__"]
|
||||
csettings.pop("__doc__", None)
|
||||
|
||||
# Check bases:
|
||||
if "__bases__" in csettings:
|
||||
for bc in csettings["__bases__"]:
|
||||
if bc.startswith("PyQt"):
|
||||
cls.bases.append(mtypes.PyClass(bc))
|
||||
else:
|
||||
cls.bases.append(self.register.get_object(bc))
|
||||
del csettings["__bases__"]
|
||||
|
||||
if "__abstract__" in csettings and csettings["__abstract__"]:
|
||||
cls.abstract = True
|
||||
del csettings["__abstract__"]
|
||||
|
||||
# Patch properties - Everything should be in config since property are poorly
|
||||
# documented by boost::python.
|
||||
properties: Dict[str, Any] = csettings.pop("properties[]", {})
|
||||
for prop in cls.properties:
|
||||
if prop.name in properties:
|
||||
sprop = properties[prop.name]
|
||||
|
||||
# If we have a type:
|
||||
if "type" in sprop:
|
||||
prop.type = mtypes.Type(sprop["type"])
|
||||
else:
|
||||
logger.warn(
|
||||
"Missing type for property {}.{}.".format(
|
||||
cls.canonical_name, prop.name
|
||||
)
|
||||
)
|
||||
|
||||
# If we have a description:
|
||||
if "desc" in sprop:
|
||||
|
||||
# If desc is None, we do not warn user, because the entry is in
|
||||
# settings, just empty:
|
||||
if sprop["desc"] is not None:
|
||||
prop.doc = sprop["desc"]
|
||||
|
||||
else:
|
||||
logger.warn(
|
||||
"Missing description for property {}.{}.".format(
|
||||
cls.canonical_name, prop.name
|
||||
)
|
||||
)
|
||||
|
||||
# Patch signals - Everything should be in config since signals are not really
|
||||
# exposed by boost::python.
|
||||
signals: List[str] = csettings.pop("signals[]", [])
|
||||
for signal in signals:
|
||||
cls.constants.append(
|
||||
mtypes.Constant(signal, mtypes.Type("pyqtSignal"), None)
|
||||
)
|
||||
|
||||
# List of all items in csettings:
|
||||
keys = {k: False for k in csettings}
|
||||
|
||||
# Group method by name:
|
||||
methods: Dict[str, List[mtypes.Method]] = defaultdict(list)
|
||||
for m in cls.methods:
|
||||
methods[m.name].append(m)
|
||||
|
||||
for k, ms in methods.items():
|
||||
for i, m in enumerate(ms):
|
||||
|
||||
# Find the name in settings:
|
||||
if m.has_overloads():
|
||||
sname = "{}.{}".format(m.name, i + 1)
|
||||
else:
|
||||
sname = m.name
|
||||
|
||||
# If the name is in the settings:
|
||||
if sname in csettings:
|
||||
keys[sname] = True
|
||||
fsettings = self._parse_function_settings(csettings[sname])
|
||||
|
||||
# Force raises:
|
||||
m.raises = fsettings.raises
|
||||
|
||||
# Force static:
|
||||
if fsettings.static is not None:
|
||||
if m.is_static() != fsettings.static:
|
||||
logger.warn(
|
||||
"Forcing method {}.{} to be {}.".format(
|
||||
cls.canonical_name,
|
||||
sname,
|
||||
"static" if fsettings.static else "non static",
|
||||
)
|
||||
)
|
||||
m.static = fsettings.static
|
||||
|
||||
if fsettings.abstract is not None:
|
||||
m.abstract = fsettings.abstract
|
||||
|
||||
# Check the args:
|
||||
if fsettings.args is not None:
|
||||
margs = m.args if m.is_static() else m.args[1:]
|
||||
if len(fsettings.args) != len(margs):
|
||||
logger.warn(
|
||||
"Mismatch number of arguments for method {}.{}.".format(
|
||||
cls.canonical_name, sname
|
||||
)
|
||||
)
|
||||
|
||||
for sarg, marg in zip(fsettings.args, margs):
|
||||
marg.doc = sarg.doc
|
||||
marg.name = sarg.name
|
||||
if not sarg.type.is_none():
|
||||
marg.type = sarg.type
|
||||
|
||||
# Check the return type:
|
||||
if fsettings.ret is not None:
|
||||
|
||||
# Force the doc anyway:
|
||||
m.ret.doc = fsettings.ret.doc
|
||||
|
||||
# Update the type if specified:
|
||||
if not fsettings.ret.type.is_none():
|
||||
m.ret.type = fsettings.ret.type
|
||||
|
||||
# Force the doc:
|
||||
m.doc = fsettings.doc
|
||||
|
||||
# Force deprecated:
|
||||
m.deprecated = fsettings.deprecated
|
||||
|
||||
# Only warn for "normal" methods:
|
||||
elif not m.name.startswith("__"):
|
||||
logger.warn(
|
||||
"Missing settings for method {}.{}.".format(
|
||||
cls.canonical_name, sname
|
||||
)
|
||||
)
|
||||
|
||||
# Remove the deprecated methods:
|
||||
noverloads = 0
|
||||
for m in ms:
|
||||
if m.is_deprecated():
|
||||
cls.methods.remove(m)
|
||||
else:
|
||||
noverloads += 1
|
||||
|
||||
for m in ms:
|
||||
m.overloads = noverloads > 1
|
||||
|
||||
# Patch inner classes:
|
||||
for ic in cls.inner_classes:
|
||||
keys[ic.name] = True
|
||||
self.patch_class(ic)
|
||||
|
||||
# Mark the constant:
|
||||
for cc in cls.constants:
|
||||
keys[cc.name] = True
|
||||
|
||||
# Print items missing in mobase
|
||||
missings = [k for k, v in keys.items() if not v]
|
||||
if missings:
|
||||
logger.warn(
|
||||
"The following members were found in settings but not in the actual"
|
||||
" class {}: {}.".format(cls.canonical_name, ", ".join(missings)) # noqa
|
||||
)
|
||||
|
||||
|
||||
def clean_class(cls: "mtypes.Class", settings: Settings):
|
||||
"""Clean the given class object.
|
||||
|
||||
Args:
|
||||
cls: The class object to clean.
|
||||
settings: The settings.
|
||||
"""
|
||||
from .register import MOBASE_REGISTER
|
||||
|
||||
# Remove duplicate methods (based on name and argument types):
|
||||
methods: Dict[
|
||||
Tuple[str, Tuple[mtypes.Arg, ...]], List[mtypes.Method]
|
||||
] = OrderedDict()
|
||||
methods_by_name = defaultdict(list)
|
||||
for m in cls.methods:
|
||||
k = (m.name, tuple(m.args[1:]))
|
||||
if k not in methods:
|
||||
methods[k] = []
|
||||
methods[k].append(m)
|
||||
methods_by_name[m.name].append(m)
|
||||
|
||||
clean_methods: List[mtypes.Method] = []
|
||||
for name, args in methods:
|
||||
ms = methods[name, args]
|
||||
method: mtypes.Method = ms[0]
|
||||
if len(ms) > 1:
|
||||
|
||||
# If we have more than two methods, there is a problem...
|
||||
assert len(methods[name, args]) == 2
|
||||
assert (
|
||||
ms[0].ret.type.is_none()
|
||||
or ms[1].ret.type.is_none()
|
||||
or ms[0].ret.type.name == ms[1].ret.type.name
|
||||
)
|
||||
|
||||
if ms[0].ret.type.is_none():
|
||||
# If both are None, we need to take the first one because the second
|
||||
# one does not contains the name of the arguments, for whatever reason.
|
||||
if ms[1].ret.type.is_none():
|
||||
method = ms[0]
|
||||
else:
|
||||
method = ms[1]
|
||||
else:
|
||||
method = ms[0]
|
||||
|
||||
# If those were the only two, we need to remove the overload:
|
||||
if len(methods_by_name[name]) == 2:
|
||||
method.overloads = False
|
||||
|
||||
# Filter methods from parent class:
|
||||
if method.is_static():
|
||||
clean_methods.append(method)
|
||||
else:
|
||||
arg0_name = method.args[0].type.name
|
||||
if arg0_name in MOBASE_REGISTER.cpp2py:
|
||||
arg0_name = MOBASE_REGISTER.cpp2py[arg0_name].name
|
||||
if arg0_name in [cls.name, "object"]:
|
||||
clean_methods.append(method)
|
||||
else:
|
||||
logger.info(
|
||||
"Removing {}({}) from {} (already in base {}).".format(
|
||||
name,
|
||||
", ".join(a.type.typing(settings) for a in args),
|
||||
cls.name,
|
||||
method.args[0].type.typing(settings),
|
||||
)
|
||||
)
|
||||
|
||||
# We need to filter-out __eq__(X, object) and __ne__(X, object) because these won't
|
||||
# be filtered since the first arg is not of the right type:
|
||||
for name in ("__eq__", "__ne__"):
|
||||
fns = [m for m in clean_methods if m.name == name]
|
||||
if len(fns) == 1 and fns[0].args[1].type.is_object():
|
||||
clean_methods.remove(fns[0])
|
||||
|
||||
cls.methods = clean_methods
|
||||
|
||||
# Remove all non-uppercases enum names - This is a temporary fix to avoid breaking
|
||||
# old plugins that uses old enum values:
|
||||
if isinstance(cls, mtypes.Enum):
|
||||
newc = [c for c in cls.constants if c.name.isupper()]
|
||||
if newc:
|
||||
cls.constants = newc
|
||||
|
||||
# Clean inner classes:
|
||||
for ic in cls.inner_classes:
|
||||
clean_class(ic, settings)
|
||||
@@ -1,167 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import black
|
||||
|
||||
from generator import logger
|
||||
from generator.loader import load_mobase
|
||||
from generator.register import MOBASE_REGISTER
|
||||
from generator.parser import is_enum
|
||||
from generator.mtypes import Type, Class, Function
|
||||
from generator.utils import Settings, clean_class
|
||||
from generator.writer import Writer
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser("Stubs generator for the MO2 python interface")
|
||||
parser.add_argument(
|
||||
"install_dir",
|
||||
metavar="INSTALL_DIR",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="installation directory of Mod Organizer 2",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=Path,
|
||||
default=sys.stdout,
|
||||
help="output file (output to stdout if not specified)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="verbose mode (all logs go to stderr)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=argparse.FileType("r"),
|
||||
default=None,
|
||||
help="configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Load settings from the configuration:
|
||||
settings: Settings = Settings(register=MOBASE_REGISTER)
|
||||
if args.config is not None:
|
||||
settings = Settings(MOBASE_REGISTER, args.config)
|
||||
|
||||
# Parse mobase:
|
||||
|
||||
# Load mobase (cannot simply do "import mobase"):
|
||||
mobase = load_mobase(Path(args.install_dir))
|
||||
|
||||
# List of objects:
|
||||
objects = []
|
||||
|
||||
for name in dir(mobase):
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
|
||||
if name in settings.ignore_names:
|
||||
continue
|
||||
|
||||
# We do not want the real MoVariant.
|
||||
if name == "MoVariant":
|
||||
continue
|
||||
|
||||
objects.append((name, getattr(mobase, name)))
|
||||
|
||||
# Enum first, and then alphabetical. Might cause issue with base classes, so
|
||||
# maybe create a kind of dependency...
|
||||
# For argument or return types, this should not be an issue since we quote
|
||||
# everything from mobase.
|
||||
objects = sorted(
|
||||
objects, key=lambda e: (isinstance(e[1], type), not is_enum(e[1]), e[0])
|
||||
)
|
||||
|
||||
for n, o in objects:
|
||||
MOBASE_REGISTER.add_object(n, o)
|
||||
|
||||
# Process everything:
|
||||
for n, o in objects:
|
||||
|
||||
# Create the corresponding object:
|
||||
c = MOBASE_REGISTER.make_object(n, o)
|
||||
|
||||
if isinstance(c, Class):
|
||||
|
||||
# Clean the class (e.g., remove duplicates methods due to wrappers):
|
||||
clean_class(c, settings)
|
||||
|
||||
# Path the class using the configuration:
|
||||
settings.patch_class(c)
|
||||
|
||||
elif isinstance(c, list) and isinstance(c[0], Function):
|
||||
settings.patch_functions(c)
|
||||
|
||||
else:
|
||||
logger.critical(
|
||||
"Cannot generated stubs for {}, unsupported object type.".format(n)
|
||||
)
|
||||
|
||||
# Write everything:
|
||||
with open(args.output, "w") as output:
|
||||
|
||||
writer = Writer(output, settings)
|
||||
writer.print_version(settings.mobase["__version__"]) # type: ignore
|
||||
writer.print_imports(
|
||||
[
|
||||
"abc",
|
||||
("enum", ["Enum"]),
|
||||
(
|
||||
"typing",
|
||||
[
|
||||
"Dict",
|
||||
"Iterator",
|
||||
"List",
|
||||
"Tuple",
|
||||
"Union",
|
||||
"Any",
|
||||
"Optional",
|
||||
"Callable",
|
||||
"overload",
|
||||
"TypeVar",
|
||||
"Type",
|
||||
],
|
||||
),
|
||||
"PyQt5.QtCore",
|
||||
"PyQt5.QtGui",
|
||||
"PyQt5.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
# Needs to define the MVariant and GameFeatureType type:
|
||||
writer._print("MoVariant = {}".format(Type.MO_VARIANT))
|
||||
writer._print('GameFeatureType = TypeVar("GameFeatureType")')
|
||||
writer._print()
|
||||
|
||||
# This is a class to represent interface not implemented:
|
||||
writer.print_class(Class("InterfaceNotImplemented", [], []))
|
||||
writer._print()
|
||||
|
||||
for n, o in objects:
|
||||
|
||||
# Get the corresponding object:
|
||||
c = MOBASE_REGISTER.get_object(n)
|
||||
|
||||
if isinstance(c, Class):
|
||||
writer.print_class(c)
|
||||
|
||||
elif isinstance(c, list) and isinstance(c[0], Function):
|
||||
for fn in c:
|
||||
writer.print_function(fn)
|
||||
|
||||
black.format_file_in_place(
|
||||
args.output,
|
||||
fast=False,
|
||||
mode=black.Mode(is_pyi=args.output.name.endswith("pyi")),
|
||||
write_back=black.WriteBack.YES,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user