mirror of
https://github.com/ModOrganizer2/pystubs-generation.git
synced 2026-07-27 14:07:13 -07:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5c16af21 | ||
|
|
3eb1daa053 | ||
|
|
0eca0f7644 | ||
|
|
7467a2b11c | ||
|
|
eb420fcd79 | ||
|
|
156b827389 | ||
|
|
104e8612d8 | ||
|
|
4f24d363ec | ||
|
|
2e4fd7c1a8 |
@@ -5,7 +5,7 @@ name: Upload Python Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*.dev[0-9]+"]
|
||||
tags: ["*"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
cd stubs/setup
|
||||
cp ../${{ steps.version.outputs.replaced }}/mobase.pyi mobase-stubs/__init__.pyi
|
||||
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
|
||||
|
||||
@@ -19,20 +19,9 @@ 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
|
||||
@@ -44,35 +33,39 @@ Some words of warning:
|
||||
|
||||
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**.
|
||||
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.4.yml ${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 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`.
|
||||
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 `main.py`:
|
||||
A few options are available for `mo2-stubs-generator`:
|
||||
|
||||
```
|
||||
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 (default stubs/setup/mobase-stubs/__init__.pyi)
|
||||
output folder (default stubs/setup/mobase-stubs)
|
||||
-v, --verbose verbose mode (all logs go to stderr)
|
||||
-c CONFIG, --config CONFIG
|
||||
configuration file
|
||||
@@ -81,19 +74,7 @@ optional arguments:
|
||||
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.
|
||||
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.4.yml`).
|
||||
only conversions or fixes considered "strange" will be shown.
|
||||
|
||||
## Configuration file
|
||||
|
||||
@@ -102,20 +83,37 @@ deduced by `main` (or are too complex to deduce), and the documentation for ever
|
||||
|
||||
## Uploading the stubs to pypi
|
||||
|
||||
The upload of the stubs to https://pypi.org/project/mobase-stubs/ should be
|
||||
done automatically when a new Github release is made.
|
||||
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.
|
||||
|
||||
## Extras — Starts a python interpreter with `mobase`
|
||||
## Extras — Using `mobase` in a Python interpreter
|
||||
|
||||
It is possible to start a (i)python interpret with `mobase` imported by running:
|
||||
It is possible to start a (i)python interpreter with `mobase` imported by running
|
||||
|
||||
```
|
||||
python -im generator.loader ${MO2_INSTALL_PATH}
|
||||
```bash
|
||||
python -i -m mo2.stubs.generator.loader ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
This has no real usage except for MO2 developers since most classes from the `mobase` module cannot be instantiated.
|
||||
You can also import `mobase` in your code using the following (after installing
|
||||
this package):
|
||||
|
||||
# License
|
||||
```python
|
||||
from mo2.stubs.generator import load_mobase
|
||||
|
||||
mobase = load_mobase(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
|
||||
```
|
||||
|
||||
|
||||
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
|
||||
for MO2 developers.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
||||
+126
-16
@@ -1,24 +1,14 @@
|
||||
---
|
||||
version: 1
|
||||
# version of the configuration
|
||||
version: 2
|
||||
|
||||
# This is the list of type to replace:
|
||||
replacements:
|
||||
Organizer::FileInfo: FileInfo
|
||||
IOrganizer::FileInfo: FileInfo
|
||||
IPluginInstaller::EInstallResult: InstallResult
|
||||
GuessedValue< QString>: GuessedString
|
||||
|
||||
# List of names to ignores:
|
||||
ignores:
|
||||
- toPyQt
|
||||
# version of the stubs - this is overridden when publishing
|
||||
__version__: "2.5.0"
|
||||
|
||||
# This is the root of the mobase module and will contain everything
|
||||
# related to functions / classes, including their documentation.
|
||||
mobase:
|
||||
|
||||
# Version of the stubs - this is overridden by when publishing.
|
||||
__version__: "2.5.0"
|
||||
|
||||
getFileVersion:
|
||||
__doc__: Retrieve the file version of the given executable.
|
||||
args:
|
||||
@@ -3014,11 +3004,11 @@ mobase:
|
||||
__doc__:
|
||||
__abstract__: true
|
||||
|
||||
BinaryName:
|
||||
binaryName:
|
||||
__doc__:
|
||||
returns: The name of the script extender binary.
|
||||
|
||||
PluginPath:
|
||||
pluginPath:
|
||||
__doc__:
|
||||
returns: The script extender plugin path, relative to the data folder.
|
||||
|
||||
@@ -3147,3 +3137,123 @@ mobase:
|
||||
|
||||
scheme:
|
||||
returns: The version scheme in effect for this VersionInfo.
|
||||
|
||||
mobase.widgets:
|
||||
|
||||
TaskDialog:
|
||||
__doc__: Customizable choice dialog.
|
||||
|
||||
__init__:
|
||||
__doc__: Construct a new TaskDialog.
|
||||
|
||||
args:
|
||||
parent: Parent widget of the dialog.
|
||||
title: Title of the dialog.
|
||||
main: Header of the dialog (big text at the top).
|
||||
content: Main message of the dialog (text below main).
|
||||
details: Details for the dialog, initially collapsed (bottom of the dialog).
|
||||
icon: Icon for the dialog.
|
||||
buttons: List of buttons for the dialog.
|
||||
remember: Remember the choice for this dialog.
|
||||
|
||||
addButton:
|
||||
__doc__: Add a custom button to this TaskDialog.
|
||||
|
||||
args:
|
||||
button: Button to add to the dialog.
|
||||
|
||||
addContent:
|
||||
__doc__: |
|
||||
Add a custom widget content to this TaskDialog. Widget content are put between
|
||||
content and buttons (above buttons).
|
||||
|
||||
args:
|
||||
widget: Widget to add.
|
||||
|
||||
exec:
|
||||
__doc__: |
|
||||
Display this dialog and wait for user-interaction to return. This is a blocking
|
||||
function.
|
||||
|
||||
returns:
|
||||
The button clicked by the user. Without custom buttons, this return Ok,
|
||||
otherwise it returns the button set in the TaskDialogButton.
|
||||
|
||||
setContent:
|
||||
__doc__: Set the top-level message of this dialog.
|
||||
|
||||
args:
|
||||
content: Top-level message to set.
|
||||
|
||||
setDetails:
|
||||
__doc__: |
|
||||
Set the details for this TaskDialog.
|
||||
|
||||
The details are hidden by default and the user can display them by clicking
|
||||
the "Details" button at the bottom of the TaskDialog.
|
||||
|
||||
args:
|
||||
details: Details content to display. Can be a multi-line string.
|
||||
|
||||
setIcon:
|
||||
__doc__: Set the icon of the dialog.
|
||||
|
||||
args:
|
||||
icon: Icon of the dialog.
|
||||
|
||||
setMain:
|
||||
__doc__: |
|
||||
Set the main message of the dialog. The main message is displayed at the top of
|
||||
the dialog in large font.
|
||||
|
||||
args:
|
||||
main: Main message of the dialog.
|
||||
|
||||
setRemember:
|
||||
__doc__: Configure the dialog to remember user-choice.
|
||||
|
||||
args:
|
||||
action:
|
||||
file:
|
||||
|
||||
setTitle:
|
||||
__doc__: Set the title of the dialog.
|
||||
|
||||
args:
|
||||
title: Title of the dialog.
|
||||
|
||||
setWidth:
|
||||
__doc__: Set the width of the dialog.
|
||||
|
||||
args:
|
||||
width: Width of the dialog.
|
||||
|
||||
|
||||
TaskDialogButton:
|
||||
__doc__: Special button to be used inside TaskDialog widgets.
|
||||
|
||||
__init__.1:
|
||||
__doc__: Create a TaskDialogButton.
|
||||
|
||||
args:
|
||||
text: Label of the button.
|
||||
description: Description of the button.
|
||||
button: Value returned by TaskDialog.exec() if this button is clicked.
|
||||
|
||||
__init__.2:
|
||||
__doc__: Create a TaskDialogButton without description.
|
||||
|
||||
args:
|
||||
text: Label of the button.
|
||||
button: Value returned by TaskDialog.exec() if this button is clicked.
|
||||
|
||||
properties[]:
|
||||
text:
|
||||
type: str
|
||||
desc: Label of the button.
|
||||
description:
|
||||
type: str
|
||||
desc: Description of the button.
|
||||
button:
|
||||
type: PyQt6.QtWidgets.QMessageBox.StandardButton
|
||||
desc: Value returned by TaskDialog.exec() if this button is clicked.
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from generator import LOGGER
|
||||
from generator.loader import load_mobase
|
||||
from generator.mtypes import Class, Function, PyType
|
||||
from generator.parser import is_enum
|
||||
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",
|
||||
metavar="INSTALL_DIR",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="installation directory of Mod Organizer 2",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=Path,
|
||||
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)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=argparse.FileType("r"),
|
||||
default=None,
|
||||
help="configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig()
|
||||
LOGGER.setLevel(logging.WARNING)
|
||||
|
||||
if args.verbose:
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
output_path = cast(Path, args.output)
|
||||
|
||||
# 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
|
||||
|
||||
# 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, should be fine with the __future__ import
|
||||
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)
|
||||
|
||||
# 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",
|
||||
[
|
||||
"Dict",
|
||||
"Iterable",
|
||||
"Iterator",
|
||||
"List",
|
||||
"Tuple",
|
||||
"Union",
|
||||
"Any",
|
||||
"Optional",
|
||||
"Callable",
|
||||
"overload",
|
||||
"Set",
|
||||
"TypeVar",
|
||||
"Type",
|
||||
],
|
||||
),
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
# Needs to define the MVariant and GameFeatureType type:
|
||||
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()
|
||||
|
||||
# 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(
|
||||
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)
|
||||
@@ -34,7 +34,7 @@ deps =
|
||||
types-PyYAML
|
||||
isort
|
||||
commands =
|
||||
black generator main.py --check --diff
|
||||
flake8 generator main.py
|
||||
mypy generator main.py
|
||||
isort -c generator main.py
|
||||
black src --check --diff
|
||||
flake8 src
|
||||
mypy src
|
||||
isort -c src
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from setuptools import find_namespace_packages, setup
|
||||
|
||||
install_requires = ["black", "isort"]
|
||||
|
||||
dev_requires = [
|
||||
"black",
|
||||
"flake8-black",
|
||||
"flake8",
|
||||
"types-chardet",
|
||||
]
|
||||
|
||||
setup(
|
||||
name="mo2-stubs-generator",
|
||||
version="1.0.0",
|
||||
package_dir={"": "src"},
|
||||
packages=find_namespace_packages(where="src", include=["mo2.*"]),
|
||||
author="Holt59",
|
||||
author_email="capelle.mikael@gmail",
|
||||
description="Python stubs generator for mobase (MO2 Python API).",
|
||||
long_description=open("README.md").read(),
|
||||
url="https://github.com/ModOrganizer2/pystubs-generation",
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
],
|
||||
license="MIT",
|
||||
install_requires=install_requires,
|
||||
extras_require={"dev": dev_requires},
|
||||
entry_points={
|
||||
"console_scripts": ["mo2-stubs-generator=mo2.stubs.generator.__main__:main"],
|
||||
},
|
||||
)
|
||||
@@ -3,3 +3,5 @@
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
from .loader import load_mobase # noqa: F401
|
||||
@@ -0,0 +1,238 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import logging
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from . import LOGGER
|
||||
from .loader import load_mobase
|
||||
from .mtypes import Class, PyTyping
|
||||
from .parser import is_enum
|
||||
from .register import MobaseRegister
|
||||
from .utils import Settings, clean_class
|
||||
from .writer import Writer, is_list_of_functions
|
||||
|
||||
|
||||
def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, object]]:
|
||||
|
||||
objects: list[tuple[str, object]] = []
|
||||
|
||||
assert hasattr(module, "__name__")
|
||||
module_name: str = module.__name__ # type: ignore
|
||||
|
||||
for name in dir(module):
|
||||
if name.startswith("__") or name in skips:
|
||||
continue
|
||||
|
||||
obj = getattr(module, name)
|
||||
|
||||
# skip submodules
|
||||
if inspect.ismodule(obj):
|
||||
continue
|
||||
|
||||
# skip imports - type object have wrong __module__?
|
||||
if hasattr(obj, "__module__") and obj.__module__ != module_name:
|
||||
if obj.__module__ != types.__name__ or hasattr(types, name):
|
||||
continue
|
||||
|
||||
objects.append((name, obj))
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def add_mobase_header(writer: Writer):
|
||||
writer.print_imports(
|
||||
[
|
||||
"abc",
|
||||
("enum", ["Enum"]),
|
||||
"os",
|
||||
(
|
||||
"typing",
|
||||
[
|
||||
"Any",
|
||||
"Callable",
|
||||
"Dict",
|
||||
"Iterator",
|
||||
"List",
|
||||
"Optional",
|
||||
"overload",
|
||||
"Sequence",
|
||||
"Set",
|
||||
"Tuple",
|
||||
"Type",
|
||||
"TypeVar",
|
||||
"Union",
|
||||
],
|
||||
),
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def add_mobase_widgets_header(writer: Writer):
|
||||
writer.print_imports(
|
||||
[
|
||||
(
|
||||
"typing",
|
||||
["List", "Tuple", "Union", "overload"],
|
||||
),
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
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=Path("stubs/setup/mobase-stubs"),
|
||||
help="output folder (default stubs/setup/mobase-stubs)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="verbose mode (all logs go to stderr)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig()
|
||||
LOGGER.setLevel(logging.WARNING)
|
||||
|
||||
if args.verbose:
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
output_path: Path = args.output
|
||||
config_path: Path | None = args.config
|
||||
|
||||
# create the register
|
||||
register = MobaseRegister()
|
||||
|
||||
# load mobase (cannot simply do "import mobase")
|
||||
mobase = load_mobase(Path(args.install_dir))
|
||||
|
||||
# headers
|
||||
module_headers: dict[str, Callable[[Writer], None]] = {
|
||||
"mobase": add_mobase_header,
|
||||
"mobase.widgets": add_mobase_widgets_header,
|
||||
}
|
||||
|
||||
# list of objects directly in mobase
|
||||
module_objects: dict[str, list[tuple[str, object]]] = {
|
||||
"mobase": extract_objects(
|
||||
mobase,
|
||||
[
|
||||
# the "real" IPlugin is IPluginBase
|
||||
"IPlugin",
|
||||
],
|
||||
),
|
||||
"mobase.widgets": extract_objects(mobase.widgets),
|
||||
}
|
||||
|
||||
for name, objects in module_objects.items():
|
||||
|
||||
# load settings from the configuration
|
||||
settings: Settings = Settings(register)
|
||||
if config_path is not None:
|
||||
with open(config_path, "r") as fp:
|
||||
settings = Settings(register, fp, module=name)
|
||||
|
||||
for n, o in objects:
|
||||
register.add_object(n, o)
|
||||
|
||||
# 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])
|
||||
)
|
||||
|
||||
# Process everything:
|
||||
for n, o in objects:
|
||||
|
||||
# Create the corresponding object:
|
||||
c = register.make_object(n, o)
|
||||
|
||||
if isinstance(c, Class):
|
||||
|
||||
# Clean the class (e.g., remove duplicates methods due to wrappers):
|
||||
clean_class(c)
|
||||
|
||||
# Path the class using the configuration:
|
||||
settings.patch_class(c)
|
||||
|
||||
elif isinstance(c, PyTyping):
|
||||
...
|
||||
|
||||
elif is_list_of_functions(c):
|
||||
settings.patch_functions(c)
|
||||
|
||||
else:
|
||||
LOGGER.critical(
|
||||
"Cannot generated stubs for {}, unsupported object type.".format(n)
|
||||
)
|
||||
|
||||
output_folder = output_path
|
||||
if name != "mobase":
|
||||
output_folder = output_path.joinpath(
|
||||
name.replace("mobase.", "").replace(".", "/")
|
||||
)
|
||||
|
||||
# create directory if required
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# write everything
|
||||
with open(output_folder.joinpath("__init__.pyi"), "w") as output:
|
||||
|
||||
writer = Writer(package=name, output=output, settings=settings)
|
||||
|
||||
# the __future__ import must be at the beginning
|
||||
writer.print_imports([("__future__", ["annotations"])])
|
||||
writer.print_version(settings.version)
|
||||
|
||||
module_headers[name](writer)
|
||||
|
||||
for n, o in objects:
|
||||
|
||||
# Get the corresponding object:
|
||||
c = register.get_object(n)
|
||||
|
||||
writer.print_object(c)
|
||||
|
||||
black.format_file_in_place(
|
||||
output_folder.joinpath("__init__.pyi"),
|
||||
fast=False,
|
||||
mode=black.Mode(is_pyi=True),
|
||||
write_back=black.WriteBack.YES,
|
||||
)
|
||||
isort.api.sort_file(output_folder.joinpath("__init__.pyi"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from modulefinder import Module
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_mobase(path: Path):
|
||||
def load_mobase(path: os.PathLike) -> Module:
|
||||
"""
|
||||
Load the mobase from the given MO2 installation path and
|
||||
returns it.
|
||||
@@ -16,6 +17,8 @@ def load_mobase(path: Path):
|
||||
Returns: The mobase module.
|
||||
"""
|
||||
|
||||
path = Path(path)
|
||||
|
||||
# We need absolute path for loading DLL and modules:
|
||||
path = path.resolve()
|
||||
|
||||
@@ -33,9 +36,9 @@ def load_mobase(path: Path):
|
||||
# We need to add plugins/data to sys.path, mainly for PyQt6
|
||||
sys.path.insert(1, path.joinpath("plugins", "plugin_python", "libs").as_posix())
|
||||
|
||||
import mobase
|
||||
import mobase # type: ignore
|
||||
|
||||
return mobase
|
||||
return mobase # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Final, TypeVar
|
||||
|
||||
|
||||
class PyType:
|
||||
@@ -30,16 +31,9 @@ class PyType:
|
||||
|
||||
self.name = name.strip()
|
||||
|
||||
# remove MOBase:: and mobase.
|
||||
self.name = self.name.replace("mobase.", "")
|
||||
|
||||
# replace QFlags[xxx] with xxx
|
||||
self.name = re.sub(r"QFlags\[([^]]*)\]", r"\1", self.name)
|
||||
|
||||
# we replace QVariant with MoVariant which is valid python type
|
||||
if self.name == "QVariant":
|
||||
self.name = "MoVariant"
|
||||
|
||||
# find PyQt types
|
||||
for m in (QtCore, QtGui, QtWidgets):
|
||||
if self.name in dir(m):
|
||||
@@ -51,8 +45,9 @@ class PyType:
|
||||
A valid typing representation for this type.
|
||||
"""
|
||||
# IPluginBase -> IPlugin
|
||||
if self.name == "IPluginBase":
|
||||
if self.name == "mobase.IPluginBase":
|
||||
return "IPlugin"
|
||||
|
||||
return self.name
|
||||
|
||||
def is_none(self) -> bool:
|
||||
@@ -142,10 +137,18 @@ class Argument:
|
||||
# pybind11 puts enum in <> so we need to fix
|
||||
m = re.match(r"<([^:]+):\s*[0-9]+>", value)
|
||||
if m:
|
||||
# we also need to use the upper case version
|
||||
value = m.group(1)
|
||||
parts = value.split(".")
|
||||
value = ".".join(parts[:-1] + [parts[-1].upper()])
|
||||
# if this is a mobase enum, we eed to use the upper case version
|
||||
if self.type.name.startswith("mobase"):
|
||||
value = m.group(1)
|
||||
parts = value.split(".")
|
||||
value = ".".join(parts[:-1] + [parts[-1].upper()])
|
||||
|
||||
# PyQt -> need to fix
|
||||
elif self.type.name.startswith("PyQt"):
|
||||
parts = m.group(1).split(".")
|
||||
value = f"{self.type.name}.{parts[-1]}"
|
||||
else:
|
||||
value = m.group(1)
|
||||
|
||||
return value
|
||||
|
||||
@@ -317,6 +320,7 @@ class Class:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package: str,
|
||||
name: str,
|
||||
bases: list[Class],
|
||||
methods: list[Method],
|
||||
@@ -325,7 +329,7 @@ class Class:
|
||||
inner_classes: list[Class] = [],
|
||||
doc: str = "",
|
||||
):
|
||||
|
||||
self.package = package
|
||||
self.name = name
|
||||
self.bases = bases
|
||||
self.methods = methods
|
||||
@@ -364,6 +368,16 @@ class Class:
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""
|
||||
Returns:
|
||||
The full name of this class, i.e., package.canonical_name.
|
||||
"""
|
||||
if self.package:
|
||||
return f"{self.package}.{self.canonical_name}"
|
||||
return self.canonical_name
|
||||
|
||||
@property
|
||||
def all_bases(self) -> set[Class]:
|
||||
"""
|
||||
@@ -378,9 +392,6 @@ class Class:
|
||||
def is_deprecated(self):
|
||||
return self.deprecated
|
||||
|
||||
def __str__(self):
|
||||
return self.canonical_name
|
||||
|
||||
|
||||
class PyClass(Class):
|
||||
|
||||
@@ -391,9 +402,10 @@ class PyClass(Class):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package: str,
|
||||
name: str,
|
||||
):
|
||||
super().__init__(name, [], [])
|
||||
super().__init__(package, name, [], [])
|
||||
self.abstract = False
|
||||
|
||||
|
||||
@@ -403,12 +415,15 @@ class Enum(Class):
|
||||
Class representing an enum.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, values: dict[str, int], methods: list[Method]):
|
||||
def __init__(
|
||||
self, package: str, name: str, values: dict[str, int], methods: list[Method]
|
||||
):
|
||||
# Note: Boost.Python.enum inherits int() not enum.Enum() but for the sake
|
||||
# of stubs, I think making them inherit enum.Enum is more appropriate:
|
||||
super().__init__(
|
||||
package,
|
||||
name,
|
||||
[PyClass("Enum")],
|
||||
[PyClass("", "Enum")],
|
||||
methods,
|
||||
inner_classes=[],
|
||||
constants=[Constant(k, None, v) for k, v in values.items()],
|
||||
@@ -416,3 +431,26 @@ class Enum(Class):
|
||||
|
||||
def is_abstract(self):
|
||||
return False
|
||||
|
||||
|
||||
class PyTyping:
|
||||
"""
|
||||
Class representing a typing object, e.g., MoVariant.
|
||||
"""
|
||||
|
||||
name: Final[str]
|
||||
typing: Final[str]
|
||||
|
||||
def __init__(self, name: str, obj: object):
|
||||
self.name = name
|
||||
|
||||
_typing: str
|
||||
if obj.__module__ == "types":
|
||||
_typing = str(obj)
|
||||
# type-var have a weird name, e.g., ~Name
|
||||
elif type(obj) is TypeVar:
|
||||
_typing = f'TypeVar("{name}")'
|
||||
else:
|
||||
_typing = str(obj)
|
||||
|
||||
self.typing = _typing
|
||||
@@ -179,7 +179,7 @@ def parse_pybind11_function_docstring(e) -> list[Overload]:
|
||||
return overloads
|
||||
|
||||
|
||||
def make_functions(name: str, e) -> list[Function]:
|
||||
def make_functions(e) -> list[Function]:
|
||||
overloads = parse_pybind11_function_docstring(e)
|
||||
|
||||
return [
|
||||
@@ -193,13 +193,11 @@ def make_functions(name: str, e) -> list[Function]:
|
||||
]
|
||||
|
||||
|
||||
def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
||||
def make_class(e: type, register: MobaseRegister) -> Class:
|
||||
"""
|
||||
Constructs a Class object from the given python class.
|
||||
|
||||
Args:
|
||||
fullname: Name of the class (might be different from __name__ for inner
|
||||
classes).
|
||||
e: The python class (created from boost) to construct an object for.
|
||||
class_register:
|
||||
|
||||
@@ -257,7 +255,7 @@ def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
||||
inner_classes = [ic[1] for ic in all_attrs if isinstance(ic[1], type)]
|
||||
|
||||
pinner_classes: list[Class] = [
|
||||
cast(Class, register.make_object(f"{fullname}.{ic.__name__}", ic))
|
||||
cast(Class, register.make_object(f"{e.__qualname__}.{ic.__name__}", ic))
|
||||
for ic in inner_classes
|
||||
]
|
||||
|
||||
@@ -285,7 +283,7 @@ def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
||||
Overload(
|
||||
return_type=PyType("bool"),
|
||||
arguments=[
|
||||
Argument("self", PyType(fullname)),
|
||||
Argument("self", PyType(e.__module__ + "." + e.__qualname__)),
|
||||
Argument("other", PyType("object")),
|
||||
],
|
||||
)
|
||||
@@ -353,7 +351,7 @@ def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
||||
"PyQt6.QtWidgets.QWidget", e.__name__
|
||||
)
|
||||
)
|
||||
direct_bases.append(PyClass("PyQt6.QtWidgets.QWidget"))
|
||||
direct_bases.append(PyClass("PyQt6.QtWidgets", "QWidget"))
|
||||
|
||||
# check if it an enum
|
||||
if is_enum(e):
|
||||
@@ -364,12 +362,14 @@ def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
||||
methods = [m for m in methods if m.name != "__init__"]
|
||||
|
||||
return Enum(
|
||||
e.__module__,
|
||||
e.__name__,
|
||||
OrderedDict((name, value) for name, (value, _) in values.items()),
|
||||
methods=methods,
|
||||
)
|
||||
|
||||
return Class(
|
||||
e.__module__,
|
||||
e.__name__,
|
||||
direct_bases,
|
||||
methods,
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from .mtypes import Class, Function
|
||||
from .mtypes import Class, Function, PyTyping
|
||||
|
||||
|
||||
class MobaseRegister:
|
||||
@@ -42,9 +42,13 @@ class MobaseRegister:
|
||||
|
||||
if name not in self.objects:
|
||||
if isinstance(e, type):
|
||||
self.objects[name] = make_class(name, e, self)
|
||||
self.objects[name] = make_class(e, self)
|
||||
elif callable(e):
|
||||
self.objects[name] = make_functions(name, e)
|
||||
self.objects[name] = make_functions(e)
|
||||
|
||||
# typing stuff
|
||||
elif type(e).__module__ == "types" or type(e).__module__ == "typing":
|
||||
self.objects[name] = PyTyping(name, e)
|
||||
|
||||
return self.objects[name]
|
||||
|
||||
@@ -60,6 +64,3 @@ class MobaseRegister:
|
||||
The object with the given name.
|
||||
"""
|
||||
return self.objects[name]
|
||||
|
||||
|
||||
MOBASE_REGISTER = MobaseRegister()
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from typing import TYPE_CHECKING, NamedTuple, TextIO, TypedDict
|
||||
from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypedDict
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -73,6 +73,8 @@ class Settings:
|
||||
|
||||
register: MobaseRegister
|
||||
|
||||
version: Final[str]
|
||||
|
||||
# Name to ignore:
|
||||
_ignore_names: list[str]
|
||||
|
||||
@@ -80,35 +82,31 @@ class Settings:
|
||||
_replacements: dict[str, str]
|
||||
|
||||
# Content of mobase:
|
||||
_mobase: dict[str, dict[str, object]]
|
||||
_module: dict[str, dict[str, object]]
|
||||
|
||||
def __init__(self, register: MobaseRegister, fp: TextIO | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
register: MobaseRegister,
|
||||
fp: TextIO | None = None,
|
||||
module: str | None = None,
|
||||
):
|
||||
|
||||
self.register = register
|
||||
|
||||
if fp is None:
|
||||
self._ignore_names = []
|
||||
self._replacements = {}
|
||||
self._mobase = {}
|
||||
self._version = ""
|
||||
self._module = {}
|
||||
else:
|
||||
data = yaml.load(fp, yaml.FullLoader)
|
||||
assert data["version"] == 1
|
||||
assert data["version"] == 2, "only settings version 2 are supported"
|
||||
|
||||
self._ignore_names = data.get("ignores", [])
|
||||
self._replacements = data.get("replacements", {})
|
||||
self._mobase = data.get("mobase", {})
|
||||
# retrieve the module version
|
||||
self.version = data["__version__"]
|
||||
|
||||
@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, object]]:
|
||||
return self._mobase
|
||||
assert module is not None
|
||||
self._module = data.get(module, None) or {}
|
||||
|
||||
def _get_class_settings(self, canonical_name: str) -> YamlClassSettings | None:
|
||||
"""
|
||||
@@ -122,7 +120,7 @@ class Settings:
|
||||
settings where not found.
|
||||
"""
|
||||
parts = canonical_name.split(".")
|
||||
base: dict[str, object] = dict(self._mobase)
|
||||
base: dict[str, object] = dict(self._module)
|
||||
for part in parts:
|
||||
if part in base:
|
||||
base = base[part] # type: ignore
|
||||
@@ -208,9 +206,9 @@ class Settings:
|
||||
setting_name = fn.name
|
||||
|
||||
# If the name is in the settings:
|
||||
if setting_name in self._mobase:
|
||||
if setting_name in self._module:
|
||||
function_settings = self._parse_function_settings(
|
||||
self._mobase[setting_name] # type: ignore
|
||||
self._module[setting_name] # type: ignore
|
||||
)
|
||||
|
||||
# Force raises:
|
||||
@@ -282,7 +280,10 @@ class Settings:
|
||||
if "__bases__" in class_settings:
|
||||
for bc in class_settings["__bases__"]:
|
||||
if bc.startswith("PyQt"):
|
||||
cls.bases.append(PyClass(bc))
|
||||
parts = bc.split(".")
|
||||
cls.bases.append(
|
||||
PyClass(package=".".join(parts[:-1]), name=parts[-1])
|
||||
)
|
||||
else:
|
||||
cls.bases.append(self.register.get_object(bc))
|
||||
del class_settings["__bases__"]
|
||||
@@ -455,13 +456,12 @@ class Settings:
|
||||
)
|
||||
|
||||
|
||||
def clean_class(cls: Class, settings: Settings):
|
||||
def clean_class(cls: Class):
|
||||
"""
|
||||
Clean the given class object.
|
||||
|
||||
Args:
|
||||
cls: The class object to clean.
|
||||
settings: The settings.
|
||||
"""
|
||||
|
||||
# Remove duplicate methods (based on name and argument types):
|
||||
@@ -507,7 +507,8 @@ def clean_class(cls: Class, settings: Settings):
|
||||
clean_methods.append(method)
|
||||
else:
|
||||
arg0_name = method.args[0].type.name
|
||||
if arg0_name in [cls.name, cls.canonical_name, "object"]:
|
||||
# print(arg0_name, [cls.name, cls.canonical_name, cls.full_name, "object"])
|
||||
if arg0_name in [cls.full_name, "object"]:
|
||||
clean_methods.append(method)
|
||||
else:
|
||||
LOGGER.info(
|
||||
@@ -570,4 +571,4 @@ def clean_class(cls: Class, settings: Settings):
|
||||
|
||||
# Clean inner classes:
|
||||
for ic in cls.inner_classes:
|
||||
clean_class(ic, settings)
|
||||
clean_class(ic)
|
||||
@@ -1,21 +1,31 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from typing import TextIO
|
||||
from typing import TextIO, TypeGuard
|
||||
|
||||
from . import LOGGER
|
||||
from .mtypes import Class, Enum, Function, Method, Property
|
||||
from .mtypes import Class, Enum, Function, Method, Property, PyTyping
|
||||
from .utils import Settings
|
||||
|
||||
|
||||
def is_list_of_functions(e: object) -> TypeGuard[list[Function]]:
|
||||
return isinstance(e, list) and all(isinstance(x, Function) for x in e)
|
||||
|
||||
|
||||
class Writer:
|
||||
|
||||
_output: TextIO
|
||||
_settings: Settings
|
||||
|
||||
def __init__(self, output: TextIO, settings: Settings):
|
||||
def __init__(self, package: str, output: TextIO, settings: Settings):
|
||||
self._package = package.split(".")
|
||||
self._output = output
|
||||
self._settings = settings
|
||||
|
||||
def _fix_typing(self, value: str) -> str:
|
||||
for pkg in self._package:
|
||||
value = value.replace(pkg + ".", "")
|
||||
return value
|
||||
|
||||
def _print(self, *args, **kwargs):
|
||||
kwargs["file"] = self._output
|
||||
print(*args, **kwargs)
|
||||
@@ -57,7 +67,7 @@ class Writer:
|
||||
|
||||
sig_return_type = ""
|
||||
if not fn.ret.type.is_none():
|
||||
sig_return_type = " -> " + fn.ret.type.typing()
|
||||
sig_return_type = " -> " + self._fix_typing(fn.ret.type.typing())
|
||||
|
||||
if isinstance(fn, Method):
|
||||
if fn.is_static():
|
||||
@@ -68,7 +78,7 @@ class Writer:
|
||||
|
||||
python_args: list[str] = []
|
||||
for i, arg in enumerate(fn.args):
|
||||
tmp = "{}: {}".format(arg.name, arg.type.typing())
|
||||
tmp = "{}: {}".format(arg.name, self._fix_typing(arg.type.typing()))
|
||||
if arg.has_default_value():
|
||||
tmp += " = {}".format(arg.value)
|
||||
python_args.append(tmp)
|
||||
@@ -106,7 +116,13 @@ class Writer:
|
||||
if fn.raises:
|
||||
doc += "\nRaises:\n"
|
||||
for rai in fn.raises:
|
||||
doc += " " + rai.type.typing() + ": " + rai.doc.strip() + "\n"
|
||||
doc += (
|
||||
" "
|
||||
+ self._fix_typing(rai.type.typing())
|
||||
+ ": "
|
||||
+ rai.doc.strip()
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
if doc:
|
||||
self._print()
|
||||
@@ -132,13 +148,15 @@ class Writer:
|
||||
|
||||
self._print("{}@property".format(indent))
|
||||
self._print(
|
||||
"{}def {}(self) -> {}: ...".format(indent, prop.name, prop.type.typing())
|
||||
"{}def {}(self) -> {}: ...".format(
|
||||
indent, prop.name, self._fix_typing(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()
|
||||
indent, prop.name, self._fix_typing(prop.type.typing())
|
||||
)
|
||||
)
|
||||
self._print()
|
||||
@@ -150,7 +168,10 @@ class Writer:
|
||||
|
||||
bc = ""
|
||||
if cls.bases or cls.is_abstract():
|
||||
bases = [str(bc) for bc in cls.bases]
|
||||
bases = [
|
||||
bc.canonical_name if bc.package.startswith("mobase") else bc.full_name
|
||||
for bc in cls.bases
|
||||
]
|
||||
if cls.is_abstract() and not any(bc.is_abstract() for bc in cls.bases):
|
||||
bases.insert(0, "abc.ABC")
|
||||
bc = "(" + ", ".join(bases) + ")"
|
||||
@@ -186,7 +207,7 @@ class Writer:
|
||||
|
||||
typing = ""
|
||||
if constant.type is not None:
|
||||
typing = ": {}".format(constant.type.typing())
|
||||
typing = ": {}".format(self._fix_typing(constant.type.typing()))
|
||||
|
||||
# Note: We do not print the value, we use ...
|
||||
self._print(
|
||||
@@ -223,3 +244,18 @@ class Writer:
|
||||
if isinstance(cls, Enum):
|
||||
self._print()
|
||||
self._print()
|
||||
|
||||
def print_typing(self, typ: PyTyping):
|
||||
self._print(f"{typ.name} = {typ.typing}")
|
||||
|
||||
def print_object(self, e: object):
|
||||
|
||||
if isinstance(e, Class):
|
||||
self.print_class(e)
|
||||
|
||||
elif is_list_of_functions(e):
|
||||
for fn in e:
|
||||
self.print_function(fn)
|
||||
|
||||
elif isinstance(e, PyTyping):
|
||||
self.print_typing(e)
|
||||
@@ -3,16 +3,16 @@ from __future__ import annotations
|
||||
__version__ = "2.5.0"
|
||||
|
||||
import abc
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
@@ -25,14 +25,10 @@ import PyQt6.QtCore
|
||||
import PyQt6.QtGui
|
||||
import PyQt6.QtWidgets
|
||||
|
||||
MoVariant = Union[None, bool, int, str, list[Any], dict[str, Any]]
|
||||
FileWrapper = Union[str, PyQt6.QtCore.QFileInfo, Path]
|
||||
DirectoryWrapper = Union[str, PyQt6.QtCore.QDir, Path]
|
||||
GameFeatureType = TypeVar("GameFeatureType")
|
||||
MoVariant = None | bool | int | str | list[object] | dict[str, object]
|
||||
|
||||
class InterfaceNotImplemented: ...
|
||||
|
||||
def getFileVersion(filepath: FileWrapper) -> str:
|
||||
def getFileVersion(filepath: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]) -> str:
|
||||
"""
|
||||
Retrieve the file version of the given executable.
|
||||
|
||||
@@ -44,7 +40,9 @@ def getFileVersion(filepath: FileWrapper) -> str:
|
||||
"""
|
||||
...
|
||||
|
||||
def getIconForExecutable(executable: FileWrapper) -> PyQt6.QtGui.QIcon:
|
||||
def getIconForExecutable(
|
||||
executable: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]
|
||||
) -> PyQt6.QtGui.QIcon:
|
||||
"""
|
||||
Retrieve the icon of an executable. Currently this always extracts the biggest icon.
|
||||
|
||||
@@ -56,7 +54,9 @@ def getIconForExecutable(executable: FileWrapper) -> PyQt6.QtGui.QIcon:
|
||||
"""
|
||||
...
|
||||
|
||||
def getProductVersion(executable: FileWrapper) -> str:
|
||||
def getProductVersion(
|
||||
executable: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the product version of the given executable.
|
||||
|
||||
@@ -297,7 +297,7 @@ class DataArchives(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def archives(self: DataArchives, profile: IProfile) -> Iterable[str]:
|
||||
def archives(self: DataArchives, profile: IProfile) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of archives in the given profile.
|
||||
|
||||
@@ -319,7 +319,7 @@ class DataArchives(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def vanillaArchives(self: DataArchives) -> Iterable[str]:
|
||||
def vanillaArchives(self: DataArchives) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of vanilla archives.
|
||||
|
||||
@@ -345,8 +345,12 @@ class ExecutableForcedLoadSetting:
|
||||
) -> ExecutableForcedLoadSetting: ...
|
||||
|
||||
class ExecutableInfo:
|
||||
def __init__(self: ExecutableInfo, title: str, binary: FileWrapper): ...
|
||||
def arguments(self: ExecutableInfo) -> Iterable[str]: ...
|
||||
def __init__(
|
||||
self: ExecutableInfo,
|
||||
title: str,
|
||||
binary: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo],
|
||||
): ...
|
||||
def arguments(self: ExecutableInfo) -> Sequence[str]: ...
|
||||
def asCustom(self: ExecutableInfo) -> ExecutableInfo: ...
|
||||
def binary(self: ExecutableInfo) -> PyQt6.QtCore.QFileInfo: ...
|
||||
def isCustom(self: ExecutableInfo) -> bool: ...
|
||||
@@ -356,7 +360,7 @@ class ExecutableInfo:
|
||||
def withArgument(self: ExecutableInfo, argument: str) -> ExecutableInfo: ...
|
||||
def withSteamAppId(self: ExecutableInfo, app_id: str) -> ExecutableInfo: ...
|
||||
def withWorkingDirectory(
|
||||
self: ExecutableInfo, directory: DirectoryWrapper
|
||||
self: ExecutableInfo, directory: Union[str, os.PathLike, PyQt6.QtCore.QDir]
|
||||
) -> ExecutableInfo: ...
|
||||
def workingDirectory(self: ExecutableInfo) -> PyQt6.QtCore.QDir: ...
|
||||
|
||||
@@ -463,7 +467,7 @@ class FileTreeEntry:
|
||||
"""
|
||||
...
|
||||
@overload
|
||||
def hasSuffix(self: FileTreeEntry, suffixes: Iterable[str]) -> bool:
|
||||
def hasSuffix(self: FileTreeEntry, suffixes: Sequence[str]) -> bool:
|
||||
"""
|
||||
Check if this entry has one of the given suffixes.
|
||||
|
||||
@@ -564,7 +568,7 @@ class FileTreeEntry:
|
||||
class GamePlugins(abc.ABC):
|
||||
def __init__(self: GamePlugins): ...
|
||||
@abc.abstractmethod
|
||||
def getLoadOrder(self: GamePlugins) -> Iterable[str]: ...
|
||||
def getLoadOrder(self: GamePlugins) -> Sequence[str]: ...
|
||||
@abc.abstractmethod
|
||||
def lightPluginsAreSupported(self: GamePlugins) -> bool:
|
||||
"""
|
||||
@@ -772,7 +776,7 @@ class IDownloadManager:
|
||||
An ID identifying the download.
|
||||
"""
|
||||
...
|
||||
def startDownloadURLs(self: IDownloadManager, urls: Iterable[str]) -> int:
|
||||
def startDownloadURLs(self: IDownloadManager, urls: Sequence[str]) -> int:
|
||||
"""
|
||||
Download a file by url.
|
||||
|
||||
@@ -1164,7 +1168,7 @@ class IFileTree(FileTreeEntry):
|
||||
True if the entry was deleted, False otherwise.
|
||||
"""
|
||||
...
|
||||
def removeAll(self: IFileTree, names: Iterable[str]) -> int:
|
||||
def removeAll(self: IFileTree, names: Sequence[str]) -> int:
|
||||
"""
|
||||
Delete the entries with the given names from the tree.
|
||||
|
||||
@@ -1253,7 +1257,7 @@ class IInstallationManager:
|
||||
...
|
||||
def extractFiles(
|
||||
self: IInstallationManager, entries: List[FileTreeEntry], silent: bool = False
|
||||
) -> Iterable[str]:
|
||||
) -> Sequence[str]:
|
||||
"""
|
||||
Extract the specified files from the currently opened archive to a temporary
|
||||
location.
|
||||
@@ -1273,7 +1277,7 @@ class IInstallationManager:
|
||||
A list containing absolute paths to the temporary files.
|
||||
"""
|
||||
...
|
||||
def getSupportedExtensions(self: IInstallationManager) -> Iterable[str]:
|
||||
def getSupportedExtensions(self: IInstallationManager) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The extensions of archives supported by this installation manager.
|
||||
@@ -1282,7 +1286,7 @@ class IInstallationManager:
|
||||
def installArchive(
|
||||
self: IInstallationManager,
|
||||
mod_name: GuessedString,
|
||||
archive: FileWrapper,
|
||||
archive: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo],
|
||||
mod_id: int = 0,
|
||||
) -> Tuple[InstallResult, str, int]:
|
||||
"""
|
||||
@@ -1323,7 +1327,7 @@ class IModInterface:
|
||||
category_id: The Nexus category ID.
|
||||
"""
|
||||
...
|
||||
def categories(self: IModInterface) -> Iterable[str]:
|
||||
def categories(self: IModInterface) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of categories this mod belongs to.
|
||||
@@ -1613,7 +1617,7 @@ class IModList:
|
||||
to translate from display name to internal name because the display name might not me un-ambiguous.
|
||||
"""
|
||||
|
||||
def allMods(self: IModList) -> Iterable[str]:
|
||||
def allMods(self: IModList) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
A list containing the internal names of all installed mods.
|
||||
@@ -1621,7 +1625,7 @@ class IModList:
|
||||
...
|
||||
def allModsByProfilePriority(
|
||||
self: IModList, profile: IProfile = None
|
||||
) -> Iterable[str]:
|
||||
) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of mod (names), sorted according to the current profile priorities.
|
||||
@@ -1743,7 +1747,7 @@ class IModList:
|
||||
"""
|
||||
...
|
||||
@overload
|
||||
def setActive(self: IModList, names: Iterable[str], active: bool) -> int:
|
||||
def setActive(self: IModList, names: Sequence[str], active: bool) -> int:
|
||||
"""
|
||||
Enable or disable a list of mods.
|
||||
|
||||
@@ -1947,8 +1951,10 @@ class IOrganizer:
|
||||
"""
|
||||
...
|
||||
def findFileInfos(
|
||||
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[FileInfo], bool]
|
||||
) -> Iterable[FileInfo]:
|
||||
self: IOrganizer,
|
||||
path: Union[str, os.PathLike, PyQt6.QtCore.QDir],
|
||||
filter: Callable[[FileInfo], bool],
|
||||
) -> Sequence[FileInfo]:
|
||||
"""
|
||||
Find files in the virtual directory matching the specified filter.
|
||||
|
||||
@@ -1962,8 +1968,10 @@ class IOrganizer:
|
||||
...
|
||||
@overload
|
||||
def findFiles(
|
||||
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[str], bool]
|
||||
) -> Iterable[str]:
|
||||
self: IOrganizer,
|
||||
path: Union[str, os.PathLike, PyQt6.QtCore.QDir],
|
||||
filter: Callable[[str], bool],
|
||||
) -> Sequence[str]:
|
||||
"""
|
||||
Find files in the given folder that matches the given filter.
|
||||
|
||||
@@ -1977,8 +1985,10 @@ class IOrganizer:
|
||||
...
|
||||
@overload
|
||||
def findFiles(
|
||||
self: IOrganizer, path: DirectoryWrapper, patterns: Iterable[str]
|
||||
) -> Iterable[str]:
|
||||
self: IOrganizer,
|
||||
path: Union[str, os.PathLike, PyQt6.QtCore.QDir],
|
||||
patterns: Sequence[str],
|
||||
) -> Sequence[str]:
|
||||
"""
|
||||
Find files in the given folder that matches one of the given glob patterns.
|
||||
|
||||
@@ -1992,8 +2002,8 @@ class IOrganizer:
|
||||
...
|
||||
@overload
|
||||
def findFiles(
|
||||
self: IOrganizer, path: DirectoryWrapper, pattern: str
|
||||
) -> Iterable[str]:
|
||||
self: IOrganizer, path: Union[str, os.PathLike, PyQt6.QtCore.QDir], pattern: str
|
||||
) -> Sequence[str]:
|
||||
"""
|
||||
Find files in the given folder that matches the given glob pattern.
|
||||
|
||||
@@ -2005,7 +2015,7 @@ class IOrganizer:
|
||||
The list of matching files.
|
||||
"""
|
||||
...
|
||||
def getFileOrigins(self: IOrganizer, filename: str) -> Iterable[str]:
|
||||
def getFileOrigins(self: IOrganizer, filename: str) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the file origins for the specified file.
|
||||
|
||||
@@ -2038,7 +2048,9 @@ class IOrganizer:
|
||||
"""
|
||||
...
|
||||
def installMod(
|
||||
self: IOrganizer, filename: FileWrapper, name_suggestion: str = ""
|
||||
self: IOrganizer,
|
||||
filename: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo],
|
||||
name_suggestion: str = "",
|
||||
) -> IModInterface:
|
||||
"""
|
||||
Install a mod archive at the specified location.
|
||||
@@ -2075,7 +2087,7 @@ class IOrganizer:
|
||||
True if the plugin is enabled, False otherwise.
|
||||
"""
|
||||
...
|
||||
def listDirectories(self: IOrganizer, directory: str) -> Iterable[str]:
|
||||
def listDirectories(self: IOrganizer, directory: str) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of (virtual) subdirectories in the given path.
|
||||
|
||||
@@ -2352,7 +2364,9 @@ class IOrganizer:
|
||||
save_changes: If True, the relevant profile information is saved first (enabled mods and order of mods).
|
||||
"""
|
||||
...
|
||||
def resolvePath(self: IOrganizer, filename: FileWrapper) -> str:
|
||||
def resolvePath(
|
||||
self: IOrganizer, filename: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]
|
||||
) -> str:
|
||||
"""
|
||||
Resolves a path relative to the virtual data directory to its absolute real path.
|
||||
|
||||
@@ -2400,9 +2414,9 @@ class IOrganizer:
|
||||
...
|
||||
def startApplication(
|
||||
self: IOrganizer,
|
||||
executable: FileWrapper,
|
||||
args: Iterable[str] = [],
|
||||
cwd: DirectoryWrapper = "",
|
||||
executable: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo],
|
||||
args: Sequence[str] = [],
|
||||
cwd: Union[str, os.PathLike, PyQt6.QtCore.QDir] = "",
|
||||
profile: str = "",
|
||||
forcedCustomOverwrite: str = "",
|
||||
ignoreCustomOverwrite: bool = False,
|
||||
@@ -2557,7 +2571,7 @@ class IPlugin(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def settings(self: IPlugin) -> Iterable[PluginSetting]:
|
||||
def settings(self: IPlugin) -> Sequence[PluginSetting]:
|
||||
"""
|
||||
Returns:
|
||||
A list of settings for this plugin.
|
||||
@@ -2685,14 +2699,14 @@ class IPluginGame(IPlugin):
|
||||
|
||||
def __init__(self: IPluginGame): ...
|
||||
@abc.abstractmethod
|
||||
def CCPlugins(self: IPluginGame) -> Iterable[str]:
|
||||
def CCPlugins(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The current list of active Creation Club plugins.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def DLCPlugins(self: IPluginGame) -> Iterable[str]:
|
||||
def DLCPlugins(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of esp/esm files that are part of known DLCs.
|
||||
@@ -2741,14 +2755,14 @@ class IPluginGame(IPlugin):
|
||||
@abc.abstractmethod
|
||||
def executableForcedLoads(
|
||||
self: IPluginGame,
|
||||
) -> Iterable[ExecutableForcedLoadSetting]:
|
||||
) -> Sequence[ExecutableForcedLoadSetting]:
|
||||
"""
|
||||
Returns:
|
||||
A list of automatically discovered libraries that can be force loaded with executables.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def executables(self: IPluginGame) -> Iterable[ExecutableInfo]:
|
||||
def executables(self: IPluginGame) -> Sequence[ExecutableInfo]:
|
||||
"""
|
||||
Returns:
|
||||
A list of automatically discovered executables of the game itself and tools surrounding it.
|
||||
@@ -2814,7 +2828,7 @@ class IPluginGame(IPlugin):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def gameVariants(self: IPluginGame) -> Iterable[str]:
|
||||
def gameVariants(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of variants for this game.
|
||||
|
||||
@@ -2849,7 +2863,7 @@ class IPluginGame(IPlugin):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def iniFiles(self: IPluginGame) -> Iterable[str]:
|
||||
def iniFiles(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of INI files this game uses. The first file in the list should be the
|
||||
@@ -2936,14 +2950,14 @@ class IPluginGame(IPlugin):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def primaryPlugins(self: IPluginGame) -> Iterable[str]:
|
||||
def primaryPlugins(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of plugins that are part of the game and not considered optional.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def primarySources(self: IPluginGame) -> Iterable[str]:
|
||||
def primarySources(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve primary alternative 'short' names for this game.
|
||||
|
||||
@@ -3008,7 +3022,7 @@ class IPluginGame(IPlugin):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def validShortNames(self: IPluginGame) -> Iterable[str]:
|
||||
def validShortNames(self: IPluginGame) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the valid 'short' names for this game.
|
||||
|
||||
@@ -3325,7 +3339,7 @@ class IPluginList:
|
||||
if the plugin does not exist.
|
||||
"""
|
||||
...
|
||||
def masters(self: IPluginList, name: str) -> Iterable[str]:
|
||||
def masters(self: IPluginList, name: str) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of masters required for a plugin.
|
||||
|
||||
@@ -3388,7 +3402,7 @@ class IPluginList:
|
||||
The name of the origin of the plugin, or an empty string if the plugin does not exist.
|
||||
"""
|
||||
...
|
||||
def pluginNames(self: IPluginList) -> Iterable[str]:
|
||||
def pluginNames(self: IPluginList) -> Sequence[str]:
|
||||
"""
|
||||
Returns:
|
||||
The list of all plugin names.
|
||||
@@ -3407,7 +3421,7 @@ class IPluginList:
|
||||
The priority of the given plugin, or -1 if the plugin does not exist.
|
||||
"""
|
||||
...
|
||||
def setLoadOrder(self: IPluginList, loadorder: Iterable[str]):
|
||||
def setLoadOrder(self: IPluginList, loadorder: Sequence[str]):
|
||||
"""
|
||||
Set the load order.
|
||||
|
||||
@@ -3712,7 +3726,9 @@ class ISaveGame:
|
||||
"""
|
||||
|
||||
def __init__(self: ISaveGame): ...
|
||||
def allFiles(self: ISaveGame) -> Iterable[str]:
|
||||
def allFiles(
|
||||
self: ISaveGame,
|
||||
) -> Sequence[Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]]:
|
||||
"""
|
||||
Returns:
|
||||
The list of all files related to this save.
|
||||
@@ -3729,7 +3745,7 @@ class ISaveGame:
|
||||
The creation time of the save.
|
||||
"""
|
||||
...
|
||||
def getFilepath(self: ISaveGame) -> str:
|
||||
def getFilepath(self: ISaveGame) -> Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]:
|
||||
"""
|
||||
Returns:
|
||||
The path name to the (main) file or folder for the save.
|
||||
@@ -4109,7 +4125,7 @@ class PluginRequirementFactory:
|
||||
...
|
||||
@overload
|
||||
@staticmethod
|
||||
def gameDependency(games: Iterable[str]) -> IPluginRequirement:
|
||||
def gameDependency(games: Sequence[str]) -> IPluginRequirement:
|
||||
"""
|
||||
Create a new game dependency requirement.
|
||||
|
||||
@@ -4139,7 +4155,7 @@ class PluginRequirementFactory:
|
||||
...
|
||||
@overload
|
||||
@staticmethod
|
||||
def pluginDependency(plugins: Iterable[str]) -> IPluginRequirement:
|
||||
def pluginDependency(plugins: Sequence[str]) -> IPluginRequirement:
|
||||
"""
|
||||
Create a new plugin dependency requirement.
|
||||
|
||||
@@ -4206,7 +4222,7 @@ class SaveGameInfo(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def getMissingAssets(
|
||||
self: SaveGameInfo, save: ISaveGame
|
||||
) -> Dict[str, Iterable[str]]:
|
||||
) -> Dict[str, Sequence[str]]:
|
||||
"""
|
||||
Retrieve missing assets from the save.
|
||||
|
||||
@@ -4237,20 +4253,13 @@ class SaveGameInfo(abc.ABC):
|
||||
class ScriptExtender(abc.ABC):
|
||||
def __init__(self: ScriptExtender): ...
|
||||
@abc.abstractmethod
|
||||
def BinaryName(self: ScriptExtender) -> str:
|
||||
def binaryName(self: ScriptExtender) -> str:
|
||||
"""
|
||||
Returns:
|
||||
The name of the script extender binary.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def PluginPath(self: ScriptExtender) -> str:
|
||||
"""
|
||||
Returns:
|
||||
The script extender plugin path, relative to the data folder.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def getArch(self: ScriptExtender) -> int:
|
||||
"""
|
||||
Returns:
|
||||
@@ -4279,13 +4288,22 @@ class ScriptExtender(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def loaderPath(self: ScriptExtender) -> str:
|
||||
def loaderPath(
|
||||
self: ScriptExtender,
|
||||
) -> Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]:
|
||||
"""
|
||||
Returns:
|
||||
The full path to the script extender loader.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def pluginPath(self: ScriptExtender) -> Union[str, os.PathLike, PyQt6.QtCore.QDir]:
|
||||
"""
|
||||
Returns:
|
||||
The script extender plugin path, relative to the data folder.
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def savegameExtension(self: ScriptExtender) -> str:
|
||||
"""
|
||||
Retrieve the extension of script extender save files.
|
||||
@@ -4310,7 +4328,7 @@ class UnmanagedMods(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def mods(self: UnmanagedMods, official_only: bool) -> Iterable[str]:
|
||||
def mods(self: UnmanagedMods, official_only: bool) -> Sequence[str]:
|
||||
"""
|
||||
Retrieve the list of unmanaged mods for the corresponding game.
|
||||
|
||||
@@ -4322,7 +4340,9 @@ class UnmanagedMods(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def referenceFile(self: UnmanagedMods, mod_name: str) -> PyQt6.QtCore.QFileInfo:
|
||||
def referenceFile(
|
||||
self: UnmanagedMods, mod_name: str
|
||||
) -> Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]:
|
||||
"""
|
||||
Retrieve the reference file for the requested mod.
|
||||
|
||||
@@ -4337,7 +4357,9 @@ class UnmanagedMods(abc.ABC):
|
||||
"""
|
||||
...
|
||||
@abc.abstractmethod
|
||||
def secondaryFiles(self: UnmanagedMods, mod_name: str) -> Iterable[str]:
|
||||
def secondaryFiles(
|
||||
self: UnmanagedMods, mod_name: str
|
||||
) -> Sequence[Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]]:
|
||||
"""
|
||||
Retrieve the secondary files for the requested mod.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "2.5.0"
|
||||
|
||||
from typing import List, Tuple, Union, overload
|
||||
|
||||
import PyQt6.QtCore
|
||||
import PyQt6.QtGui
|
||||
import PyQt6.QtWidgets
|
||||
|
||||
class TaskDialog:
|
||||
"""
|
||||
Customizable choice dialog.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self: TaskDialog,
|
||||
parent: PyQt6.QtWidgets.QWidget = None,
|
||||
title: str = "",
|
||||
main: str = "",
|
||||
content: str = "",
|
||||
details: str = "",
|
||||
icon: PyQt6.QtWidgets.QMessageBox.Icon = PyQt6.QtWidgets.QMessageBox.Icon.NoIcon,
|
||||
buttons: List[TaskDialogButton] = [],
|
||||
remember: Union[str, Tuple[str, str]] = "",
|
||||
):
|
||||
"""
|
||||
Construct a new TaskDialog.
|
||||
|
||||
Args:
|
||||
parent: Parent widget of the dialog.
|
||||
title: Title of the dialog.
|
||||
main: Header of the dialog (big text at the top).
|
||||
content: Main message of the dialog (text below main).
|
||||
details: Details for the dialog, initially collapsed (bottom of the dialog).
|
||||
icon: Icon for the dialog.
|
||||
buttons: List of buttons for the dialog.
|
||||
remember: Remember the choice for this dialog.
|
||||
"""
|
||||
...
|
||||
def addButton(self: TaskDialog, button: TaskDialogButton) -> TaskDialog:
|
||||
"""
|
||||
Add a custom button to this TaskDialog.
|
||||
|
||||
Args:
|
||||
button: Button to add to the dialog.
|
||||
"""
|
||||
...
|
||||
def addContent(self: TaskDialog, widget: PyQt6.QtWidgets.QWidget):
|
||||
"""
|
||||
Add a custom widget content to this TaskDialog. Widget content are put between
|
||||
content and buttons (above buttons).
|
||||
|
||||
Args:
|
||||
widget: Widget to add.
|
||||
"""
|
||||
...
|
||||
def exec(self: TaskDialog) -> PyQt6.QtWidgets.QMessageBox.StandardButton:
|
||||
"""
|
||||
Display this dialog and wait for user-interaction to return. This is a blocking
|
||||
function.
|
||||
|
||||
Returns:
|
||||
The button clicked by the user. Without custom buttons, this return Ok, otherwise it returns the button set in the TaskDialogButton.
|
||||
"""
|
||||
...
|
||||
def setContent(self: TaskDialog, content: str) -> TaskDialog:
|
||||
"""
|
||||
Set the top-level message of this dialog.
|
||||
|
||||
Args:
|
||||
content: Top-level message to set.
|
||||
"""
|
||||
...
|
||||
def setDetails(self: TaskDialog, details: str) -> TaskDialog:
|
||||
"""
|
||||
Set the details for this TaskDialog.
|
||||
|
||||
The details are hidden by default and the user can display them by clicking
|
||||
the "Details" button at the bottom of the TaskDialog.
|
||||
|
||||
Args:
|
||||
details: Details content to display. Can be a multi-line string.
|
||||
"""
|
||||
...
|
||||
def setIcon(self: TaskDialog, icon: PyQt6.QtWidgets.QMessageBox.Icon) -> TaskDialog:
|
||||
"""
|
||||
Set the icon of the dialog.
|
||||
|
||||
Args:
|
||||
icon: Icon of the dialog.
|
||||
"""
|
||||
...
|
||||
def setMain(self: TaskDialog, main: str) -> TaskDialog:
|
||||
"""
|
||||
Set the main message of the dialog. The main message is displayed at the top of
|
||||
the dialog in large font.
|
||||
|
||||
Args:
|
||||
main: Main message of the dialog.
|
||||
"""
|
||||
...
|
||||
def setRemember(self: TaskDialog, action: str, file: str = "") -> TaskDialog:
|
||||
"""
|
||||
Configure the dialog to remember user-choice.
|
||||
"""
|
||||
...
|
||||
def setTitle(self: TaskDialog, title: str) -> TaskDialog:
|
||||
"""
|
||||
Set the title of the dialog.
|
||||
|
||||
Args:
|
||||
title: Title of the dialog.
|
||||
"""
|
||||
...
|
||||
def setWidth(self: TaskDialog, width: int):
|
||||
"""
|
||||
Set the width of the dialog.
|
||||
|
||||
Args:
|
||||
width: Width of the dialog.
|
||||
"""
|
||||
...
|
||||
|
||||
class TaskDialogButton:
|
||||
"""
|
||||
Special button to be used inside TaskDialog widgets.
|
||||
"""
|
||||
|
||||
@property
|
||||
def button(self) -> PyQt6.QtWidgets.QMessageBox.StandardButton: ...
|
||||
@button.setter
|
||||
def button(self, arg0: PyQt6.QtWidgets.QMessageBox.StandardButton): ...
|
||||
@property
|
||||
def description(self) -> str: ...
|
||||
@description.setter
|
||||
def description(self, arg0: str): ...
|
||||
@property
|
||||
def text(self) -> str: ...
|
||||
@text.setter
|
||||
def text(self, arg0: str): ...
|
||||
@overload
|
||||
def __init__(
|
||||
self: TaskDialogButton,
|
||||
text: str,
|
||||
description: str,
|
||||
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
|
||||
):
|
||||
"""
|
||||
Create a TaskDialogButton.
|
||||
|
||||
Args:
|
||||
text: Label of the button.
|
||||
description: Description of the button.
|
||||
button: Value returned by TaskDialog.exec() if this button is clicked.
|
||||
"""
|
||||
...
|
||||
@overload
|
||||
def __init__(
|
||||
self: TaskDialogButton,
|
||||
text: str,
|
||||
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
|
||||
):
|
||||
"""
|
||||
Create a TaskDialogButton without description.
|
||||
|
||||
Args:
|
||||
text: Label of the button.
|
||||
button: Value returned by TaskDialog.exec() if this button is clicked.
|
||||
"""
|
||||
...
|
||||
+13
-2
@@ -11,6 +11,8 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
@@ -34,19 +36,28 @@ def find_version(*file_paths):
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
|
||||
def find_package_data(path: str):
|
||||
package_data: dict[str, list[str]] = defaultdict(lambda: [])
|
||||
for stubfile in Path(path).glob("**/*.pyi"):
|
||||
package_data[stubfile.parent.as_posix().replace("/", ".")].append(stubfile.name)
|
||||
return dict(package_data)
|
||||
|
||||
|
||||
long_description = read("README.md")
|
||||
|
||||
package_data = find_package_data("mobase-stubs")
|
||||
|
||||
setup(
|
||||
name="mobase-stubs",
|
||||
url="https://github.com/ModOrganizer2/mo2-pystubs-generation",
|
||||
author="Holt59",
|
||||
author_email="capelle.mikael@gmail.com",
|
||||
description="PEP561 stub files for the mobase python API",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
version=find_version("mobase-stubs", "__init__.pyi"),
|
||||
package_data={"mobase-stubs": ["*.pyi"]},
|
||||
packages=["mobase-stubs"],
|
||||
packages=list(package_data.keys()),
|
||||
package_data=package_data,
|
||||
install_requires=[],
|
||||
python_requires="==3.10.*",
|
||||
classifiers=[
|
||||
|
||||
Reference in New Issue
Block a user