Compare commits

..
Author SHA1 Message Date
Mikaël Capelle ca2ca66de8 Fix version in config. 2022-04-21 21:16:20 +02:00
40 changed files with 3959 additions and 10371 deletions
+24 -26
View File
@@ -1,38 +1,36 @@
name: Build Documentation
on:
pull_request:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Install
run: |
poetry install
- name: Install libgl1
run: sudo apt install -y libgl1 libegl1 libglib2.0-0 libxkbcommon0 libdbus-1-3
- name: Copy stubs
run: cp stubs/2.5.2/mobase-stubs/__init__.pyi docs/mobase.py
- name: Build
run: poetry run sphinx-build -b html docs/source docs/build
env:
PYTHONPATH: docs
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
name: Deploy Documentation
uses: JamesIves/github-pages-deploy-action@v4
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
repository-name: ModOrganizer2/python-plugins-doc
branch: master
folder: docs/build
- uses: actions/checkout@v2
with:
persist-credentials: false
# Standard drop-in approach that should work for most people.
- uses: ammaraskar/sphinx-action@master
env:
PYTHONPATH: .
with:
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py"
docs-folder: "docs/"
- name: Install SSH Client 🔑
uses: webfactory/ssh-agent@v0.4.1
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
SSH: true
REPOSITORY_NAME: ModOrganizer2/python-plugins-doc
BRANCH: master
FOLDER: docs/build/html
+16 -12
View File
@@ -5,16 +5,20 @@ on: [push, pull_request]
jobs:
checks:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Install
run: |
poetry install
- name: Lint
run: |
poetry run poe lint
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Test with tox
run: tox -e py38-lint
+18
View File
@@ -0,0 +1,18 @@
name: Check Documentation
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
# Standard drop-in approach that should work for most people.
- uses: ammaraskar/sphinx-action@master
env:
PYTHONPATH: .
with:
pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py"
docs-folder: "docs/"
+29 -46
View File
@@ -1,57 +1,40 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
name: Upload Python Package
on:
push:
tags: ["*"]
release:
types: [published]
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Replace string
uses: frabert/replace-string-action@v1.1
id: version
with:
string: ${{ github.ref_name }}
pattern: "v?([0-9][.][0-9][.][0-9]).*"
replace-with: "$1"
- uses: actions/setup-python@v2
with:
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Build
run: |
cd stubs/setup
mkdir mobase-stubs
cp -r ../${{ steps.version.outputs.replaced }}/mobase-stubs/* mobase-stubs/
sed -i 's/__version__ = ".*"/__version__ = "${{ github.ref_name }}"/' mobase-stubs/__init__.pyi
TAG=${{ github.ref_name }}
poetry version ${TAG#v}
poetry build
- name: Store the distribution packages
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: stubs/setup/dist/
deploy:
publish-to-pypi:
name: Publish Python 🐍 distribution 📦 to PyPI
needs:
- build
runs-on: ubuntu-latest
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- uses: actions/checkout@v2
- name: Replace string
uses: frabert/replace-string-action@v1.1
id: version
with:
string: ${{ github.event.release.tag_name }}
pattern: "v?([0-9][.][0-9][.][0-9]).*"
replace-with: "$1"
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
cd stubs/setup
cp ../${{ steps.version.outputs.replaced }}/mobase.pyi mobase-stubs/__init__.pyi
python setup.py sdist bdist_wheel
twine upload dist/*
+1 -2
View File
@@ -2,10 +2,9 @@
.mypy_cache
__pycache__
.vscode
**/*.egg-info
# The 'bin/' directory:
bin
docs/build
docs/mobase.py
docs/source/api
docs/source/api
+49 -47
View File
@@ -16,12 +16,23 @@ MO2.
You can install stubs for a specific version of MO2:
```bash
pip install mobase-stubs==2.5.*
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
@@ -33,39 +44,35 @@ 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 `python310.dll` in your MO2 installation path, then you need **Python 3.10**.
have a `python38.dll` in your MO2 installation path, then you need **Python 3.8**.
To generate the stubs, you can run:
```bash
# install the package
poetry install
# change the output folder to whatever you want
mo2-stubs-generator -c configs/config-2.4.yml -o mobase-stubs ${MO2_INSTALL_PATH}
```
# Change the output folder to whatever you want:
python main.py -c configs\config-2.4.yml ${MO2_INSTALL_PATH}
```
Where `${MO2_INSTALL_PATH}` is the path to your MO2 installation (the one
containing `ModOrganizer.exe`).
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` 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`.
The stubs are generated under `stubs/setup/mobase-stubs/__init__.pyi`, you
can change the output file by using the `-o` option
The latest stubs are kept under `stubs/setup/mobase-stubs/__init__.pyi`,
and when a new version is released, the stubs are backed-up under
`stubs/x.y.z/mobase.pyi`.
A few options are available for `mo2-stubs-generator`:
A few options are available for `main.py`:
```bash
$ mo2-stubs-generator --help
usage: stubs generator for the MO2 python interface [-h] [-o OUTPUT] [-v] [-c CONFIG] INSTALL_DIR
```
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
options:
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
output folder (default stubs/setup/mobase-stubs)
output file (default stubs/setup/mobase-stubs/__init__.pyi)
-v, --verbose verbose mode (all logs go to stderr)
-c CONFIG, --config CONFIG
configuration file
@@ -74,7 +81,19 @@ options:
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.
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`).
## Configuration file
@@ -83,37 +102,20 @@ 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/](https://pypi.org/project/mobase-stubs/)
should be done automatically when a new Github tag is pushed.
The upload of the stubs to https://pypi.org/project/mobase-stubs/ should be
done automatically when a new Github release is made.
## Extras — Using `mobase` in a Python interpreter
## Extras — Starts a python interpreter with `mobase`
It is possible to start a (i)python interpreter with `mobase` imported by running
It is possible to start a (i)python interpret with `mobase` imported by running:
```bash
python -i -m mo2.stubs.generator.loader ${MO2_INSTALL_PATH}
```
python -im generator.loader ${MO2_INSTALL_PATH}
```
You can also import `mobase` in your code using the following (after installing
this package):
This has no real usage except for MO2 developers since most classes from the `mobase` module cannot be instantiated.
```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
# License
The MIT License (MIT)
+86 -399
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+35
View File
@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx-rtd-theme
sphinx-autodoc-typehints
sphinx-automodapi
PyQt6
PyQt5
+10 -2
View File
@@ -10,13 +10,21 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath("../../stubs/2.4.0/"))
# -- Project information -----------------------------------------------------
project = "MO2 Python Plugin API"
copyright = "2023, Holt59"
copyright = "2020, Holt59"
author = "Holt59"
# The full version, including alpha/beta/rc tags
release = "2.3rc1"
# -- General configuration ---------------------------------------------------
@@ -61,5 +69,5 @@ html_favicon = "mo2.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]
html_static_path = ["_static"]
html_extra_path = [".nojekyll"]
+8
View File
@@ -0,0 +1,8 @@
# -*- encoding: utf-8 -*-
import logging
import sys
logging.basicConfig(stream=sys.stderr, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
+94
View File
@@ -0,0 +1,94 @@
# -*- encoding: utf-8 -*-
import importlib.machinery
import importlib.util
import os
import sys
from pathlib import Path
def load_module(name: str, path: Path):
# Create the loader:
loader = importlib.machinery.ExtensionFileLoader( # type: ignore
name, path.as_posix()
)
# Extract the spec:
spec = importlib.util.spec_from_loader(name, loader)
# Create the module and execute it?
module = importlib.util.module_from_spec(spec)
if module is None:
raise ImportError(f"Failed to import module {name} from {path}.")
loader.exec_module(module)
return module
def load_mobase(path: Path, moprivate: bool = False):
"""
Load the mobase from the given MO2 installation path and
returns it.
Args:
path: Path to the MO2 installation (folder containing the ModOrganizer.exe).
moprivate: If True, the moprivate module will also be loaded and returned
alongside mobase.
Returns: The mobase module.
"""
# We need absolute path for loading DLL and modules:
path = path.resolve()
# Adding to PATH environment variable for python < 3.8 and
# via os.add_dll_directory (python >= 3.8).
# See: https://stackoverflow.com/a/58632354/2666289
if sys.version_info < (3, 8):
os.environ["PATH"] = os.pathsep.join(
[str(path), str(path.joinpath("dlls")), os.environ.get("PATH", "")]
)
else:
os.add_dll_directory(str(path)) # type: ignore[attr-defined]
os.add_dll_directory(str(path.joinpath("dlls"))) # type: ignore[attr-defined]
# We need to add plugins/data to sys.path, mainly for PyQt5
sys.path.insert(1, path.joinpath("plugins", "data").as_posix())
mobase = load_module("mobase", path.joinpath("plugins", "data", "pythonrunner.dll"))
if not moprivate:
return mobase
moprivate = load_module(
"moprivate", path.joinpath("plugins", "data", "pythonrunner.dll")
)
return mobase, moprivate
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
"Load mobase python module from MO2 installation directory"
)
parser.add_argument(
"install_dir",
metavar="INSTALL_DIR",
type=Path,
default=None,
help="installation directory of Mod Organizer 2",
)
parser.add_argument(
"-p", "--private", action="store_true", help="also load the moprivate module"
)
args = parser.parse_args()
if args.private:
mobase, moprivate = load_mobase(args.install_dir, moprivate=True)
else:
mobase = load_mobase(args.install_dir)
+713
View File
File diff suppressed because it is too large Load Diff
+587
View File
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
# -*- encoding: utf-8 -*-
from collections import OrderedDict
from typing import Dict, List, Optional, Union
from . import logger
from .mtypes import Class, CType, Function, Type
class MobaseRegister:
"""
Class that register classes.
"""
objects: Dict[str, Union[Class, List[Function]]]
def __init__(self):
self.raw_objects: Dict[str, Union[type]] = OrderedDict()
self.objects = {}
self._cpptypes = {}
self.cpp2py = {}
def add_object(self, name, object):
self.raw_objects[name] = object
def make_object(
self, name: str, e: Optional[type] = None
) -> Union["Class", List["Function"]]:
"""
Construct a Function, Class or Enum for the given object.
Args:
name: The name of the object to inspect.
e: The object to inspect, or None to fetch it from the underlying list.
Returns:
A Class object for the given type, or a list of function overloads.
"""
from .parser import is_enum, make_class, make_enum, make_functions
if e is None:
e = self.raw_objects[name]
if name not in self.raw_objects:
self.raw_objects[name] = e
if name not in self.objects:
if is_enum(e):
self.objects[name] = make_enum(name, e)
elif isinstance(e, type):
self.objects[name] = make_class(name, e, self)
elif callable(e):
self.objects[name] = make_functions(name, e)
return self.objects[name]
def get_object(self, name: str):
"""
Retrieve the object if the given name. Fails if no object with this
name exists (if `make_object(name, ...)` has never been called).
Args:
name: Name of the object to retrieve.
Returns:
The object with the given name.
"""
return self.objects[name]
def register_type(self, ptype: "Type", ctype: "CType"):
"""Register an equivalence between a python name and a C++ name.
Args:
python_name: Name of the Python class.
cpp_name: Name of the C++ class.
"""
# Register the const equivalent for smart pointers:
cname = ctype.name
if ctype.is_smart_pointer():
if cname.find(" const >") != -1:
c2name = cname.replace(" const >", ">")
if c2name in self._cpptypes:
ptype = self.cpp2py[c2name]
# Not the const, replace the const one:
else:
c2name = cname.replace(">", " const >")
if c2name in self._cpptypes and self.cpp2py[c2name].is_object():
self._cpptypes[c2name] = ctype
self.cpp2py[c2name] = ptype
logger.warning(
"Replace registration {} [c++] with {} [python] using {}"
" information.".format(c2name, ptype.name, cname) # noqa: E501
)
if cname not in MOBASE_REGISTER.cpp2py:
self._cpptypes[cname] = ctype
self.cpp2py[cname] = ptype
logger.info("Registered {} [c++] as {} [python].".format(cname, ptype.name))
@property
def py2cpp(self):
result = {v.name: [] for v in self.cpp2py.values()}
for k in self.cpp2py:
result[self.cpp2py[k].name].append(self._cpptypes[k])
return result
MOBASE_REGISTER = MobaseRegister()
+504
View File
File diff suppressed because it is too large Load Diff
@@ -1,42 +1,24 @@
import logging
from typing import Any, Iterable, TextIO
# -*- encoding: utf-8 -*-
from typing_extensions import TypeIs
from typing import List, TextIO, Tuple, Union
from .mtypes import Class, Constant, Enum, Function, Method, Property, PyTyping
from . import logger
from .mtypes import Class, Enum, Function, Method, Property
from .utils import Settings
LOGGER = logging.getLogger(__package__)
def is_list_of_functions(e: Any | Iterable[Any]) -> TypeIs[list[Function]]:
if not isinstance(e, list):
return False
return all(isinstance(x, Function) for x in e)
class Writer:
_output: TextIO
_settings: Settings
def __init__(self, package: str, output: TextIO, settings: Settings):
self._package = package.split(".")
def __init__(self, output: TextIO, settings: Settings):
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,
*values: object,
sep: str | None = " ",
end: str | None = "\n",
flush: bool = False,
) -> None:
print(*values, sep=sep, end=end, flush=flush, file=self._output)
def _print(self, *args, **kwargs):
kwargs["file"] = self._output
print(*args, **kwargs)
def _print_doc(self, doc: str, indent: str):
"""
@@ -54,7 +36,7 @@ class Writer:
self._print('__version__ = "{}"'.format(version))
self._print()
def print_imports(self, imports: list[str | tuple[str, list[str]]]):
def print_imports(self, imports: List[Union[str, Tuple[str, List[str]]]]):
"""
Print the given imports.
"""
@@ -73,12 +55,12 @@ class Writer:
if fn.has_overloads():
self._print("{}@overload".format(indent))
sig_return_type = ""
srtype = ""
if not fn.ret.type.is_none():
sig_return_type = " -> " + self._fix_typing(fn.ret.type.typing())
else:
sig_return_type = " -> None"
srtype = " -> " + fn.ret.type.typing(self._settings)
fargs = fn.args
largs: List[str] = []
if isinstance(fn, Method):
if fn.is_static():
self._print("{}@staticmethod".format(indent))
@@ -86,19 +68,17 @@ class Writer:
if fn.is_abstract():
self._print("{}@abc.abstractmethod".format(indent))
python_args: list[str] = []
for arg in fn.args:
tmp = "{}: {}".format(arg.name, self._fix_typing(arg.type.typing()))
largs.insert(0, "self")
fargs = fargs[1:]
for i, arg in enumerate(fargs):
tmp = "{}: {}".format(arg.name, arg.type.typing(self._settings))
if arg.has_default_value():
tmp += " = {}".format(arg.value)
python_args.append(tmp)
largs.append(tmp)
sargs = ", ".join(largs)
self._print(
"{}def {}({}){}:".format(
indent, fn.name, ", ".join(python_args), sig_return_type
),
end="",
)
self._print("{}def {}({}){}:".format(indent, fn.name, sargs, srtype), end="")
# Add the documentation, if any:
doc = ""
@@ -113,12 +93,9 @@ class Writer:
if any(arg.doc for arg in args):
doc += "\nArgs:\n"
for arg in args:
arg_doc_list = arg.doc.strip().split("\n")
arg_doc = "\n".join(
[arg_doc_list[0]]
+ [" " + line_doc for line_doc in arg_doc_list[1:]]
)
doc += " " + arg.name + ": " + arg_doc + "\n"
adocl = arg.doc.strip().split("\n")
adoc = "\n".join([adocl[0]] + [" " + ldoc for ldoc in adocl[1:]])
doc += " " + arg.name + ": " + adoc + "\n"
if not fn.ret.type.is_none() and fn.ret.doc:
doc += "\nReturns:\n " + fn.ret.doc.strip() + "\n"
@@ -128,7 +105,7 @@ class Writer:
for rai in fn.raises:
doc += (
" "
+ self._fix_typing(rai.type.typing())
+ rai.type.typing(self._settings)
+ ": "
+ rai.doc.strip()
+ "\n"
@@ -150,7 +127,7 @@ class Writer:
"""
if prop.type.is_object() or prop.type.is_any():
LOGGER.warning(
logger.warning(
"Property {}.{} does not have a specified type.".format(
cls.name, prop.name
)
@@ -159,14 +136,14 @@ class Writer:
self._print("{}@property".format(indent))
self._print(
"{}def {}(self) -> {}: ...".format(
indent, prop.name, self._fix_typing(prop.type.typing())
indent, prop.name, prop.type.typing(self._settings)
)
)
if not prop.is_read_only():
self._print("{}@{}.setter".format(indent, prop.name))
self._print(
"{}def {}(self, arg0: {}) -> None: ...".format(
indent, prop.name, self._fix_typing(prop.type.typing())
"{}def {}(self, arg0: {}): ...".format(
indent, prop.name, prop.type.typing(self._settings)
)
)
self._print()
@@ -178,10 +155,7 @@ class Writer:
bc = ""
if cls.bases or cls.is_abstract():
bases: list[str] = [
bc.canonical_name if bc.package.startswith("mobase") else bc.full_name
for bc in cls.bases
]
bases = [str(bc) 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) + ")"
@@ -205,8 +179,8 @@ class Writer:
self._print()
# Inner classes:
for inner_class in cls.inner_classes:
self.print_class(inner_class, indent=indent + " ")
for iclass in cls.inner_classes:
self.print_class(iclass, indent=indent + " ")
self._print()
# Constants:
@@ -217,7 +191,7 @@ class Writer:
typing = ""
if constant.type is not None:
typing = ": {}".format(self._fix_typing(constant.type.typing()))
typing = ": {}".format(constant.type.typing(self._settings))
# Note: We do not print the value, we use ...
self._print(
@@ -243,7 +217,6 @@ class Writer:
cls.methods,
key=lambda m: (m.name != "__init__", not m.is_special(), m.name),
)
for method in methods:
self.print_function(method, indent=indent + " ")
@@ -254,28 +227,3 @@ 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_constent(self, constant: Constant):
assert constant.type is not None
self._print(
"{}: {} = ...".format(
constant.name, self._fix_typing(constant.type.typing())
)
)
def print_object(self, e: Class | Constant | list[Function] | PyTyping):
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)
else:
self.print_constent(e)
+169
View File
@@ -0,0 +1,169 @@
# -*- encoding: utf-8 -*-
import argparse
import logging
from pathlib import Path
import black
from generator import logger
from generator.loader import load_mobase
from generator.mtypes import Class, Function, Type
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()
if args.verbose:
logger.setLevel(logging.INFO)
# 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
# For now, ignore this since it is a submodule and we
# not handle them.
if name == "widgets":
continue
objects.append((name, getattr(mobase, name)))
# Enum first, and then alphabetical. Might cause issue with base classes, so
# maybe create a kind of dependency...
# For argument or return types, this should not be an issue since we quote
# everything from mobase.
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)
writer.print_version(settings.mobase["__version__"]) # type: ignore
writer.print_imports(
[
"abc",
("enum", ["Enum"]),
(
"typing",
[
"Dict",
"Iterator",
"List",
"Tuple",
"Union",
"Any",
"Optional",
"Callable",
"overload",
"TypeVar",
"Type",
],
),
"PyQt5.QtCore",
"PyQt5.QtGui",
"PyQt5.QtWidgets",
]
)
# Needs to define the MVariant and GameFeatureType type:
writer._print("MoVariant = {}".format(Type.MO_VARIANT))
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(
args.output,
fast=False,
mode=black.Mode(is_pyi=args.output.name.endswith("pyi")),
write_back=black.WriteBack.YES,
)
Generated
-787
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More