Compare commits

...
18 changed files with 511 additions and 372 deletions
+2 -2
View File
@@ -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
View File
@@ -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
+30 -48
View File
@@ -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,21 @@ 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.
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
for MO2 developers.
# License
## License
The MIT License (MIT)
+10 -14
View File
@@ -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:
@@ -3147,3 +3137,9 @@ mobase:
scheme:
returns: The version scheme in effect for this VersionInfo.
mobase.widgets:
TaskDialog: {}
TaskDialogButton: {}
-190
View File
@@ -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)
+4 -4
View File
@@ -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
+35
View File
@@ -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"],
},
)
+240
View File
@@ -0,0 +1,240 @@
# -*- encoding: utf-8 -*-
import argparse
import inspect
import logging
from pathlib import Path
from typing import Callable, TextIO, cast
import black
import isort
from . import LOGGER
from .loader import load_mobase
from .mtypes import Class, Function, PyType
from .parser import is_enum
from .register import MobaseRegister
from .utils import Settings, clean_class
from .writer import Writer
def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, object]]:
objects: list[tuple[str, object]] = []
for name in dir(module):
if name.startswith("__") or name in skips:
continue
obj = getattr(module, name)
# skip submodules
if inspect.ismodule(obj):
continue
objects.append((name, obj))
return objects
def add_mobase_header(writer: Writer):
writer.print_imports(
[
"abc",
("enum", ["Enum"]),
("pathlib", ["Path"]),
(
"typing",
[
"Dict",
"Iterator",
"List",
"Tuple",
"Union",
"Any",
"Optional",
"Callable",
"overload",
"Sequence",
"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()
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,
[
# we do not want the real MoVariant
"MoVariant",
# 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, list) and isinstance(c[0], Function):
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)
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_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()
@@ -33,7 +33,7 @@ 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
@@ -30,16 +30,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 +44,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 +136,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 +319,7 @@ class Class:
def __init__(
self,
package: str,
name: str,
bases: list[Class],
methods: list[Method],
@@ -325,7 +328,7 @@ class Class:
inner_classes: list[Class] = [],
doc: str = "",
):
self.package = package
self.name = name
self.bases = bases
self.methods = methods
@@ -364,6 +367,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 +391,6 @@ class Class:
def is_deprecated(self):
return self.deprecated
def __str__(self):
return self.canonical_name
class PyClass(Class):
@@ -391,9 +401,10 @@ class PyClass(Class):
def __init__(
self,
package: str,
name: str,
):
super().__init__(name, [], [])
super().__init__(package, name, [], [])
self.abstract = False
@@ -403,12 +414,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()],
@@ -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,
@@ -42,9 +42,9 @@ 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)
return self.objects[name]
@@ -60,6 +60,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)
@@ -12,10 +12,16 @@ 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 +63,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 +74,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 +112,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 +144,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 +164,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 +203,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(
@@ -9,10 +9,10 @@ from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Sequence,
Set,
Tuple,
Type,
@@ -30,8 +30,6 @@ FileWrapper = Union[str, PyQt6.QtCore.QFileInfo, Path]
DirectoryWrapper = Union[str, PyQt6.QtCore.QDir, Path]
GameFeatureType = TypeVar("GameFeatureType")
class InterfaceNotImplemented: ...
def getFileVersion(filepath: FileWrapper) -> str:
"""
Retrieve the file version of the given executable.
@@ -297,7 +295,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 +317,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.
@@ -346,7 +344,7 @@ class ExecutableForcedLoadSetting:
class ExecutableInfo:
def __init__(self: ExecutableInfo, title: str, binary: FileWrapper): ...
def arguments(self: ExecutableInfo) -> Iterable[str]: ...
def arguments(self: ExecutableInfo) -> Sequence[str]: ...
def asCustom(self: ExecutableInfo) -> ExecutableInfo: ...
def binary(self: ExecutableInfo) -> PyQt6.QtCore.QFileInfo: ...
def isCustom(self: ExecutableInfo) -> bool: ...
@@ -463,7 +461,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 +562,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 +770,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 +1162,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 +1251,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 +1271,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.
@@ -1323,7 +1321,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 +1611,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 +1619,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 +1741,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.
@@ -1948,7 +1946,7 @@ class IOrganizer:
...
def findFileInfos(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[FileInfo], bool]
) -> Iterable[FileInfo]:
) -> Sequence[FileInfo]:
"""
Find files in the virtual directory matching the specified filter.
@@ -1963,7 +1961,7 @@ class IOrganizer:
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[str], bool]
) -> Iterable[str]:
) -> Sequence[str]:
"""
Find files in the given folder that matches the given filter.
@@ -1977,8 +1975,8 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, patterns: Iterable[str]
) -> Iterable[str]:
self: IOrganizer, path: DirectoryWrapper, patterns: Sequence[str]
) -> Sequence[str]:
"""
Find files in the given folder that matches one of the given glob patterns.
@@ -1993,7 +1991,7 @@ class IOrganizer:
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, pattern: str
) -> Iterable[str]:
) -> Sequence[str]:
"""
Find files in the given folder that matches the given glob pattern.
@@ -2005,7 +2003,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.
@@ -2075,7 +2073,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.
@@ -2401,7 +2399,7 @@ class IOrganizer:
def startApplication(
self: IOrganizer,
executable: FileWrapper,
args: Iterable[str] = [],
args: Sequence[str] = [],
cwd: DirectoryWrapper = "",
profile: str = "",
forcedCustomOverwrite: str = "",
@@ -2557,7 +2555,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 +2683,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 +2739,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 +2812,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 +2847,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 +2934,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 +3006,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 +3323,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 +3386,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 +3405,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 +3710,7 @@ class ISaveGame:
"""
def __init__(self: ISaveGame): ...
def allFiles(self: ISaveGame) -> Iterable[str]:
def allFiles(self: ISaveGame) -> Sequence[str]:
"""
Returns:
The list of all files related to this save.
@@ -4109,7 +4107,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 +4137,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 +4204,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.
@@ -4310,7 +4308,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.
@@ -4337,7 +4335,7 @@ class UnmanagedMods(abc.ABC):
"""
...
@abc.abstractmethod
def secondaryFiles(self: UnmanagedMods, mod_name: str) -> Iterable[str]:
def secondaryFiles(self: UnmanagedMods, mod_name: str) -> Sequence[str]:
"""
Retrieve the secondary files for the requested mod.
@@ -0,0 +1,49 @@
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:
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]] = "",
): ...
def addButton(self: TaskDialog, button: TaskDialogButton) -> TaskDialog: ...
def addContent(self: TaskDialog, widget: PyQt6.QtWidgets.QWidget): ...
def exec(self: TaskDialog) -> PyQt6.QtWidgets.QMessageBox.StandardButton: ...
def setContent(self: TaskDialog, content: str) -> TaskDialog: ...
def setDetails(self: TaskDialog, details: str) -> TaskDialog: ...
def setIcon(
self: TaskDialog, icon: PyQt6.QtWidgets.QMessageBox.Icon
) -> TaskDialog: ...
def setMain(self: TaskDialog, main: str) -> TaskDialog: ...
def setRemember(self: TaskDialog, action: str, file: str = "") -> TaskDialog: ...
def setTitle(self: TaskDialog, title: str) -> TaskDialog: ...
def setWidth(self: TaskDialog, widget: int): ...
class TaskDialogButton:
@overload
def __init__(
self: TaskDialogButton,
text: str,
description: str,
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
): ...
@overload
def __init__(
self: TaskDialogButton,
text: str,
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
): ...
+2 -3
View File
@@ -12,7 +12,7 @@ import io
import os
import re
from setuptools import setup
from setuptools import find_packages, setup
def read(*names, **kwargs):
@@ -45,8 +45,7 @@ setup(
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=find_packages(),
install_requires=[],
python_requires="==3.10.*",
classifiers=[