Compare commits

...
18 changed files with 855 additions and 378 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
+46 -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,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
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:
@@ -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.
-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",
"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()
# 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"],
},
)
@@ -3,3 +3,5 @@
import logging
LOGGER = logging.getLogger(__name__)
from .loader import load_mobase # noqa: F401
+237
View File
@@ -0,0 +1,237 @@
# -*- 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",
[
"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:
@@ -10,15 +11,6 @@ class PyType:
Class representing a python type.
"""
# The `MoVariant` actual type - This should be list["MoVariant"] and
# Dict[str, "MoVariant"], but mypy (and other type checkers) do not
# handle recursive definition yet:
MO_VARIANT = """Union[None, bool, int, str, list[Any], dict[str, Any]]"""
# File/Directory wrappers
FILE_WRAPPER = """Union[str, PyQt6.QtCore.QFileInfo, Path]"""
DIRECTORY_WRAPPER = """Union[str, PyQt6.QtCore.QDir, Path]"""
name: str
def __init__(self, name: str | type):
@@ -30,16 +22,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 +36,12 @@ class PyType:
A valid typing representation for this type.
"""
# IPluginBase -> IPlugin
if self.name == "IPluginBase":
if self.name == "mobase.IPluginBase":
return "IPlugin"
# PathLike should be [] in the stubs
self.name = self.name.replace("os.PathLike", "os.PathLike[str]")
return self.name
def is_none(self) -> bool:
@@ -142,10 +131,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 +314,7 @@ class Class:
def __init__(
self,
package: str,
name: str,
bases: list[Class],
methods: list[Method],
@@ -325,7 +323,7 @@ class Class:
inner_classes: list[Class] = [],
doc: str = "",
):
self.package = package
self.name = name
self.bases = bases
self.methods = methods
@@ -364,6 +362,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 +386,6 @@ class Class:
def is_deprecated(self):
return self.deprecated
def __str__(self):
return self.canonical_name
class PyClass(Class):
@@ -391,9 +396,10 @@ class PyClass(Class):
def __init__(
self,
package: str,
name: str,
):
super().__init__(name, [], [])
super().__init__(package, name, [], [])
self.abstract = False
@@ -403,12 +409,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 +425,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,10 +3,9 @@ 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,
Iterator,
@@ -25,14 +24,12 @@ 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[str], PyQt6.QtCore.QFileInfo]
) -> str:
"""
Retrieve the file version of the given executable.
@@ -44,7 +41,9 @@ def getFileVersion(filepath: FileWrapper) -> str:
"""
...
def getIconForExecutable(executable: FileWrapper) -> PyQt6.QtGui.QIcon:
def getIconForExecutable(
executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]
) -> PyQt6.QtGui.QIcon:
"""
Retrieve the icon of an executable. Currently this always extracts the biggest icon.
@@ -56,7 +55,9 @@ def getIconForExecutable(executable: FileWrapper) -> PyQt6.QtGui.QIcon:
"""
...
def getProductVersion(executable: FileWrapper) -> str:
def getProductVersion(
executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]
) -> str:
"""
Retrieve the product version of the given executable.
@@ -345,7 +346,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[str], PyQt6.QtCore.QFileInfo],
): ...
def arguments(self: ExecutableInfo) -> Sequence[str]: ...
def asCustom(self: ExecutableInfo) -> ExecutableInfo: ...
def binary(self: ExecutableInfo) -> PyQt6.QtCore.QFileInfo: ...
@@ -356,7 +361,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[str], PyQt6.QtCore.QDir]
) -> ExecutableInfo: ...
def workingDirectory(self: ExecutableInfo) -> PyQt6.QtCore.QDir: ...
@@ -1282,7 +1287,7 @@ class IInstallationManager:
def installArchive(
self: IInstallationManager,
mod_name: GuessedString,
archive: FileWrapper,
archive: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo],
mod_id: int = 0,
) -> Tuple[InstallResult, str, int]:
"""
@@ -1947,7 +1952,9 @@ class IOrganizer:
"""
...
def findFileInfos(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[FileInfo], bool]
self: IOrganizer,
path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir],
filter: Callable[[FileInfo], bool],
) -> Sequence[FileInfo]:
"""
Find files in the virtual directory matching the specified filter.
@@ -1962,7 +1969,9 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, filter: Callable[[str], bool]
self: IOrganizer,
path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir],
filter: Callable[[str], bool],
) -> Sequence[str]:
"""
Find files in the given folder that matches the given filter.
@@ -1977,7 +1986,9 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, patterns: Sequence[str]
self: IOrganizer,
path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir],
patterns: Sequence[str],
) -> Sequence[str]:
"""
Find files in the given folder that matches one of the given glob patterns.
@@ -1992,7 +2003,9 @@ class IOrganizer:
...
@overload
def findFiles(
self: IOrganizer, path: DirectoryWrapper, pattern: str
self: IOrganizer,
path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir],
pattern: str,
) -> Sequence[str]:
"""
Find files in the given folder that matches the given glob pattern.
@@ -2038,7 +2051,9 @@ class IOrganizer:
"""
...
def installMod(
self: IOrganizer, filename: FileWrapper, name_suggestion: str = ""
self: IOrganizer,
filename: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo],
name_suggestion: str = "",
) -> IModInterface:
"""
Install a mod archive at the specified location.
@@ -2352,7 +2367,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[str], PyQt6.QtCore.QFileInfo]
) -> str:
"""
Resolves a path relative to the virtual data directory to its absolute real path.
@@ -2400,9 +2417,9 @@ class IOrganizer:
...
def startApplication(
self: IOrganizer,
executable: FileWrapper,
executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo],
args: Sequence[str] = [],
cwd: DirectoryWrapper = "",
cwd: Union[str, os.PathLike[str], PyQt6.QtCore.QDir] = "",
profile: str = "",
forcedCustomOverwrite: str = "",
ignoreCustomOverwrite: bool = False,
@@ -3712,7 +3729,9 @@ class ISaveGame:
"""
def __init__(self: ISaveGame): ...
def allFiles(self: ISaveGame) -> Sequence[str]:
def allFiles(
self: ISaveGame,
) -> Sequence[Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]]:
"""
Returns:
The list of all files related to this save.
@@ -3729,7 +3748,9 @@ class ISaveGame:
The creation time of the save.
"""
...
def getFilepath(self: ISaveGame) -> str:
def getFilepath(
self: ISaveGame,
) -> Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]:
"""
Returns:
The path name to the (main) file or folder for the save.
@@ -4237,20 +4258,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 +4293,24 @@ class ScriptExtender(abc.ABC):
"""
...
@abc.abstractmethod
def loaderPath(self: ScriptExtender) -> str:
def loaderPath(
self: ScriptExtender,
) -> Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]:
"""
Returns:
The full path to the script extender loader.
"""
...
@abc.abstractmethod
def pluginPath(
self: ScriptExtender,
) -> Union[str, os.PathLike[str], 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.
@@ -4322,7 +4347,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[str], PyQt6.QtCore.QFileInfo]:
"""
Retrieve the reference file for the requested mod.
@@ -4337,7 +4364,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[str], 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
View File
@@ -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=[