mirror of
https://github.com/ModOrganizer2/pystubs-generation.git
synced 2026-07-27 14:07:13 -07:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
156b827389 | ||
|
|
104e8612d8 | ||
|
|
4f24d363ec | ||
|
|
2e4fd7c1a8 | ||
|
|
ee3faa0c87 | ||
|
|
c5ee7f8a43 | ||
|
|
33b7222b13 | ||
|
|
e083e0832e | ||
|
|
478af17c58 | ||
|
|
6ca51655aa | ||
|
|
0ec04f4e9e | ||
|
|
d43d067c84 | ||
|
|
06e0fe1cca | ||
|
|
c57280578d | ||
|
|
178b1d111d |
@@ -4,8 +4,8 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags: ["*"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -18,13 +18,13 @@ jobs:
|
||||
uses: frabert/replace-string-action@v1.1
|
||||
id: version
|
||||
with:
|
||||
string: ${{ github.event.release.tag_name }}
|
||||
string: ${{ github.ref_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.8'
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -35,6 +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/*
|
||||
twine upload dist/*
|
||||
|
||||
+2
-1
@@ -2,9 +2,10 @@
|
||||
.mypy_cache
|
||||
__pycache__
|
||||
.vscode
|
||||
**/*.egg-info
|
||||
|
||||
# The 'bin/' directory:
|
||||
bin
|
||||
docs/build
|
||||
docs/mobase.py
|
||||
docs/source/api
|
||||
docs/source/api
|
||||
|
||||
@@ -19,20 +19,9 @@ You can install stubs for a specific version of MO2:
|
||||
pip install mobase-stubs==2.3.2.*
|
||||
```
|
||||
|
||||
If you want development stubs, you can install them this way:
|
||||
```bash
|
||||
# Clone this repository:
|
||||
git clone https://github.com/ModOrganizer2/pystubs-generation.git
|
||||
|
||||
# Install the stubs:
|
||||
cd pystubs-generation/stubs/setup
|
||||
pip install .
|
||||
```
|
||||
|
||||
Some words of warning:
|
||||
|
||||
- The stubs are as correct as possible, but some errors are expected.
|
||||
- If you see a `InterfaceNotImplemented` class anywhere in the stubs, it means that
|
||||
a proper interface is currently not available.
|
||||
- Some classes are said (in the stubs) to inherit `QWidget` or `QObject`. This is true
|
||||
on the C++ side but NOT on the python side. The inheritance is only added to help with
|
||||
auto-completion since these classes also override `__getattr__` to dispatch to the
|
||||
@@ -44,35 +33,39 @@ Some words of warning:
|
||||
|
||||
The stubs are generated using python by parsing the `mobase` module.
|
||||
You need the version of python that matches your current MO2 installation: e.g., if you
|
||||
have a `python38.dll` in your MO2 installation path, then you need **Python 3.8**.
|
||||
have a `python310.dll` in your MO2 installation path, then you need **Python 3.10**.
|
||||
|
||||
To generate the stubs, you can run:
|
||||
|
||||
```
|
||||
# Change the output folder to whatever you want:
|
||||
python main.py -c configs\config-2.4.yml ${MO2_INSTALL_PATH}
|
||||
```bash
|
||||
# install the package (-e if you want editable mode)
|
||||
pip install [-e] .
|
||||
|
||||
# change the output folder to whatever you want
|
||||
mo2-stubs-generator -c configs/config-2.4.yml -o mobase-stubs ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
Where `${MO2_INSTALL_PATH}` is the path to your MO2 installation (the one containing `ModOrganizer.exe`).
|
||||
Where `${MO2_INSTALL_PATH}` is the path to your MO2 installation (the one
|
||||
containing `ModOrganizer.exe`).
|
||||
|
||||
The stubs are generated under `stubs/setup/mobase-stubs/__init__.pyi`, you
|
||||
can change the output file by using the `-o` option
|
||||
The latest stubs are kept under `stubs/setup/mobase-stubs/__init__.pyi`,
|
||||
and when a new version is released, the stubs are backed-up under
|
||||
`stubs/x.y.z/mobase.pyi`.
|
||||
The stubs are generated under `stubs/setup/mobase-stubs` by default, you
|
||||
can change the output file by using the `-o` option.
|
||||
The stubs under `stubs/setup/mobase-stubs` should not be committed as these are
|
||||
generated from the version stubs under `stubs/${VERSION}/mobase-stubs`.
|
||||
|
||||
A few options are available for `main.py`:
|
||||
A few options are available for `mo2-stubs-generator`:
|
||||
|
||||
```
|
||||
usage: Stubs generator for the MO2 python interface [-h] [-o OUTPUT] [-v] [-c CONFIG] INSTALL_DIR
|
||||
```bash
|
||||
$ mo2-stubs-generator --help
|
||||
usage: stubs generator for the MO2 python interface [-h] [-o OUTPUT] [-v] [-c CONFIG] INSTALL_DIR
|
||||
|
||||
positional arguments:
|
||||
INSTALL_DIR installation directory of Mod Organizer 2
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-o OUTPUT, --output OUTPUT
|
||||
output file (default stubs/setup/mobase-stubs/__init__.pyi)
|
||||
output folder (default stubs/setup/mobase-stubs)
|
||||
-v, --verbose verbose mode (all logs go to stderr)
|
||||
-c CONFIG, --config CONFIG
|
||||
configuration file
|
||||
@@ -81,19 +74,7 @@ optional arguments:
|
||||
The stubs generator will try hard to find a valid stubs for all classes
|
||||
and methods of `mobase`.
|
||||
A lot of information is available through the `-v` options. Without it,
|
||||
only conversions or fixes
|
||||
considered "strange" will be shown.
|
||||
For instance, here is the output with the current `config-2.4.yml` file:
|
||||
|
||||
```
|
||||
WARNING: Replacing IOrganizer::FileInfo with FileInfo.
|
||||
WARNING: Replacing IOrganizer::FileInfo with FileInfo.
|
||||
WARNING: Replacing IPluginInstaller::EInstallResult with InstallResult.
|
||||
WARNING: Replacing IPluginInstaller::EInstallResult with InstallResult.
|
||||
```
|
||||
|
||||
As you can see, only a few types were manually fixed (specified in
|
||||
`config-2.4.yml`).
|
||||
only conversions or fixes considered "strange" will be shown.
|
||||
|
||||
## Configuration file
|
||||
|
||||
@@ -102,20 +83,21 @@ deduced by `main` (or are too complex to deduce), and the documentation for ever
|
||||
|
||||
## Uploading the stubs to pypi
|
||||
|
||||
The upload of the stubs to https://pypi.org/project/mobase-stubs/ should be
|
||||
done automatically when a new Github release is made.
|
||||
The upload of the stubs to [https://pypi.org/project/mobase-stubs/](https://pypi.org/project/mobase-stubs/)
|
||||
should be done automatically when a new Github tag is pushed.
|
||||
|
||||
## Extras — Starts a python interpreter with `mobase`
|
||||
## Extras — Using `mobase` in a Python interpreter
|
||||
|
||||
It is possible to start a (i)python interpret with `mobase` imported by running:
|
||||
It is possible to start a (i)python interpreter with `mobase` imported by running
|
||||
|
||||
```
|
||||
python -im generator.loader ${MO2_INSTALL_PATH}
|
||||
```bash
|
||||
python -i -m mo2.stubs.generator.loader ${MO2_INSTALL_PATH}
|
||||
```
|
||||
|
||||
This has no real usage except for MO2 developers since most classes from the `mobase` module cannot be instantiated.
|
||||
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
|
||||
for MO2 developers.
|
||||
|
||||
# License
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
||||
@@ -642,7 +642,7 @@ mobase:
|
||||
The path to copy the entry to. If the path ends with / or \\, the entry will
|
||||
be copied in the corresponding directory instead of replacing it. If the
|
||||
given path is empty (`""`), the entry is copied directly under this tree.
|
||||
policy: Policy to use to resolve conflicts.
|
||||
insert_policy: Policy to use to resolve conflicts.
|
||||
returns: The new entry (copy of the specified entry).
|
||||
raises:
|
||||
RuntimeError: If the entry could not be copied.
|
||||
@@ -2947,7 +2947,7 @@ mobase:
|
||||
getMissingAssets:
|
||||
__doc__: Retrieve missing assets from the save.
|
||||
args:
|
||||
filepath: The save to find missing assets for.
|
||||
save: The save to find missing assets for.
|
||||
returns: |
|
||||
A collection of missing assets and the modules that can supply those assets.
|
||||
|
||||
@@ -3095,7 +3095,7 @@ mobase:
|
||||
args:
|
||||
value: String to parse.
|
||||
scheme: Scheme to use to parse the string.
|
||||
manual_input: True if the given string should be treated as user input.
|
||||
is_manual: True if the given string should be treated as user input.
|
||||
|
||||
scheme:
|
||||
returns: The version scheme in effect for this VersionInfo.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+22
-23
@@ -15,7 +15,7 @@ This guide assumes that:
|
||||
`Python extension <https://marketplace.visualstudio.com/items?itemName=ms-python.python>`_.
|
||||
- You have Python installed: https://www.python.org/downloads/.
|
||||
|
||||
- It is recommended but not mandatory to use the Python version that is used by MO2.
|
||||
- You must use the Python version that used by MO2.
|
||||
You can check the ``pythonXX.dll`` in the MO2 installation folder to find the Python version used by MO2 (``python38.dll`` means Python 3.8).
|
||||
- You obviously need a valid MO2 installation: https://github.com/modorganizer2/modorganizer/releases
|
||||
|
||||
@@ -28,8 +28,8 @@ Preparation
|
||||
-----------
|
||||
|
||||
**Note:** This part is optional but highly recommended if you want a proper environment to work with.
|
||||
Everything here is written to be as simple as possible but you can of course adapt it to your preferences: use a python virtual
|
||||
environment, use workspace settings instead of global ones, etc.
|
||||
Everything here is written to be as simple as possible but you can of course adapt it to your preferences:
|
||||
use a python virtual environment, use workspace settings instead of global ones, etc.
|
||||
|
||||
1. Get the ``mobase`` stubs
|
||||
...........................
|
||||
@@ -39,11 +39,14 @@ as ``flake8`` or ``mypy``.
|
||||
Instead, we provide `stubs <https://stackoverflow.com/questions/59051631/what-is-the-use-of-stub-files-pyi-in-python>`_
|
||||
which can be used for auto-completion or type-checking.
|
||||
|
||||
The stubs for ``mobase`` are available at https://github.com/ModOrganizer2/pystubs-generation/tree/master/stubs.
|
||||
You want to download the ``mobase.pyi`` file in the folder corresponding to your MO2 version and put it under ``$MO2DIR/plugins/data``.
|
||||
You can install the stubs for ``mobase`` using ``pip``:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install mobase-stubs
|
||||
|
||||
This will install the stubs for ``mobase`` but also for PyQt5, which is heavily used by MO2.
|
||||
|
||||
**Note:** It is possible to put the stubs in a different location, but we are going to use ``$MO2DIR/plugins/data`` for PyQt5,
|
||||
so we might as well use it for the stubs.
|
||||
|
||||
2. Configure Visual Studio Code for ``mobase``
|
||||
..............................................
|
||||
@@ -57,32 +60,28 @@ Open ``settings.json`` (Ctrl+Shift+P, then "Open Settings (JSON)"), and add the
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.autoComplete.extraPaths": [
|
||||
"$MO2DIR\\plugins\\data",
|
||||
]
|
||||
|
||||
3. Configure ``mypy`` to find the ``mobase`` stubs
|
||||
..................................................
|
||||
3. [Optional] Configure ``black`` to auto-format your source files
|
||||
..................................................................
|
||||
|
||||
There are multiply way to configure ``mypy``:
|
||||
This step is optional for your own plugin but recent MO2 plugins use ``black``
|
||||
to get consistent formatting.
|
||||
|
||||
1. You can create a ``mypy.ini`` file somewhere containing:
|
||||
You can install ``black`` with ``pip``:
|
||||
|
||||
.. code-block:: ini
|
||||
.. code::
|
||||
|
||||
[mypy]
|
||||
mypy_path = $MO2DIR\plugins\data
|
||||
pip install black flake8-black
|
||||
|
||||
And then add the following to ``settings.json`` (with the correct path):
|
||||
To configure Visual Studio Code to auto-format your code with ``black`` when saving, open ``settings.json``
|
||||
(Ctrl+Shift+P, then "Open Settings (JSON)"), and add the following entries:
|
||||
|
||||
.. code-block:: json-object
|
||||
|
||||
"python.linting.mypyArgs": [
|
||||
"--config-file=path-to-mypy.ini",
|
||||
]
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"python.formatting.provider": "black",
|
||||
|
||||
2. You can set the ``MYPYPATH`` environment variable to ``$MO2DIR\plugins\data`` (this requires
|
||||
restarting VS code).
|
||||
|
||||
4. [Optional] Automatically reload plugins during development
|
||||
.............................................................
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- 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)
|
||||
@@ -1,95 +0,0 @@
|
||||
# -*- 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)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,109 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, Union, List
|
||||
|
||||
from . import logger
|
||||
from .mtypes import Class, Type, CType, Function
|
||||
|
||||
|
||||
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 make_enum, make_class, is_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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
# -*- 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.register import MOBASE_REGISTER
|
||||
from generator.parser import is_enum
|
||||
from generator.mtypes import Type, Class, Function
|
||||
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,
|
||||
)
|
||||
@@ -15,19 +15,26 @@ warn_return_any = True
|
||||
warn_unused_configs = True
|
||||
namespace_packages = True
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
multi_line_output = 3
|
||||
|
||||
[tox:tox]
|
||||
skipsdist = true
|
||||
envlist = py38-lint
|
||||
envlist = py310-lint
|
||||
|
||||
[testenv:py38-lint]
|
||||
[testenv:py310-lint]
|
||||
skip_install = true
|
||||
deps =
|
||||
black
|
||||
mypy
|
||||
flake8
|
||||
flake8-black
|
||||
PyQt5-stubs
|
||||
git+https://github.com/TilmanK/PyQt6-stubs.git
|
||||
types-PyYAML
|
||||
isort
|
||||
commands =
|
||||
black generator main.py --check --diff
|
||||
flake8 generator main.py
|
||||
mypy generator main.py
|
||||
black src --check --diff
|
||||
flake8 src
|
||||
mypy src
|
||||
isort -c src
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from setuptools import find_namespace_packages, setup
|
||||
|
||||
install_requires = ["black", "isort"]
|
||||
|
||||
dev_requires = [
|
||||
"black",
|
||||
"flake8-black",
|
||||
"flake8",
|
||||
"types-chardet",
|
||||
]
|
||||
|
||||
setup(
|
||||
name="mo2-stubs-generator",
|
||||
version="1.0.0",
|
||||
package_dir={"": "src"},
|
||||
packages=find_namespace_packages(where="src", include=["mo2.*"]),
|
||||
author="Holt59",
|
||||
author_email="capelle.mikael@gmail",
|
||||
description="Python stubs generator for mobase (MO2 Python API).",
|
||||
long_description=open("README.md").read(),
|
||||
url="https://github.com/ModOrganizer2/pystubs-generation",
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
],
|
||||
license="MIT",
|
||||
install_requires=install_requires,
|
||||
extras_require={"dev": dev_requires},
|
||||
entry_points={
|
||||
"console_scripts": ["mo2-stubs-generator=mo2.stubs.generator.__main__:main"],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Callable, TextIO, cast
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from . import LOGGER
|
||||
from .loader import load_mobase
|
||||
from .mtypes import Class, Function, PyType
|
||||
from .parser import is_enum
|
||||
from .register import MobaseRegister
|
||||
from .utils import Settings, clean_class
|
||||
from .writer import Writer
|
||||
|
||||
|
||||
def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, object]]:
|
||||
|
||||
objects: list[tuple[str, object]] = []
|
||||
|
||||
for name in dir(module):
|
||||
if name.startswith("__") or name in skips:
|
||||
continue
|
||||
|
||||
obj = getattr(module, name)
|
||||
|
||||
# skip submodules
|
||||
if inspect.ismodule(obj):
|
||||
continue
|
||||
|
||||
objects.append((name, obj))
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def add_mobase_header(writer: Writer):
|
||||
writer.print_imports(
|
||||
[
|
||||
"abc",
|
||||
("enum", ["Enum"]),
|
||||
("pathlib", ["Path"]),
|
||||
(
|
||||
"typing",
|
||||
[
|
||||
"Dict",
|
||||
"Iterator",
|
||||
"List",
|
||||
"Tuple",
|
||||
"Union",
|
||||
"Any",
|
||||
"Optional",
|
||||
"Callable",
|
||||
"overload",
|
||||
"Sequence",
|
||||
"Set",
|
||||
"TypeVar",
|
||||
"Type",
|
||||
],
|
||||
),
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
# Needs to define the MVariant and GameFeatureType type:
|
||||
writer._print(f"MoVariant = {PyType.MO_VARIANT}")
|
||||
writer._print(f"FileWrapper = {PyType.FILE_WRAPPER}")
|
||||
writer._print(f"DirectoryWrapper = {PyType.DIRECTORY_WRAPPER}")
|
||||
writer._print('GameFeatureType = TypeVar("GameFeatureType")')
|
||||
writer._print()
|
||||
|
||||
|
||||
def add_mobase_widgets_header(writer: Writer):
|
||||
writer.print_imports(
|
||||
[
|
||||
(
|
||||
"typing",
|
||||
["List", "Tuple", "Union", "overload"],
|
||||
),
|
||||
"PyQt6.QtCore",
|
||||
"PyQt6.QtGui",
|
||||
"PyQt6.QtWidgets",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser("stubs generator for the MO2 python interface")
|
||||
parser.add_argument(
|
||||
"install_dir",
|
||||
metavar="INSTALL_DIR",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="installation directory of Mod Organizer 2",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path("stubs/setup/mobase-stubs"),
|
||||
help="output folder (default stubs/setup/mobase-stubs)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="verbose mode (all logs go to stderr)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig()
|
||||
LOGGER.setLevel(logging.WARNING)
|
||||
|
||||
if args.verbose:
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
output_path: Path = args.output
|
||||
config_path: Path | None = args.config
|
||||
|
||||
# create the register
|
||||
register = MobaseRegister()
|
||||
|
||||
# load mobase (cannot simply do "import mobase")
|
||||
mobase = load_mobase(Path(args.install_dir))
|
||||
|
||||
# headers
|
||||
module_headers: dict[str, Callable[[Writer], None]] = {
|
||||
"mobase": add_mobase_header,
|
||||
"mobase.widgets": add_mobase_widgets_header,
|
||||
}
|
||||
|
||||
# list of objects directly in mobase
|
||||
module_objects: dict[str, list[tuple[str, object]]] = {
|
||||
"mobase": extract_objects(
|
||||
mobase,
|
||||
[
|
||||
# we do not want the real MoVariant
|
||||
"MoVariant",
|
||||
# the "real" IPlugin is IPluginBase
|
||||
"IPlugin",
|
||||
],
|
||||
),
|
||||
"mobase.widgets": extract_objects(mobase.widgets),
|
||||
}
|
||||
|
||||
for name, objects in module_objects.items():
|
||||
|
||||
# load settings from the configuration
|
||||
settings: Settings = Settings(register)
|
||||
if config_path is not None:
|
||||
with open(config_path, "r") as fp:
|
||||
settings = Settings(register, fp, module=name)
|
||||
|
||||
for n, o in objects:
|
||||
register.add_object(n, o)
|
||||
|
||||
# enum first, and then alphabetical, should be fine with the __future__ import
|
||||
objects = sorted(
|
||||
objects, key=lambda e: (isinstance(e[1], type), not is_enum(e[1]), e[0])
|
||||
)
|
||||
|
||||
# Process everything:
|
||||
for n, o in objects:
|
||||
|
||||
# Create the corresponding object:
|
||||
c = register.make_object(n, o)
|
||||
|
||||
if isinstance(c, Class):
|
||||
|
||||
# Clean the class (e.g., remove duplicates methods due to wrappers):
|
||||
clean_class(c)
|
||||
|
||||
# Path the class using the configuration:
|
||||
settings.patch_class(c)
|
||||
|
||||
elif isinstance(c, list) and isinstance(c[0], Function):
|
||||
settings.patch_functions(c)
|
||||
|
||||
else:
|
||||
LOGGER.critical(
|
||||
"Cannot generated stubs for {}, unsupported object type.".format(n)
|
||||
)
|
||||
|
||||
output_folder = output_path
|
||||
if name != "mobase":
|
||||
output_folder = output_path.joinpath(
|
||||
name.replace("mobase.", "").replace(".", "/")
|
||||
)
|
||||
|
||||
# create directory if required
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# write everything
|
||||
with open(output_folder.joinpath("__init__.pyi"), "w") as output:
|
||||
|
||||
writer = Writer(package=name, output=output, settings=settings)
|
||||
|
||||
# the __future__ import must be at the beginning
|
||||
writer.print_imports([("__future__", ["annotations"])])
|
||||
writer.print_version(settings.version)
|
||||
|
||||
module_headers[name](writer)
|
||||
|
||||
for n, o in objects:
|
||||
|
||||
# Get the corresponding object:
|
||||
c = register.get_object(n)
|
||||
|
||||
if isinstance(c, Class):
|
||||
writer.print_class(c)
|
||||
|
||||
elif isinstance(c, list) and isinstance(c[0], Function):
|
||||
for fn in c:
|
||||
writer.print_function(fn)
|
||||
|
||||
black.format_file_in_place(
|
||||
output_folder.joinpath("__init__.pyi"),
|
||||
fast=False,
|
||||
mode=black.Mode(is_pyi=True),
|
||||
write_back=black.WriteBack.YES,
|
||||
)
|
||||
isort.api.sort_file(output_folder.joinpath("__init__.pyi"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_mobase(path: Path):
|
||||
"""
|
||||
Load the mobase from the given MO2 installation path and
|
||||
returns it.
|
||||
|
||||
Args:
|
||||
path: Path to the MO2 installation (folder containing the ModOrganizer.exe).
|
||||
|
||||
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))
|
||||
os.add_dll_directory(str(path.joinpath("dlls")))
|
||||
|
||||
# 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 # type: ignore
|
||||
|
||||
return mobase
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
mobase = load_mobase(args.install_dir)
|
||||
@@ -0,0 +1,432 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
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):
|
||||
# import only here since we change the path to find them
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
if isinstance(name, type):
|
||||
name = name.__name__
|
||||
|
||||
self.name = name.strip()
|
||||
|
||||
# replace QFlags[xxx] with xxx
|
||||
self.name = re.sub(r"QFlags\[([^]]*)\]", r"\1", self.name)
|
||||
|
||||
# find PyQt types
|
||||
for m in (QtCore, QtGui, QtWidgets):
|
||||
if self.name in dir(m):
|
||||
self.name = "{}.{}".format(m.__name__, self.name)
|
||||
|
||||
def typing(self) -> str:
|
||||
"""
|
||||
Returns:
|
||||
A valid typing representation for this type.
|
||||
"""
|
||||
# IPluginBase -> IPlugin
|
||||
if self.name == "mobase.IPluginBase":
|
||||
return "IPlugin"
|
||||
|
||||
return self.name
|
||||
|
||||
def is_none(self) -> bool:
|
||||
"""
|
||||
Check if this type represent None.
|
||||
|
||||
Returns:
|
||||
True if this type represents None.
|
||||
"""
|
||||
return self.name.lower() in ("none", "nonetype")
|
||||
|
||||
def is_object(self) -> bool:
|
||||
"""
|
||||
Check if this type represent the generic "object" type.
|
||||
|
||||
Returns:
|
||||
True if this type represent the generic object type.
|
||||
"""
|
||||
return self.name.lower() == "object"
|
||||
|
||||
def is_any(self) -> bool:
|
||||
"""
|
||||
Check if this type represent the typing "Any".
|
||||
|
||||
Returns:
|
||||
True if this type represent the typing "Any".
|
||||
"""
|
||||
return self.name == "Any"
|
||||
|
||||
def __str__(self):
|
||||
return "Type({})".format(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, PyType):
|
||||
return NotImplemented
|
||||
return self.name == other.name
|
||||
|
||||
|
||||
class Return:
|
||||
"""
|
||||
Class representing the return value of a function (type and documentation).
|
||||
"""
|
||||
|
||||
type: PyType
|
||||
doc: str
|
||||
|
||||
def __init__(self, type: PyType, doc: str = ""):
|
||||
self.type = type
|
||||
self.doc = doc
|
||||
|
||||
|
||||
class Argument:
|
||||
"""
|
||||
Class representing a function argument (type and eventual default value).
|
||||
"""
|
||||
|
||||
# Constant representing None since None indicates no default value:
|
||||
DEFAULT_NONE = "None"
|
||||
|
||||
name: str
|
||||
type: PyType
|
||||
_value: str | None
|
||||
doc: str
|
||||
|
||||
def __init__(
|
||||
self, name: str, type: PyType, value: str | None = None, doc: str = ""
|
||||
):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self._value = value
|
||||
self.doc = doc
|
||||
|
||||
@property
|
||||
def value(self) -> str | None:
|
||||
|
||||
value = self._value
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# pybind11 puts enum in <> so we need to fix
|
||||
m = re.match(r"<([^:]+):\s*[0-9]+>", value)
|
||||
if m:
|
||||
# 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
|
||||
|
||||
def has_default_value(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
def __str__(self):
|
||||
if self.has_default_value():
|
||||
return "Arg({}={})".format(self.type, self.value)
|
||||
return "Arg({})".format(self.type)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.type)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Argument):
|
||||
return NotImplemented
|
||||
return self.type == other.type
|
||||
|
||||
|
||||
class Exception:
|
||||
|
||||
"""
|
||||
Small class representing exception that can be raised from functions.
|
||||
"""
|
||||
|
||||
type: PyType
|
||||
doc: str
|
||||
|
||||
def __init__(self, type: PyType, doc: str = ""):
|
||||
self.type = type
|
||||
self.doc = doc
|
||||
|
||||
|
||||
class Function:
|
||||
"""
|
||||
Class representing a function.
|
||||
"""
|
||||
|
||||
name: str
|
||||
ret: Return
|
||||
args: list[Argument]
|
||||
overloads: bool
|
||||
raises: list[Exception]
|
||||
doc: str
|
||||
deprecated: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
ret: Return,
|
||||
args: list[Argument],
|
||||
has_overloads: bool = False,
|
||||
doc: str = "",
|
||||
):
|
||||
self.name = name
|
||||
self.ret = ret
|
||||
self.args = args
|
||||
self.overloads = has_overloads
|
||||
self.raises = []
|
||||
self.doc = ""
|
||||
self.deprecated = False
|
||||
|
||||
def has_overloads(self):
|
||||
return self.overloads
|
||||
|
||||
def is_deprecated(self):
|
||||
return self.deprecated
|
||||
|
||||
|
||||
class Method(Function):
|
||||
"""
|
||||
Class representing a method.
|
||||
"""
|
||||
|
||||
cls: Class
|
||||
abstract: str | bool
|
||||
static: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
ret: Return,
|
||||
args: list[Argument],
|
||||
static: bool,
|
||||
has_overloads: bool = False,
|
||||
doc: str = "",
|
||||
):
|
||||
super().__init__(name, ret, args, has_overloads, doc)
|
||||
self.static = static
|
||||
self.abstract = "auto"
|
||||
|
||||
def is_abstract(self):
|
||||
if self.name.startswith("__"):
|
||||
return False
|
||||
if self.abstract == "auto":
|
||||
return self.cls.is_abstract()
|
||||
return self.abstract
|
||||
|
||||
def is_static(self):
|
||||
return self.static
|
||||
|
||||
def is_special(self):
|
||||
return self.name.startswith("__")
|
||||
|
||||
def is_constructor(self):
|
||||
return self.name == "__init__"
|
||||
|
||||
|
||||
class Constant:
|
||||
"""
|
||||
Class representing a constant.
|
||||
"""
|
||||
|
||||
name: str
|
||||
type: PyType | None
|
||||
value: object
|
||||
doc: str | None
|
||||
|
||||
def __init__(
|
||||
self, name: str, type: PyType | None, value: object, doc: str | None = None
|
||||
):
|
||||
self.name = name
|
||||
self.type = type
|
||||
|
||||
# Note: The value is not used actually since we can hide it using `...`.
|
||||
self.value = value
|
||||
self.doc = doc
|
||||
|
||||
|
||||
class Property:
|
||||
"""
|
||||
Class representing a property.
|
||||
"""
|
||||
|
||||
name: str
|
||||
type: PyType
|
||||
doc: str
|
||||
read_only: bool
|
||||
|
||||
def __init__(self, name: str, type: PyType, read_only: bool, doc: str = ""):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.read_only = read_only
|
||||
self.doc = doc
|
||||
|
||||
def is_read_only(self):
|
||||
return self.read_only
|
||||
|
||||
|
||||
class Class:
|
||||
"""
|
||||
Class representing a class.
|
||||
"""
|
||||
|
||||
name: str
|
||||
bases: list[Class]
|
||||
methods: list[Method]
|
||||
constants: list[Constant]
|
||||
properties: list[Property]
|
||||
inner_classes: list[Class]
|
||||
outer_class: Class | None
|
||||
doc: str
|
||||
abstract: bool
|
||||
deprecated: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package: str,
|
||||
name: str,
|
||||
bases: list[Class],
|
||||
methods: list[Method],
|
||||
constants: list[Constant] = [],
|
||||
properties: list[Property] = [],
|
||||
inner_classes: list[Class] = [],
|
||||
doc: str = "",
|
||||
):
|
||||
self.package = package
|
||||
self.name = name
|
||||
self.bases = bases
|
||||
self.methods = methods
|
||||
self.properties = properties
|
||||
self.constants = constants
|
||||
self.inner_classes = inner_classes
|
||||
self.doc = ""
|
||||
self.abstract = False
|
||||
self.outer_class = None
|
||||
self.deprecated = False
|
||||
|
||||
# Update class in method:
|
||||
for m in self.methods:
|
||||
m.cls = self
|
||||
for ic in self.inner_classes:
|
||||
ic.outer_class = self
|
||||
|
||||
def is_abstract(self):
|
||||
"""
|
||||
Returns:
|
||||
True if this class is abstract, False otherwise.
|
||||
"""
|
||||
return self.abstract or any(bc.is_abstract() for bc in self.bases)
|
||||
|
||||
@property
|
||||
def canonical_name(self):
|
||||
"""
|
||||
Returns:
|
||||
The canonical name of this class.
|
||||
"""
|
||||
name = self.name
|
||||
oc = self.outer_class
|
||||
while oc is not None:
|
||||
name = "{}.{}".format(oc.name, name)
|
||||
oc = oc.outer_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]:
|
||||
"""
|
||||
Returns:
|
||||
All the bases of this class, including bases of bases and so on.
|
||||
"""
|
||||
bases = set(self.bases)
|
||||
for b in self.bases:
|
||||
bases = bases.union(b.all_bases)
|
||||
return bases
|
||||
|
||||
def is_deprecated(self):
|
||||
return self.deprecated
|
||||
|
||||
|
||||
class PyClass(Class):
|
||||
|
||||
"""
|
||||
Class use to wrap Python class to be used as parent class for some classes
|
||||
in mobase.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package: str,
|
||||
name: str,
|
||||
):
|
||||
super().__init__(package, name, [], [])
|
||||
self.abstract = False
|
||||
|
||||
|
||||
class Enum(Class):
|
||||
|
||||
"""
|
||||
Class representing an enum.
|
||||
"""
|
||||
|
||||
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")],
|
||||
methods,
|
||||
inner_classes=[],
|
||||
constants=[Constant(k, None, v) for k, v in values.items()],
|
||||
)
|
||||
|
||||
def is_abstract(self):
|
||||
return False
|
||||
@@ -0,0 +1,379 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import types
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import chain
|
||||
from typing import Iterable, cast
|
||||
|
||||
from . import LOGGER
|
||||
from .mtypes import (
|
||||
Argument,
|
||||
Class,
|
||||
Constant,
|
||||
Enum,
|
||||
Function,
|
||||
Method,
|
||||
Property,
|
||||
PyClass,
|
||||
PyType,
|
||||
Return,
|
||||
)
|
||||
from .register import MobaseRegister
|
||||
|
||||
|
||||
def magic_split(value: str, sep=",", open="(<[", close=")>]"):
|
||||
"""
|
||||
Split the value according to the given separator, but keeps together elements
|
||||
within the given separator. Useful to split C++ signature function since type names
|
||||
can contain special characters...
|
||||
|
||||
Examples:
|
||||
- magic_split("a,b,c", sep=",") -> ["a", "b", "c"]
|
||||
- magic_split("a<b,c>,d(e,<k,c>),p) -> ["a<b,c>", "d(e,<k,c>)", "p"]
|
||||
|
||||
Args:
|
||||
value: String to split.
|
||||
sep: Separator to use.
|
||||
open: List of opening characters.
|
||||
close: List of closing characters. Order must match open.
|
||||
|
||||
Returns: The list of split parts from value.
|
||||
"""
|
||||
i, j = 0, 0
|
||||
s: list[str] = []
|
||||
r = []
|
||||
while i < len(value):
|
||||
j = i + 1
|
||||
while j < len(value):
|
||||
c = value[j]
|
||||
|
||||
# Separator found and the stack is empty:
|
||||
if c == sep and not s:
|
||||
break
|
||||
|
||||
# Check close/open:
|
||||
if c in open:
|
||||
s.append(open.index(c))
|
||||
elif c in close:
|
||||
# The stack might be empty if the separator is also an opening element:
|
||||
if not s and sep in open and j + 1 == len(value):
|
||||
pass
|
||||
else:
|
||||
t = s.pop()
|
||||
if t != close.index(c):
|
||||
raise ValueError(
|
||||
"Found closing element {} for opening element {}.".format(
|
||||
c, open[t]
|
||||
)
|
||||
)
|
||||
j += 1
|
||||
r.append(value[i:j])
|
||||
i = j + 1
|
||||
|
||||
assert not s
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def parse_python_signature(s: str, name: str) -> tuple[PyType, list[Argument]]:
|
||||
"""
|
||||
Parse a pybind11 python signature.
|
||||
|
||||
Args:
|
||||
s: The signature to parse.
|
||||
name: Name of the function.
|
||||
|
||||
Returns: (RType, Args) where RType is a Type object, and Args is a list of Arg
|
||||
objects containing Type.
|
||||
"""
|
||||
|
||||
m = re.search(rf"{name}\((.*)\)\s*->\s*([^:]+)\s*", s)
|
||||
if not m:
|
||||
raise ValueError(f"invalid signature: {s}")
|
||||
|
||||
args = magic_split(m.group(1).strip(), ",", open="[", close="]")
|
||||
return_type = m.group(2)
|
||||
|
||||
arguments = []
|
||||
for i, pa in enumerate(args):
|
||||
m = re.search(
|
||||
r"(?P<name>[^:]+)\s*:\s*(?P<type>[^=]+)\s*(=\s*(?P<value>[^,]+))?",
|
||||
pa.strip(),
|
||||
)
|
||||
if not m:
|
||||
raise ValueError(f"invalid argument: {pa}, {s}")
|
||||
|
||||
matches = m.groupdict()
|
||||
arguments.append(
|
||||
Argument(matches["name"], PyType(matches["type"]), matches["value"])
|
||||
)
|
||||
|
||||
return PyType(return_type), arguments
|
||||
|
||||
|
||||
def is_enum(e: type) -> bool:
|
||||
"""Check if the given class is an enumeration.
|
||||
|
||||
Args:
|
||||
e: The class object to check.
|
||||
|
||||
Returns: True if the object is an enumeration (boost::python enumeration, not
|
||||
python) False otherwise.
|
||||
"""
|
||||
# Yet to find a better way...
|
||||
if not isinstance(e, type):
|
||||
return False
|
||||
return hasattr(e, "__entries")
|
||||
|
||||
|
||||
class Overload:
|
||||
|
||||
"""Small class to avoid mypy issues..."""
|
||||
|
||||
return_type: PyType
|
||||
arguments: list[Argument]
|
||||
|
||||
def __init__(self, return_type: PyType, arguments: list[Argument]):
|
||||
self.return_type = return_type
|
||||
self.arguments = arguments
|
||||
|
||||
|
||||
def parse_pybind11_function_docstring(e) -> list[Overload]:
|
||||
"""
|
||||
Parse the docstring of the given element.
|
||||
|
||||
Args:
|
||||
e: The function to "parse".
|
||||
|
||||
Returns:
|
||||
A list of overloads for the given function.
|
||||
"""
|
||||
lines = e.__doc__.strip().split("\n")
|
||||
|
||||
signatures: list[str]
|
||||
if len(lines) == 1:
|
||||
signatures = lines
|
||||
else:
|
||||
signatures = []
|
||||
for line in lines:
|
||||
m = re.match(rf"^[0-9]+[.]\s+({e.__name__}.*)$", line)
|
||||
if m:
|
||||
signatures.append(m.group(1).strip())
|
||||
|
||||
# We are going to parse the python and C++ signature, and try to merge
|
||||
# them...
|
||||
overloads: list[Overload] = []
|
||||
for signature in signatures:
|
||||
|
||||
# fix MOBase:: in some places to get proper Python types
|
||||
signature = signature.replace("MOBase::", "mobase.").replace("::", ".")
|
||||
|
||||
try:
|
||||
return_type, arguments = parse_python_signature(signature, e.__name__)
|
||||
except ValueError:
|
||||
raise ValueError(f"invalid signature: {e.__name__}, {e.__doc__}")
|
||||
overloads.append(Overload(return_type=return_type, arguments=arguments))
|
||||
|
||||
return overloads
|
||||
|
||||
|
||||
def make_functions(e) -> list[Function]:
|
||||
overloads = parse_pybind11_function_docstring(e)
|
||||
|
||||
return [
|
||||
Function(
|
||||
e.__name__,
|
||||
Return(overload.return_type),
|
||||
overload.arguments,
|
||||
has_overloads=len(overloads) > 1,
|
||||
)
|
||||
for overload in overloads
|
||||
]
|
||||
|
||||
|
||||
def make_class(e: type, register: MobaseRegister) -> Class:
|
||||
"""
|
||||
Constructs a Class object from the given python class.
|
||||
|
||||
Args:
|
||||
e: The python class (created from boost) to construct an object for.
|
||||
class_register:
|
||||
|
||||
Returns: A Class object corresponding to the given class.
|
||||
"""
|
||||
|
||||
base_classes_s: list[str] = []
|
||||
|
||||
# Kind of ugly, but...:
|
||||
for c in inspect.getmro(e):
|
||||
if c != e and c.__module__ == "mobase":
|
||||
base_classes_s.append(c.__name__)
|
||||
if c.__module__ == "pybind11_builtins":
|
||||
break
|
||||
|
||||
first_base = inspect.getmro(e)[1]
|
||||
|
||||
# This contains ALL the parent classes, not the direct ones:
|
||||
base_classes: list[Class] = [
|
||||
register.make_object(name) for name in base_classes_s # type: ignore
|
||||
]
|
||||
|
||||
# retrieve all the attributes that are not in a base class
|
||||
all_attrs = [
|
||||
(n, getattr(e, n))
|
||||
for n in dir(e)
|
||||
if not hasattr(first_base, n) or getattr(first_base, n) is not getattr(e, n)
|
||||
]
|
||||
|
||||
# members to exclude
|
||||
EXCLUDED_MEMBERS = [
|
||||
"__init_subclass__",
|
||||
"__module__",
|
||||
"__subclasshook__",
|
||||
"__hash__",
|
||||
"__getstate__",
|
||||
"__setstate__",
|
||||
"__index__",
|
||||
"__repr__",
|
||||
]
|
||||
all_attrs = [a for a in all_attrs if a[0] not in EXCLUDED_MEMBERS]
|
||||
|
||||
# fetch all attributes from the base classes
|
||||
base_attrs: dict[str, list[Constant | Property | Method | Class]] = defaultdict(
|
||||
list
|
||||
)
|
||||
for bc in base_classes:
|
||||
for a in cast(
|
||||
Iterable[Constant | Property | Method | Class],
|
||||
chain(bc.constants, bc.methods, bc.properties, bc.inner_classes),
|
||||
):
|
||||
base_attrs[a.name].append(a)
|
||||
|
||||
# retrieve the enumerations and classes
|
||||
inner_classes = [ic[1] for ic in all_attrs if isinstance(ic[1], type)]
|
||||
|
||||
pinner_classes: list[Class] = [
|
||||
cast(Class, register.make_object(f"{e.__qualname__}.{ic.__name__}", ic))
|
||||
for ic in inner_classes
|
||||
]
|
||||
|
||||
# find the methods
|
||||
raw_methods = [
|
||||
m[1] for m in all_attrs if callable(m[1]) and m[1] not in inner_classes
|
||||
]
|
||||
raw_methods = sorted(raw_methods, key=lambda m: str(m.__name__))
|
||||
raw_methods = [m for m in raw_methods if m.__doc__ is not None]
|
||||
|
||||
# remove __init__
|
||||
raw_methods = [
|
||||
m for m in raw_methods if not isinstance(m, types.WrapperDescriptorType)
|
||||
]
|
||||
|
||||
methods: list[Method] = []
|
||||
for method in raw_methods:
|
||||
if method.__doc__ is None:
|
||||
continue
|
||||
|
||||
# __eq__ must accept an object in python (and it does with pybind11), so we
|
||||
# force the overload
|
||||
if method.__name__ in ["__eq__", "__ne__"]:
|
||||
overloads = [
|
||||
Overload(
|
||||
return_type=PyType("bool"),
|
||||
arguments=[
|
||||
Argument("self", PyType(e.__module__ + "." + e.__qualname__)),
|
||||
Argument("other", PyType("object")),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
# otherwise we parse the docstring
|
||||
else:
|
||||
overloads = parse_pybind11_function_docstring(method)
|
||||
|
||||
for overload in overloads:
|
||||
args = overload.arguments
|
||||
|
||||
# pybind11 seems to be consistent with the naming of "self", so we can
|
||||
# mostly rely on it
|
||||
static = len(args) == 0 or args[0].name != "self"
|
||||
|
||||
# we need to fix some default values (basically default values that
|
||||
# comes from inner enum) and argument
|
||||
for arg in overload.arguments:
|
||||
if arg.has_default_value():
|
||||
value: str = arg.value # type: ignore
|
||||
base_name = value.split(".")[0]
|
||||
|
||||
for base_class in base_classes:
|
||||
for biclass in base_class.inner_classes:
|
||||
if isinstance(biclass, Enum) and biclass.name == base_name:
|
||||
arg._value = base_class.name + "." + value
|
||||
|
||||
methods.append(
|
||||
Method(
|
||||
method.__name__,
|
||||
Return(overload.return_type),
|
||||
overload.arguments,
|
||||
static=static,
|
||||
has_overloads=len(overloads) > 1,
|
||||
)
|
||||
)
|
||||
|
||||
# Retrieve the attributes:
|
||||
constants = []
|
||||
properties = []
|
||||
for name, attr in all_attrs:
|
||||
if callable(attr) or isinstance(attr, type):
|
||||
continue
|
||||
|
||||
# Maybe we should check an override here (e.g., different value for a constant):
|
||||
if name in base_attrs:
|
||||
continue
|
||||
|
||||
if isinstance(attr, property):
|
||||
properties.append(Property(name, PyType("Any"), attr.fset is None))
|
||||
elif not hasattr(attr, "__name__"):
|
||||
constants.append(Constant(name, PyType(type(attr).__name__), attr))
|
||||
|
||||
direct_bases: list[Class] = []
|
||||
for c in e.__bases__:
|
||||
if c.__module__ != "pybind11_builtins":
|
||||
direct_bases.append(register.get_object(c.__name__))
|
||||
|
||||
# Forcing QWidget base for XWidget classes since these do not show up
|
||||
# and we use a trick:
|
||||
if e.__name__.endswith("Widget"):
|
||||
LOGGER.info(
|
||||
"Forcing base {} for class {}.".format(
|
||||
"PyQt6.QtWidgets.QWidget", e.__name__
|
||||
)
|
||||
)
|
||||
direct_bases.append(PyClass("PyQt6.QtWidgets", "QWidget"))
|
||||
|
||||
# check if it an enum
|
||||
if is_enum(e):
|
||||
# all pybind11 enums have a .__entries attribute
|
||||
values = e.__entries # type: ignore
|
||||
|
||||
# drop the __init__
|
||||
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,
|
||||
inner_classes=pinner_classes,
|
||||
properties=properties,
|
||||
constants=constants,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user