Compare commits

...
11 changed files with 420 additions and 82 deletions
+16
View File
@@ -94,6 +94,22 @@ It is possible to start a (i)python interpreter with `mobase` imported by runnin
python -i -m mo2.stubs.generator.loader ${MO2_INSTALL_PATH}
```
You can also import `mobase` in your code using the following (after installing
this package):
```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.
+118 -4
View File
@@ -3004,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.
@@ -3140,6 +3140,120 @@ mobase:
mobase.widgets:
TaskDialog: {}
TaskDialog:
__doc__: Customizable choice dialog.
TaskDialogButton: {}
__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.
+2
View File
@@ -3,3 +3,5 @@
import logging
LOGGER = logging.getLogger(__name__)
from .loader import load_mobase # noqa: F401
+23 -25
View File
@@ -3,25 +3,29 @@
import argparse
import inspect
import logging
import types
from pathlib import Path
from typing import Callable, TextIO, cast
from typing import Callable
import black
import isort
from . import LOGGER
from .loader import load_mobase
from .mtypes import Class, Function, PyType
from .mtypes import Class, PyTyping
from .parser import is_enum
from .register import MobaseRegister
from .utils import Settings, clean_class
from .writer import Writer
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
@@ -32,6 +36,11 @@ def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, ob
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
@@ -42,23 +51,23 @@ def add_mobase_header(writer: Writer):
[
"abc",
("enum", ["Enum"]),
("pathlib", ["Path"]),
"os",
(
"typing",
[
"Any",
"Callable",
"Dict",
"Iterator",
"List",
"Tuple",
"Union",
"Any",
"Optional",
"Callable",
"overload",
"Sequence",
"Set",
"TypeVar",
"Tuple",
"Type",
"TypeVar",
"Union",
],
),
"PyQt6.QtCore",
@@ -67,13 +76,6 @@ def add_mobase_header(writer: Writer):
]
)
# 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(
@@ -148,8 +150,6 @@ def main():
"mobase": extract_objects(
mobase,
[
# we do not want the real MoVariant
"MoVariant",
# the "real" IPlugin is IPluginBase
"IPlugin",
],
@@ -187,7 +187,10 @@ def main():
# Path the class using the configuration:
settings.patch_class(c)
elif isinstance(c, list) and isinstance(c[0], Function):
elif isinstance(c, PyTyping):
...
elif is_list_of_functions(c):
settings.patch_functions(c)
else:
@@ -220,12 +223,7 @@ def main():
# 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)
writer.print_object(c)
black.format_file_in_place(
output_folder.joinpath("__init__.pyi"),
+5 -2
View File
@@ -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()
@@ -35,7 +38,7 @@ def load_mobase(path: Path):
import mobase # type: ignore
return mobase
return mobase # type: ignore
if __name__ == "__main__":
+24
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import re
from typing import Final, TypeVar
class PyType:
@@ -430,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
+5 -1
View File
@@ -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:
@@ -46,6 +46,10 @@ class MobaseRegister:
elif callable(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]
def get_object(self, name: str):
+21 -2
View File
@@ -1,12 +1,16 @@
# -*- 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
@@ -240,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)
+55 -31
View File
@@ -3,8 +3,8 @@ 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,
@@ -25,12 +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]
def getFileVersion(filepath: FileWrapper) -> str:
def getFileVersion(filepath: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]) -> str:
"""
Retrieve the file version of the given executable.
@@ -42,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.
@@ -54,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.
@@ -343,7 +345,11 @@ class ExecutableForcedLoadSetting:
) -> ExecutableForcedLoadSetting: ...
class ExecutableInfo:
def __init__(self: ExecutableInfo, title: str, binary: FileWrapper): ...
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: ...
@@ -354,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: ...
@@ -1280,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]:
"""
@@ -1945,7 +1951,9 @@ class IOrganizer:
"""
...
def findFileInfos(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[FileInfo], bool]
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.
@@ -1960,7 +1968,9 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[str], bool]
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.
@@ -1975,7 +1985,9 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, patterns: Sequence[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.
@@ -1990,7 +2002,7 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, pattern: 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.
@@ -2036,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.
@@ -2350,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.
@@ -2398,9 +2414,9 @@ class IOrganizer:
...
def startApplication(
self: IOrganizer,
executable: FileWrapper,
executable: Union[str, os.PathLike, PyQt6.QtCore.QFileInfo],
args: Sequence[str] = [],
cwd: DirectoryWrapper = "",
cwd: Union[str, os.PathLike, PyQt6.QtCore.QDir] = "",
profile: str = "",
forcedCustomOverwrite: str = "",
ignoreCustomOverwrite: bool = False,
@@ -3710,7 +3726,9 @@ class ISaveGame:
"""
def __init__(self: ISaveGame): ...
def allFiles(self: ISaveGame) -> Sequence[str]:
def allFiles(
self: ISaveGame,
) -> Sequence[Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]]:
"""
Returns:
The list of all files related to this save.
@@ -3727,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.
@@ -4235,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:
@@ -4277,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.
@@ -4320,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.
@@ -4335,7 +4357,9 @@ class UnmanagedMods(abc.ABC):
"""
...
@abc.abstractmethod
def secondaryFiles(self: UnmanagedMods, mod_name: str) -> Sequence[str]:
def secondaryFiles(
self: UnmanagedMods, mod_name: str
) -> Sequence[Union[str, os.PathLike, PyQt6.QtCore.QFileInfo]]:
"""
Retrieve the secondary files for the requested mod.
+137 -15
View File
@@ -9,6 +9,10 @@ import PyQt6.QtGui
import PyQt6.QtWidgets
class TaskDialog:
"""
Customizable choice dialog.
"""
def __init__(
self: TaskDialog,
parent: PyQt6.QtWidgets.QWidget = None,
@@ -19,31 +23,149 @@ class TaskDialog:
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): ...
):
"""
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.
"""
...
+14 -2
View File
@@ -11,8 +11,10 @@
import io
import os
import re
from collections import defaultdict
from pathlib import Path
from setuptools import find_packages, setup
from setuptools import setup
def read(*names, **kwargs):
@@ -34,18 +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"),
packages=find_packages(),
packages=list(package_data.keys()),
package_data=package_data,
install_requires=[],
python_requires="==3.10.*",
classifiers=[