Compare commits

..
Author SHA1 Message Date
Mikaël Capelle ee3faa0c87 Update for pybind11. 2022-05-05 12:59:28 +02:00
Mikaël Capelle c5ee7f8a43 Pypi-publish on tag. 2022-04-22 13:17:01 +02:00
Mikaël Capelle 33b7222b13 Fix enabledByDefault(). 2022-04-22 12:19:47 +02:00
Mikaël Capelle e083e0832e Linting. 2022-04-21 22:03:21 +02:00
Mikaël Capelle 478af17c58 Fix PyQt5 -> PyQt6. 2022-04-21 21:40:18 +02:00
13 changed files with 1398 additions and 5928 deletions
+4 -3
View File
@@ -4,8 +4,8 @@
name: Upload Python Package
on:
release:
types: [published]
push:
tags: ["*.dev[0-9]+"]
jobs:
deploy:
@@ -18,7 +18,7 @@ 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
@@ -36,5 +36,6 @@ jobs:
run: |
cd stubs/setup
cp ../${{ steps.version.outputs.replaced }}/mobase.pyi mobase-stubs/__init__.pyi
sed -i 's/__version__ = ".*"/__version__ = "${{ github.ref_name }}"/' mobase-stubs/__init__.pyi
python setup.py sdist bdist_wheel
twine upload dist/*
+19 -23
View File
@@ -16,8 +16,8 @@ ignores:
# related to functions / classes, including their documentation.
mobase:
# Version of the stubs.
__version__: 2.5.0.dev0
# Version of the stubs - this is overridden by when publishing.
__version__: "2.5.0"
getFileVersion:
__doc__: Retrieve the file version of the given executable.
@@ -329,7 +329,7 @@ mobase:
__doc__: |
The parent tree containing this entry, or a `None` if this entry is the root
or the parent tree is unreachable.
type: Optional["IFileTree"]
type: Optional[IFileTree]
path:
__doc__: |
@@ -673,7 +673,7 @@ mobase:
__doc__: |
The entry at the given location, or `None` if the entry was not found or
was not of the correct type.
type: Optional[Union["IFileTree", "FileTreeEntry"]]
type: Optional[Union[IFileTree, FileTreeEntry]]
insert:
__doc__: |
@@ -1208,7 +1208,7 @@ mobase:
IModRepositoryBridge:
__bases__:
- PyQt5.QtCore.QObject
- PyQt6.QtCore.QObject
signals[]:
descriptionAvailable:
@@ -1372,7 +1372,7 @@ mobase:
type: MoVariant
desc: The data that was included in the request.
error:
type: PyQt5.QtNetwork.QNetworkReply.NetworkError
type: PyQt6.QtNetwork.QNetworkReply.NetworkError
desc: The actual error.
message:
type: str
@@ -1814,6 +1814,7 @@ mobase:
returns: True if the plugin was initialized correctly, False otherwise.
enabledByDefault:
abstract: false
__doc__: Check whether this plugin should be enabled by default.
returns: True if this plugin should be enabled by default, False otherwise.
@@ -2033,6 +2034,9 @@ mobase:
The name of the launcher executable to run (relative to the game folder), or an
empty string if there is no launcher.
getSupportURL:
returns: An URL for the support page of this game.
iniFiles:
returns: |
The list of INI files this game uses. The first file in the list should be the
@@ -2222,13 +2226,6 @@ mobase:
args:
parent: The parent widget.
IPluginInstallerCustom:
__doc__: |
Custom installer for mods. Custom installers receive the archive name and have to go
from there. They have to be able to extract the archive themselves.
Example of such installers are the external NCC installer or the OMOD installer.
_manager:
abstract: false
returns: The installation manager.
@@ -2237,6 +2234,13 @@ mobase:
abstract: false
returns: The parent widget.
IPluginInstallerCustom:
__doc__: |
Custom installer for mods. Custom installers receive the archive name and have to go
from there. They have to be able to extract the archive themselves.
Example of such installers are the external NCC installer or the OMOD installer.
install:
__doc__: |
Install the given archive.
@@ -2277,14 +2281,6 @@ mobase:
representing the archive and can modify what to install and where by editing this structure.
Actually extracting the archive is handled by the manager.
_manager:
abstract: false
returns: The installation manager.
_parentWidget:
abstract: false
returns: The parent widget.
install:
__doc__: |
Install a mod from an archive filetree.
@@ -2919,7 +2915,7 @@ mobase:
type: int
desc:
fileTime:
type: PyQt5.QtCore.QDateTime
type: PyQt6.QtCore.QDateTime
desc:
gameName:
type: str
@@ -3012,7 +3008,7 @@ mobase:
parent: The parent widget.
returns:
__doc__: A SaveGameInfoWidget to display information about save game.
type: Optional["ISaveGameInfoWidget"]
type: Optional[ISaveGameInfoWidget]
ScriptExtender:
__doc__:
+1 -4
View File
@@ -1,8 +1,5 @@
# -*- 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)
LOGGER = logging.getLogger(__name__)
+8 -44
View File
@@ -1,40 +1,17 @@
# -*- 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):
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).
moprivate: If True, the moprivate module will also be loaded and returned
alongside mobase.
Returns: The mobase module.
"""
@@ -50,22 +27,15 @@ def load_mobase(path: Path, moprivate: bool = False):
[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]
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 PyQt5
sys.path.insert(1, path.joinpath("plugins", "data").as_posix())
# We need to add plugins/data to sys.path, mainly for PyQt6
sys.path.insert(1, path.joinpath("plugins", "plugin_python", "libs").as_posix())
mobase = load_module("mobase", path.joinpath("plugins", "data", "pythonrunner.dll"))
import mobase
if not moprivate:
return mobase
moprivate = load_module(
"moprivate", path.joinpath("plugins", "data", "pythonrunner.dll")
)
return mobase, moprivate
return mobase
if __name__ == "__main__":
@@ -82,13 +52,7 @@ if __name__ == "__main__":
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)
mobase = load_mobase(args.install_dir)
+72 -367
View File
File diff suppressed because it is too large Load Diff
+179 -387
View File
File diff suppressed because it is too large Load Diff
+9 -53
View File
@@ -1,10 +1,10 @@
# -*- encoding: utf-8 -*-
from collections import OrderedDict
from typing import Dict, List, Optional, Union
from __future__ import annotations
from . import logger
from .mtypes import Class, CType, Function, Type
from collections import OrderedDict
from .mtypes import Class, Function
class MobaseRegister:
@@ -12,21 +12,16 @@ class MobaseRegister:
Class that register classes.
"""
objects: Dict[str, Union[Class, List[Function]]]
objects: dict[str, Class | list[Function]]
def __init__(self):
self.raw_objects: Dict[str, Union[type]] = OrderedDict()
self.raw_objects: dict[str, 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"]]:
def make_object(self, name: str, e: type | None = None) -> Class | list[Function]:
"""
Construct a Function, Class or Enum for the given object.
@@ -37,7 +32,7 @@ class MobaseRegister:
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
from .parser import make_class, make_functions
if e is None:
e = self.raw_objects[name]
@@ -46,9 +41,7 @@ class MobaseRegister:
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):
if isinstance(e, type):
self.objects[name] = make_class(name, e, self)
elif callable(e):
self.objects[name] = make_functions(name, e)
@@ -68,42 +61,5 @@ class MobaseRegister:
"""
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()
+259 -190
View File
File diff suppressed because it is too large Load Diff
+29 -33
View File
@@ -1,8 +1,8 @@
# -*- encoding: utf-8 -*-
from typing import List, TextIO, Tuple, Union
from typing import TextIO
from . import logger
from . import LOGGER
from .mtypes import Class, Enum, Function, Method, Property
from .utils import Settings
@@ -36,7 +36,7 @@ class Writer:
self._print('__version__ = "{}"'.format(version))
self._print()
def print_imports(self, imports: List[Union[str, Tuple[str, List[str]]]]):
def print_imports(self, imports: list[str | tuple[str, list[str]]]):
"""
Print the given imports.
"""
@@ -55,12 +55,10 @@ class Writer:
if fn.has_overloads():
self._print("{}@overload".format(indent))
srtype = ""
sig_return_type = ""
if not fn.ret.type.is_none():
srtype = " -> " + fn.ret.type.typing(self._settings)
sig_return_type = " -> " + fn.ret.type.typing()
fargs = fn.args
largs: List[str] = []
if isinstance(fn, Method):
if fn.is_static():
self._print("{}@staticmethod".format(indent))
@@ -68,17 +66,19 @@ class Writer:
if fn.is_abstract():
self._print("{}@abc.abstractmethod".format(indent))
largs.insert(0, "self")
fargs = fargs[1:]
for i, arg in enumerate(fargs):
tmp = "{}: {}".format(arg.name, arg.type.typing(self._settings))
python_args: list[str] = []
for i, arg in enumerate(fn.args):
tmp = "{}: {}".format(arg.name, arg.type.typing())
if arg.has_default_value():
tmp += " = {}".format(arg.value)
largs.append(tmp)
sargs = ", ".join(largs)
python_args.append(tmp)
self._print("{}def {}({}){}:".format(indent, fn.name, sargs, srtype), end="")
self._print(
"{}def {}({}){}:".format(
indent, fn.name, ", ".join(python_args), sig_return_type
),
end="",
)
# Add the documentation, if any:
doc = ""
@@ -93,9 +93,12 @@ class Writer:
if any(arg.doc for arg in args):
doc += "\nArgs:\n"
for arg in args:
adocl = arg.doc.strip().split("\n")
adoc = "\n".join([adocl[0]] + [" " + ldoc for ldoc in adocl[1:]])
doc += " " + arg.name + ": " + adoc + "\n"
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"
if not fn.ret.type.is_none() and fn.ret.doc:
doc += "\nReturns:\n " + fn.ret.doc.strip() + "\n"
@@ -103,13 +106,7 @@ class Writer:
if fn.raises:
doc += "\nRaises:\n"
for rai in fn.raises:
doc += (
" "
+ rai.type.typing(self._settings)
+ ": "
+ rai.doc.strip()
+ "\n"
)
doc += " " + rai.type.typing() + ": " + rai.doc.strip() + "\n"
if doc:
self._print()
@@ -127,7 +124,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
)
@@ -135,15 +132,13 @@ class Writer:
self._print("{}@property".format(indent))
self._print(
"{}def {}(self) -> {}: ...".format(
indent, prop.name, prop.type.typing(self._settings)
)
"{}def {}(self) -> {}: ...".format(indent, prop.name, 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(self._settings)
indent, prop.name, prop.type.typing()
)
)
self._print()
@@ -179,8 +174,8 @@ class Writer:
self._print()
# Inner classes:
for iclass in cls.inner_classes:
self.print_class(iclass, indent=indent + " ")
for inner_class in cls.inner_classes:
self.print_class(inner_class, indent=indent + " ")
self._print()
# Constants:
@@ -191,7 +186,7 @@ class Writer:
typing = ""
if constant.type is not None:
typing = ": {}".format(constant.type.typing(self._settings))
typing = ": {}".format(constant.type.typing())
# Note: We do not print the value, we use ...
self._print(
@@ -217,6 +212,7 @@ 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 + " ")
+37 -16
View File
@@ -3,12 +3,14 @@
import argparse
import logging
from pathlib import Path
from typing import cast
import black
import isort
from generator import logger
from generator import LOGGER
from generator.loader import load_mobase
from generator.mtypes import Class, Function, Type
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
@@ -42,8 +44,13 @@ parser.add_argument(
args = parser.parse_args()
logging.basicConfig()
LOGGER.setLevel(logging.WARNING)
if args.verbose:
logger.setLevel(logging.INFO)
LOGGER.setLevel(logging.INFO)
output_path = cast(Path, args.output)
# Load settings from the configuration:
settings: Settings = Settings(register=MOBASE_REGISTER)
@@ -65,21 +72,26 @@ for name in dir(mobase):
if name in settings.ignore_names:
continue
# We do not want the real MoVariant.
# 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.
# 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. 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.
# 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])
)
@@ -105,7 +117,7 @@ for n, o in objects:
settings.patch_functions(c)
else:
logger.critical(
LOGGER.critical(
"Cannot generated stubs for {}, unsupported object type.".format(n)
)
@@ -113,15 +125,20 @@ for n, o in objects:
with open(args.output, "w") as output:
writer = Writer(output, settings)
# the __future__ import must be at the beginning
writer.print_imports([("__future__", ["annotations"])])
writer.print_version(settings.mobase["__version__"]) # type: ignore
writer.print_imports(
[
"abc",
("enum", ["Enum"]),
("pathlib", ["Path"]),
(
"typing",
[
"Dict",
"Iterable",
"Iterator",
"List",
"Tuple",
@@ -130,18 +147,21 @@ with open(args.output, "w") as output:
"Optional",
"Callable",
"overload",
"Set",
"TypeVar",
"Type",
],
),
"PyQt5.QtCore",
"PyQt5.QtGui",
"PyQt5.QtWidgets",
"PyQt6.QtCore",
"PyQt6.QtGui",
"PyQt6.QtWidgets",
]
)
# Needs to define the MVariant and GameFeatureType type:
writer._print("MoVariant = {}".format(Type.MO_VARIANT))
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()
@@ -162,8 +182,9 @@ with open(args.output, "w") as output:
writer.print_function(fn)
black.format_file_in_place(
args.output,
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)
+10 -3
View File
@@ -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
isort -c generator main.py
+771 -550
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff