mirror of
https://github.com/ModOrganizer2/pystubs-generation.git
synced 2026-07-27 14:07:13 -07:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Build Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,22 +11,24 @@ 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/"
|
||||
|
||||
- name: Install SSH Client 🔑
|
||||
uses: webfactory/ssh-agent@v0.2.0
|
||||
uses: webfactory/ssh-agent@v0.4.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.1
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
SSH: true
|
||||
REPOSITORY_NAME: ModOrganizer2/python-plugins-doc
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags: ["*.dev[0-9]+"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -18,13 +18,13 @@ jobs:
|
||||
uses: frabert/replace-string-action@v1.1
|
||||
id: version
|
||||
with:
|
||||
string: ${{ github.event.release.tag_name }}
|
||||
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.8'
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -36,5 +36,6 @@ jobs:
|
||||
run: |
|
||||
cd stubs/setup
|
||||
cp ../${{ steps.version.outputs.replaced }}/mobase.pyi mobase-stubs/__init__.pyi
|
||||
sed -i 's/__version__ = ".*"/__version__ = "${{ github.ref_name }}"/' mobase-stubs/__init__.pyi
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
twine upload dist/*
|
||||
|
||||
@@ -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
|
||||
@@ -31,35 +31,35 @@ 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.
|
||||
- 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., `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 `python38.dll` in your MO2 installation path, then you need **Python 3.8**.
|
||||
|
||||
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}
|
||||
python main.py -c configs\config-2.4.yml ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
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`.
|
||||
The stubs are generated under `stubs/setup/mobase-stubs/__init__.pyi`, you
|
||||
can change the output file by using the `-o` option
|
||||
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`.
|
||||
|
||||
A few options are available for `main.py`:
|
||||
|
||||
@@ -72,16 +72,18 @@ positional arguments:
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-o OUTPUT, --output OUTPUT
|
||||
output file (output to stdout if not specified)
|
||||
output file (default stubs/setup/mobase-stubs/__init__.pyi)
|
||||
-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:
|
||||
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.
|
||||
For instance, here is the output with the current `config-2.4.yml` file:
|
||||
|
||||
```
|
||||
WARNING: Replacing IOrganizer::FileInfo with FileInfo.
|
||||
@@ -90,33 +92,18 @@ 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`).
|
||||
As you can see, only a few types were manually fixed (specified in
|
||||
`config-2.4.yml`).
|
||||
|
||||
## 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:
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
You need to set the environment variables `TWINE_USERNAME` and `TWINE_PASSWORD` to appropriate values
|
||||
before running `twine upload`.
|
||||
The upload of the stubs to https://pypi.org/project/mobase-stubs/ should be
|
||||
done automatically when a new Github release is made.
|
||||
|
||||
## Extras — Starts a python interpreter with `mobase`
|
||||
|
||||
@@ -138,4 +125,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).
|
||||
|
||||
+200
-78
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-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 -----------------------------------------------------
|
||||
|
||||
+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
|
||||
|
||||
@@ -226,6 +226,8 @@ These plugins are not as well documented as the ones in the repository above.
|
||||
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.
|
||||
@@ -243,7 +245,7 @@ These plugins are not as well documented as the ones in the repository above.
|
||||
Unofficial Plugins
|
||||
..................
|
||||
|
||||
These plugins have been created by developpers for MO2 and are usually distributed on Nexus.
|
||||
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``.
|
||||
@@ -254,4 +256,67 @@ These plugins have been created by developpers for MO2 and are usually distribut
|
||||
- `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.*
|
||||
*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 +1,5 @@
|
||||
# -*- 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)
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
+11
-28
@@ -1,22 +1,20 @@
|
||||
# -*- 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
|
||||
def load_mobase(path: Path):
|
||||
"""
|
||||
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.
|
||||
path: Path to the MO2 installation (folder containing the ModOrganizer.exe).
|
||||
|
||||
Returns: The mobase module. """
|
||||
Returns: The mobase module.
|
||||
"""
|
||||
|
||||
# We need absolute path for loading DLL and modules:
|
||||
path = path.resolve()
|
||||
@@ -32,21 +30,12 @@ def load_mobase(path: Path, moprivate: bool = False):
|
||||
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())
|
||||
# We need to add plugins/data to sys.path, mainly for PyQt6
|
||||
sys.path.insert(1, path.joinpath("plugins", "plugin_python", "libs").as_posix())
|
||||
|
||||
mobase = importlib.machinery.ExtensionFileLoader(
|
||||
"mobase", path.joinpath("plugins", "data", "pythonrunner.dll").as_posix()
|
||||
).load_module()
|
||||
import mobase
|
||||
|
||||
if not moprivate:
|
||||
return mobase
|
||||
|
||||
moprivate = importlib.machinery.ExtensionFileLoader(
|
||||
"moprivate", path.joinpath("plugins", "data", "pythonrunner.dll").as_posix()
|
||||
).load_module()
|
||||
|
||||
return mobase, moprivate
|
||||
return mobase
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -63,13 +52,7 @@ if __name__ == "__main__":
|
||||
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)
|
||||
mobase = load_mobase(args.install_dir)
|
||||
|
||||
+136
-345
File diff suppressed because it is too large
Load Diff
+179
-396
File diff suppressed because it is too large
Load Diff
+16
-57
@@ -1,40 +1,38 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, Union, List
|
||||
from __future__ import annotations
|
||||
|
||||
from . import logger
|
||||
from .mtypes import Class, Type, CType, Function
|
||||
from collections import OrderedDict
|
||||
|
||||
from .mtypes import Class, Function
|
||||
|
||||
|
||||
class MobaseRegister:
|
||||
""" Class that register class. """
|
||||
"""
|
||||
Class that register classes.
|
||||
"""
|
||||
|
||||
objects: Dict[str, Union[Class, List[Function]]]
|
||||
objects: dict[str, Class | list[Function]]
|
||||
|
||||
def __init__(self):
|
||||
""" Create a new register with the list of objects. """
|
||||
self.raw_objects: Dict[str, Union[type]] = OrderedDict()
|
||||
self.raw_objects: dict[str, 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.
|
||||
def make_object(self, name: str, e: type | None = None) -> 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.
|
||||
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
|
||||
from .parser import make_class, make_functions
|
||||
|
||||
if e is None:
|
||||
e = self.raw_objects[name]
|
||||
@@ -43,9 +41,7 @@ class MobaseRegister:
|
||||
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):
|
||||
if isinstance(e, type):
|
||||
self.objects[name] = make_class(name, e, self)
|
||||
elif callable(e):
|
||||
self.objects[name] = make_functions(name, e)
|
||||
@@ -65,42 +61,5 @@ class MobaseRegister:
|
||||
"""
|
||||
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()
|
||||
|
||||
+278
-191
File diff suppressed because it is too large
Load Diff
+49
-40
@@ -1,9 +1,9 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from typing import TextIO, List, Union, Tuple
|
||||
from typing import TextIO
|
||||
|
||||
from . import logger
|
||||
from .mtypes import Function, Class, Method, Property, Enum
|
||||
from . import LOGGER
|
||||
from .mtypes import Class, Enum, Function, Method, Property
|
||||
from .utils import Settings
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ class Writer:
|
||||
print(*args, **kwargs)
|
||||
|
||||
def _print_doc(self, doc: str, indent: str):
|
||||
"""Print the given documentation at the given indentaiton level.
|
||||
"""
|
||||
Print the given documentation at the given indentation level.
|
||||
|
||||
Args:
|
||||
doc: Documentation to print.
|
||||
@@ -35,8 +36,10 @@ class Writer:
|
||||
self._print('__version__ = "{}"'.format(version))
|
||||
self._print()
|
||||
|
||||
def print_imports(self, imports: List[Union[str, Tuple[str, List[str]]]]):
|
||||
""" Print the given imports. """
|
||||
def print_imports(self, imports: list[str | tuple[str, list[str]]]):
|
||||
"""
|
||||
Print the given imports.
|
||||
"""
|
||||
for imp in imports:
|
||||
if isinstance(imp, str):
|
||||
self._print("import {}".format(imp))
|
||||
@@ -45,17 +48,17 @@ class Writer:
|
||||
self._print()
|
||||
|
||||
def print_function(self, fn: Function, indent: str = ""):
|
||||
""" Print the given Function object at the given indentation level. """
|
||||
"""
|
||||
Print the given Function object at the given indentation level.
|
||||
"""
|
||||
|
||||
if fn.has_overloads():
|
||||
self._print("{}@overload".format(indent))
|
||||
|
||||
srtype = ""
|
||||
sig_return_type = ""
|
||||
if not fn.ret.type.is_none():
|
||||
srtype = " -> " + fn.ret.type.typing(self._settings)
|
||||
sig_return_type = " -> " + fn.ret.type.typing()
|
||||
|
||||
fargs = fn.args
|
||||
largs: List[str] = []
|
||||
if isinstance(fn, Method):
|
||||
if fn.is_static():
|
||||
self._print("{}@staticmethod".format(indent))
|
||||
@@ -63,17 +66,19 @@ class Writer:
|
||||
if fn.is_abstract():
|
||||
self._print("{}@abc.abstractmethod".format(indent))
|
||||
|
||||
largs.insert(0, "self")
|
||||
fargs = fargs[1:]
|
||||
|
||||
for i, arg in enumerate(fargs):
|
||||
tmp = "{}: {}".format(arg.name, arg.type.typing(self._settings))
|
||||
python_args: list[str] = []
|
||||
for i, arg in enumerate(fn.args):
|
||||
tmp = "{}: {}".format(arg.name, arg.type.typing())
|
||||
if arg.has_default_value():
|
||||
tmp += " = {}".format(arg.value)
|
||||
largs.append(tmp)
|
||||
sargs = ", ".join(largs)
|
||||
python_args.append(tmp)
|
||||
|
||||
self._print("{}def {}({}){}:".format(indent, fn.name, sargs, srtype), end="")
|
||||
self._print(
|
||||
"{}def {}({}){}:".format(
|
||||
indent, fn.name, ", ".join(python_args), sig_return_type
|
||||
),
|
||||
end="",
|
||||
)
|
||||
|
||||
# Add the documentation, if any:
|
||||
doc = ""
|
||||
@@ -88,9 +93,12 @@ class Writer:
|
||||
if any(arg.doc for arg in args):
|
||||
doc += "\nArgs:\n"
|
||||
for arg in args:
|
||||
adocl = arg.doc.strip().split("\n")
|
||||
adoc = "\n".join([adocl[0]] + [" " + ldoc for ldoc in adocl[1:]])
|
||||
doc += " " + arg.name + ": " + adoc + "\n"
|
||||
arg_doc_list = arg.doc.strip().split("\n")
|
||||
arg_doc = "\n".join(
|
||||
[arg_doc_list[0]]
|
||||
+ [" " + line_doc for line_doc in arg_doc_list[1:]]
|
||||
)
|
||||
doc += " " + arg.name + ": " + arg_doc + "\n"
|
||||
|
||||
if not fn.ret.type.is_none() and fn.ret.doc:
|
||||
doc += "\nReturns:\n " + fn.ret.doc.strip() + "\n"
|
||||
@@ -98,13 +106,7 @@ class Writer:
|
||||
if fn.raises:
|
||||
doc += "\nRaises:\n"
|
||||
for rai in fn.raises:
|
||||
doc += (
|
||||
" "
|
||||
+ rai.type.typing(self._settings)
|
||||
+ ": "
|
||||
+ rai.doc.strip()
|
||||
+ "\n"
|
||||
)
|
||||
doc += " " + rai.type.typing() + ": " + rai.doc.strip() + "\n"
|
||||
|
||||
if doc:
|
||||
self._print()
|
||||
@@ -117,10 +119,12 @@ class Writer:
|
||||
self._print()
|
||||
|
||||
def print_property(self, cls: Class, prop: Property, indent: str):
|
||||
""" Print the given Property object at the given indentation level. """
|
||||
"""
|
||||
Print the given Property object at the given indentation level.
|
||||
"""
|
||||
|
||||
if prop.type.is_object() or prop.type.is_any():
|
||||
logger.warning(
|
||||
LOGGER.warning(
|
||||
"Property {}.{} does not have a specified type.".format(
|
||||
cls.name, prop.name
|
||||
)
|
||||
@@ -128,21 +132,21 @@ class Writer:
|
||||
|
||||
self._print("{}@property".format(indent))
|
||||
self._print(
|
||||
"{}def {}(self) -> {}: ...".format(
|
||||
indent, prop.name, prop.type.typing(self._settings)
|
||||
)
|
||||
"{}def {}(self) -> {}: ...".format(indent, prop.name, prop.type.typing())
|
||||
)
|
||||
if not prop.is_read_only():
|
||||
self._print("{}@{}.setter".format(indent, prop.name))
|
||||
self._print(
|
||||
"{}def {}(self, arg0: {}): ...".format(
|
||||
indent, prop.name, prop.type.typing(self._settings)
|
||||
indent, prop.name, prop.type.typing()
|
||||
)
|
||||
)
|
||||
self._print()
|
||||
|
||||
def print_class(self, cls: Class, indent: str = ""):
|
||||
""" Print the given Class object at the given indentation level. """
|
||||
"""
|
||||
Print the given Class object at the given indentation level.
|
||||
"""
|
||||
|
||||
bc = ""
|
||||
if cls.bases or cls.is_abstract():
|
||||
@@ -170,8 +174,8 @@ class Writer:
|
||||
self._print()
|
||||
|
||||
# Inner classes:
|
||||
for iclass in cls.inner_classes:
|
||||
self.print_class(iclass, indent=indent + " ")
|
||||
for inner_class in cls.inner_classes:
|
||||
self.print_class(inner_class, indent=indent + " ")
|
||||
self._print()
|
||||
|
||||
# Constants:
|
||||
@@ -182,12 +186,16 @@ class Writer:
|
||||
|
||||
typing = ""
|
||||
if constant.type is not None:
|
||||
typing = ": {}".format(constant.type.typing(self._settings))
|
||||
typing = ": {}".format(constant.type.typing())
|
||||
|
||||
# Note: We do not print the value, we use ...
|
||||
self._print(
|
||||
"{}{}{} = {}{}".format(
|
||||
indent + " ", constant.name, typing, "...", comment,
|
||||
indent + " ",
|
||||
constant.name,
|
||||
typing,
|
||||
"...",
|
||||
comment,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -204,6 +212,7 @@ class Writer:
|
||||
cls.methods,
|
||||
key=lambda m: (m.name != "__init__", not m.is_special(), m.name),
|
||||
)
|
||||
|
||||
for method in methods:
|
||||
self.print_function(method, indent=indent + " ")
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from generator import logger
|
||||
from generator import LOGGER
|
||||
from generator.loader import load_mobase
|
||||
from generator.register import MOBASE_REGISTER
|
||||
from generator.mtypes import Class, Function, PyType
|
||||
from generator.parser import is_enum
|
||||
from generator.mtypes import Type, Class, Function
|
||||
from generator.register import MOBASE_REGISTER
|
||||
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",
|
||||
@@ -29,8 +28,8 @@ parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=Path,
|
||||
default=sys.stdout,
|
||||
help="output file (output to stdout if not specified)",
|
||||
default="stubs/setup/mobase-stubs/__init__.pyi",
|
||||
help="output file (default stubs/setup/mobase-stubs/__init__.pyi)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="verbose mode (all logs go to stderr)"
|
||||
@@ -45,8 +44,13 @@ parser.add_argument(
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig()
|
||||
LOGGER.setLevel(logging.WARNING)
|
||||
|
||||
if args.verbose:
|
||||
logger.setLevel(logging.INFO)
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
output_path = cast(Path, args.output)
|
||||
|
||||
# Load settings from the configuration:
|
||||
settings: Settings = Settings(register=MOBASE_REGISTER)
|
||||
@@ -68,16 +72,26 @@ for name in dir(mobase):
|
||||
if name in settings.ignore_names:
|
||||
continue
|
||||
|
||||
# We do not want the real MoVariant.
|
||||
# we do not want the real MoVariant
|
||||
if name == "MoVariant":
|
||||
continue
|
||||
|
||||
# ignore the private module
|
||||
if name == "private":
|
||||
continue
|
||||
|
||||
# for now, ignore this since it is a submodule and we
|
||||
# not handle them
|
||||
if name == "widgets":
|
||||
continue
|
||||
|
||||
# IPlugin is not the real object
|
||||
if name == "IPlugin":
|
||||
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.
|
||||
# enum first, and then alphabetical, should be fine with the __future__ import
|
||||
objects = sorted(
|
||||
objects, key=lambda e: (isinstance(e[1], type), not is_enum(e[1]), e[0])
|
||||
)
|
||||
@@ -103,7 +117,7 @@ for n, o in objects:
|
||||
settings.patch_functions(c)
|
||||
|
||||
else:
|
||||
logger.critical(
|
||||
LOGGER.critical(
|
||||
"Cannot generated stubs for {}, unsupported object type.".format(n)
|
||||
)
|
||||
|
||||
@@ -111,11 +125,15 @@ for n, o in objects:
|
||||
with open(args.output, "w") as output:
|
||||
|
||||
writer = Writer(output, settings)
|
||||
|
||||
# the __future__ import must be at the beginning
|
||||
writer.print_imports([("__future__", ["annotations"])])
|
||||
writer.print_version(settings.mobase["__version__"]) # type: ignore
|
||||
writer.print_imports(
|
||||
[
|
||||
"abc",
|
||||
("enum", ["Enum"]),
|
||||
("pathlib", ["Path"]),
|
||||
(
|
||||
"typing",
|
||||
[
|
||||
@@ -128,18 +146,22 @@ with open(args.output, "w") as output:
|
||||
"Optional",
|
||||
"Callable",
|
||||
"overload",
|
||||
"Sequence",
|
||||
"Set",
|
||||
"TypeVar",
|
||||
"Type",
|
||||
],
|
||||
),
|
||||
"PyQt5.QtCore",
|
||||
"PyQt5.QtGui",
|
||||
"PyQt5.QtWidgets",
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
# Needs to define the MVariant and GameFeatureType type:
|
||||
writer._print("MoVariant = {}".format(Type.MO_VARIANT))
|
||||
writer._print(f"MoVariant = {PyType.MO_VARIANT}")
|
||||
writer._print(f"FileWrapper = {PyType.FILE_WRAPPER}")
|
||||
writer._print(f"DirectoryWrapper = {PyType.DIRECTORY_WRAPPER}")
|
||||
writer._print('GameFeatureType = TypeVar("GameFeatureType")')
|
||||
writer._print()
|
||||
|
||||
@@ -160,8 +182,9 @@ with open(args.output, "w") as output:
|
||||
writer.print_function(fn)
|
||||
|
||||
black.format_file_in_place(
|
||||
args.output,
|
||||
output_path,
|
||||
fast=False,
|
||||
mode=black.Mode(is_pyi=args.output.name.endswith("pyi")),
|
||||
write_back=black.WriteBack.YES,
|
||||
)
|
||||
isort.api.sort_file(output_path)
|
||||
|
||||
@@ -14,3 +14,27 @@ per-file-ignores =
|
||||
warn_return_any = True
|
||||
warn_unused_configs = True
|
||||
namespace_packages = True
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
multi_line_output = 3
|
||||
|
||||
[tox:tox]
|
||||
skipsdist = true
|
||||
envlist = py310-lint
|
||||
|
||||
[testenv:py310-lint]
|
||||
skip_install = true
|
||||
deps =
|
||||
black
|
||||
mypy
|
||||
flake8
|
||||
flake8-black
|
||||
git+https://github.com/TilmanK/PyQt6-stubs.git
|
||||
types-PyYAML
|
||||
isort
|
||||
commands =
|
||||
black generator main.py --check --diff
|
||||
flake8 generator main.py
|
||||
mypy generator main.py
|
||||
isort -c generator main.py
|
||||
|
||||
+304
-117
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user