Compare commits

...
14 Commits
27 changed files with 16930 additions and 220 deletions
@@ -15,15 +15,16 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Install
run: |
poetry 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.0/mobase-stubs/__init__.pyi docs/mobase.py
run: |
mkdir -p docs/src
cp -r stubs/2.5.2/mobase-stubs docs/src/mobase
- name: Build
run: poetry run sphinx-build -b html docs/source docs/build
env:
+2 -6
View File
@@ -10,15 +10,11 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Install
run: |
poetry install
- name: Lint
run: |
poetry run black src --check --diff
poetry run isort -c src
poetry run mypy src
poetry run ruff src
poetry run pyright src
poetry run poe lint
+3 -3
View File
@@ -22,7 +22,7 @@ jobs:
replace-with: "$1"
- uses: actions/setup-python@v2
with:
python-version: 3.11
python-version: 3.12
- uses: abatilo/actions-poetry@v2
- name: Build
run: |
@@ -34,7 +34,7 @@ jobs:
poetry version ${TAG#v}
poetry build
- name: Store the distribution packages
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v7
with:
name: python-package-distributions
path: stubs/setup/dist/
@@ -49,7 +49,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
-1
View File
@@ -109,7 +109,6 @@ import mobase
import mobase.widgets
```
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
for MO2 developers.
+519 -45
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
sphinx-rtd-theme
sphinx-autodoc-typehints
sphinx-automodapi
PyQt6
+5 -1
View File
@@ -27,8 +27,9 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.napoleon",
"autoapi.extension",
# "sphinx.ext.autosummary",
"sphinx_automodapi.automodapi",
# "sphinx_automodapi.automodapi",
]
# Add any paths that contain templates here, relative to this directory.
@@ -39,6 +40,9 @@ templates_path = ["_templates"]
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
autoapi_dirs = ["../src"]
autoapi_member_order = "groupwise"
# -- Options for HTML output -------------------------------------------------
-1
View File
@@ -18,7 +18,6 @@ This documentation is dedicated to writting MO2 **Python** plugins.
plugin-types
writing-plugins
faq
mobase
-11
View File
@@ -1,11 +0,0 @@
``mobase`` API
==============
.. currentmodule:: mobase
.. automodapi:: mobase
:no-inheritance-diagram:
:no-heading:
.. toctree::
:maxdepth: 4
-7
View File
@@ -1,7 +0,0 @@
mobase
======
.. toctree::
:maxdepth: 4
mobase
+3 -5
View File
@@ -64,10 +64,8 @@ These plugins (shall eventually) implement all the game specific features and fu
able to add support for further games.
The plugin is also responsible to help MO determine if (and where) the game is installed in the first place.
Since supporting a game properly requires extensions in many places of the UI.
To allow this without creating one huge plugin interface that involves every aspect of MO, game plugins
expose a *feature list*.
The list of possible features can be found in the "game_features" project and each feature can itself be
considered a plugin interface.
To allow this without creating one huge plugin interface that involves every aspect of MO,
game plugins can register only the features they need to MO2 using :meth:`registerFeature<mobase.IGameFeatures.registerFeature>`
As an example for a game feature take BSA invalidation: If the game requires BSA invalidation it will implement
this feature.
@@ -147,4 +145,4 @@ File Mappings
This interface allows plugins to add virtual file (or directory) links to the virtual file system in addition to the
mod files.
Profile-local save games, ini-files and load-orders are all implemented this way in MO2.
Profile-local save games, ini-files and load-orders are all implemented this way in MO2.
Generated
+892
View File
File diff suppressed because it is too large Load Diff
+39 -44
View File
@@ -1,61 +1,56 @@
[tool.poetry]
[project]
name = "mo2-pystubs-generation"
version = "0.1.0"
description = ""
authors = ["Holt59 <capelle.mikael@gmail.com>"]
license = "MIT"
version = "0.1.0"
readme = "README.md"
packages = [{ include = "mo2", from = "src" }]
authors = [{ name = "Holt59", email = "capelle.mikael@gmail.com" }]
requires-python = ">=3.12,<4.0"
dependencies = [
"pyqt6 (==6.11.0)",
"pyyaml (>=6.0.3,<7.0.0)",
"typing-extensions (>=4.15.0,<5.0.0)",
]
[tool.poetry.scripts]
[project.scripts]
mo2-stubs-generator = "mo2.stubs.generator.__main__:main"
[tool.poetry.dependencies]
python = "^3.11"
pyqt6 = "^6.5.2"
pyyaml = "^6.0.1"
[tool.poetry.group.dev.dependencies]
black = "^23.9.1"
mypy = "^1.5.1"
pyright = "^1.1.327"
isort = "^5.12.0"
ruff = "^0.0.290"
flake8 = "^6.1.0"
flake8-black = "^0.3.6"
flake8-pyproject = "^1.2.3"
types-pyyaml = "^6.0.12.11"
[tool.poetry.group.doc.dependencies]
sphinx-rtd-theme = "^1.3.0"
sphinx-autodoc-typehints = "^1.24.0"
sphinx-automodapi = "^0.16.0"
sphinx = "^7.2.6"
[build-system]
requires = ["poetry-core"]
requires = ['poetry-core (>=2.0,<3.0)']
build-backend = "poetry.core.masonry.api"
[tool.flake8]
max-line-length = 88
extend-ignore = ["E203"]
[tool.poetry]
packages = [{ include = "mo2", from = "src" }]
[tool.isort]
profile = "black"
multi_line_output = 3
[tool.poetry.group.dev.dependencies]
pyright = "^1.1.409"
ruff = "^0.15.12"
types-pyyaml = "^6.0.12.20260508"
poethepoet = "^0.45.0"
[tool.poetry.group.doc.dependencies]
sphinx-rtd-theme = "^3.0.2"
sphinx-autodoc-typehints = "^3.2.0"
sphinx-automodapi = "^0.20.0"
sphinx = "^8.2.3"
sphinx-autoapi = "^3.6.0"
[tool.poe.tasks]
format-imports = "ruff check --select I src --fix"
format-ruff = "ruff format src"
format.sequence = ["format-imports", "format-ruff"]
lint-ruff = "ruff check src"
lint-ruff-format = "ruff format --check src"
lint-pyright = "pyright src"
lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
lint.ignore_fail = "return_non_zero"
[tool.ruff]
line-length = 88
target-version = "py311"
target-version = "py312"
[tool.mypy]
warn_return_any = true
warn_unused_configs = true
namespace_packages = true
[tool.ruff.lint]
extend-select = ["B", "Q", "I"]
[tool.pyright]
# reportMissingTypeStubs = true
# reportUntypedBaseClass = false
typeCheckingMode = "strict"
reportMissingTypeStubs = true
+46 -24
View File
@@ -1,25 +1,26 @@
import argparse
import inspect
import logging
import subprocess
import types
from collections.abc import Sequence
from pathlib import Path
from typing import Callable
import black
import isort
from .loader import load_mobase
from .mtypes import Class, PyTyping
from .mtypes import Class, Constant, Enum, Function, PyTyping
from .parser import is_enum
from .register import MobaseRegister
from .utils import Settings, clean_class
from .writer import Writer, is_list_of_functions
from .writer import Writer, is_list_of
LOGGER = logging.getLogger(__package__)
def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, type]]:
objects: list[tuple[str, type]] = []
def extract_objects(
module: object, skips: Sequence[str] = []
) -> list[tuple[str, object]]:
objects: list[tuple[str, object]] = []
assert hasattr(module, "__name__")
module_name: str = module.__name__ # type: ignore
@@ -56,12 +57,9 @@ def add_mobase_header(writer: Writer):
"Callable",
"Dict",
"Iterator",
"List",
"Optional",
"overload",
"Sequence",
"Set",
"Tuple",
"Type",
"TypeVar",
"Union",
@@ -142,7 +140,7 @@ def main() -> None:
}
# list of objects directly in mobase
module_objects: dict[str, list[tuple[str, type]]] = {
module_objects: dict[str, list[tuple[str, object]]] = {
"mobase": extract_objects(
mobase,
[
@@ -150,7 +148,7 @@ def main() -> None:
"IPlugin",
],
),
"mobase.widgets": extract_objects(getattr(mobase, "widgets")),
"mobase.widgets": extract_objects(mobase.widgets), # type: ignore
}
for name, objects in module_objects.items():
@@ -180,10 +178,10 @@ def main() -> None:
# Path the class using the configuration:
settings.patch_class(c)
elif isinstance(c, PyTyping):
elif isinstance(c, (PyTyping, Constant)):
...
elif is_list_of_functions(c):
elif is_list_of(c, Function):
settings.patch_functions(c)
else:
@@ -200,6 +198,21 @@ def main() -> None:
# create directory if required
output_folder.mkdir(parents=True, exist_ok=True)
# sort the stubs
def _key_fn(o: Class | Constant | list[Function] | PyTyping) -> tuple[int, ...]:
# order is PyTyping -> Constant -> Function -> Enum -> Top-Level Class -> Child Level Classes
return (
not isinstance(o, PyTyping),
not isinstance(o, Constant),
not isinstance(o, list),
not isinstance(o, Enum),
isinstance(o, Class) and len(o.all_bases),
)
stub_objects = sorted(
(register.get_object(n) for n, _o in objects), key=_key_fn
)
# write everything
with open(output_folder.joinpath("__init__.pyi"), "w") as output:
writer = Writer(package=name, output=output, settings=settings)
@@ -210,19 +223,28 @@ def main() -> None:
module_headers[name](writer)
for n, o in objects:
# Get the corresponding object:
c = register.get_object(n)
for c in stub_objects:
writer.print_object(c)
black.format_file_in_place(
output_folder.joinpath("__init__.pyi"),
fast=False,
mode=black.Mode(is_pyi=True),
write_back=black.WriteBack.YES,
subprocess.run(
[
"ruff",
"format",
"--silent",
output_folder.joinpath("__init__.pyi").as_posix(),
]
)
subprocess.run(
[
"ruff",
"check",
"--silent",
"--select",
"I",
"--fix",
output_folder.joinpath("__init__.pyi").as_posix(),
]
)
isort.api.sort_file(output_folder.joinpath("__init__.pyi"))
if __name__ == "__main__":
+11 -13
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import re
from collections.abc import Sequence
from typing import Final, TypeVar
@@ -164,7 +165,6 @@ class Argument:
class Exception:
"""
Small class representing exception that can be raised from functions.
"""
@@ -313,20 +313,20 @@ class Class:
self,
package: str,
name: str,
bases: list[Class],
methods: list[Method],
constants: list[Constant] = [],
properties: list[Property] = [],
inner_classes: list[Class] = [],
bases: Sequence[Class],
methods: Sequence[Method],
constants: Sequence[Constant] = [],
properties: Sequence[Property] = [],
inner_classes: Sequence[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.bases = list(bases)
self.methods = list(methods)
self.properties = list(properties)
self.constants = list(constants)
self.inner_classes = list(inner_classes)
self.doc = ""
self.abstract = False
self.outer_class = None
@@ -385,7 +385,6 @@ class Class:
class PyClass(Class):
"""
Class use to wrap Python class to be used as parent class for some classes
in mobase.
@@ -401,7 +400,6 @@ class PyClass(Class):
class Enum(Class):
"""
Class representing an enum.
"""
+18 -15
View File
@@ -4,7 +4,7 @@ import re
import types
from collections import OrderedDict, defaultdict
from itertools import chain
from typing import Any, Iterable, cast
from typing import Any, Callable, Iterable, cast
from .mtypes import (
Argument,
@@ -118,7 +118,7 @@ def parse_python_signature(s: str, name: str) -> tuple[PyType, list[Argument]]:
return PyType(return_type), arguments
def is_enum(e: type) -> bool:
def is_enum(e: object) -> bool:
"""Check if the given class is an enumeration.
Args:
@@ -134,7 +134,6 @@ def is_enum(e: type) -> bool:
class Overload:
"""Small class to avoid mypy issues..."""
return_type: PyType
@@ -145,17 +144,18 @@ class Overload:
self.arguments = arguments
def parse_pybind11_function_docstring(e: type) -> list[Overload]:
def parse_pybind11_function_docstring(name: str, doc: str | None) -> list[Overload]:
"""
Parse the docstring of the given element.
Parse the docstring of a Pybind11 function.
Args:
e: The function to "parse".
name: Name of the function.
doc: The docstring of the function generated by Pybind11.
Returns:
A list of overloads for the given function.
"""
lines = (e.__doc__ or "").strip().split("\n")
lines = (doc or "").strip().split("\n")
signatures: list[str]
if len(lines) == 1:
@@ -163,7 +163,7 @@ def parse_pybind11_function_docstring(e: type) -> list[Overload]:
else:
signatures = []
for line in lines:
m = re.match(rf"^[0-9]+[.]\s+({e.__name__}.*)$", line)
m = re.match(rf"^[0-9]+[.]\s+({name}.*)$", line)
if m:
signatures.append(m.group(1).strip())
@@ -175,16 +175,16 @@ def parse_pybind11_function_docstring(e: type) -> list[Overload]:
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__}")
return_type, arguments = parse_python_signature(signature, name)
except ValueError as err:
raise ValueError(f"invalid signature: {name}, {doc}") from err
overloads.append(Overload(return_type=return_type, arguments=arguments))
return overloads
def make_functions(e: type) -> list[Function]:
overloads = parse_pybind11_function_docstring(e)
def make_functions(e: Callable[..., Any]) -> list[Function]:
overloads = parse_pybind11_function_docstring(e.__name__, e.__doc__)
return [
Function(
@@ -221,7 +221,8 @@ def make_class(e: type, register: MobaseRegister) -> Class:
# 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
register.make_object(name)
for name in base_classes_s # type: ignore
]
# retrieve all the attributes that are not in a base class
@@ -295,7 +296,9 @@ def make_class(e: type, register: MobaseRegister) -> Class:
# otherwise we parse the docstring
else:
overloads = parse_pybind11_function_docstring(method)
overloads = parse_pybind11_function_docstring(
method.__name__, method.__doc__
)
for overload in overloads:
args = overload.arguments
+11 -11
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections import OrderedDict
from .mtypes import Class, Function, PyTyping
from .mtypes import Class, Constant, Function, PyType, PyTyping
class MobaseRegister:
@@ -12,20 +12,20 @@ class MobaseRegister:
Class that register classes.
"""
objects: dict[str, Class | list[Function] | PyTyping]
objects: dict[str, Class | Constant | list[Function] | PyTyping]
def __init__(self) -> None:
self.raw_objects: dict[str, type] = OrderedDict()
self.raw_objects: dict[str, object] = OrderedDict()
self.objects = {}
def add_object(self, name: str, object: type) -> None:
self.raw_objects[name] = object
def add_object(self, n: str, o: object, /) -> None:
self.raw_objects[n] = o
def make_object(
self, name: str, e: type | None = None
) -> Class | list[Function] | PyTyping:
self, name: str, e: object | None = None
) -> Class | list[Function] | Constant | PyTyping:
"""
Construct a Function, Class or Enum for the given object.
Construct a Function, Class, Constant or Enum for the given object.
Args:
name: The name of the object to inspect.
@@ -47,14 +47,14 @@ class MobaseRegister:
self.objects[name] = make_class(e, self)
elif callable(e):
self.objects[name] = make_functions(e)
# typing stuff
elif type(e).__module__ == "types" or type(e).__module__ == "typing":
self.objects[name] = PyTyping(name, e)
else:
self.objects[name] = Constant(name, type=PyType(type(e)), value=e)
return self.objects[name]
def get_object(self, name: str) -> Class | list[Function] | PyTyping:
def get_object(self, name: str) -> Class | Constant | list[Function] | PyTyping:
"""
Retrieve the object if the given name. Fails if no object with this
name exists (if `make_object(name, ...)` has never been called).
+13 -11
View File
@@ -215,12 +215,14 @@ class Settings:
# Check the args:
if function_settings.args is not None:
if len(function_settings.args) != len(fn.args):
LOGGER.warn(
LOGGER.warning(
f"Mismatch number of arguments for function "
f"mobase.{setting_name}."
)
for setting_arg, method_arg in zip(function_settings.args, fn.args):
for setting_arg, method_arg in zip(
function_settings.args, fn.args, strict=True
):
method_arg.doc = setting_arg.doc
if not setting_arg.type.is_none():
method_arg.type = setting_arg.type
@@ -241,7 +243,7 @@ class Settings:
fn.deprecated = function_settings.deprecated
else:
LOGGER.warn(
LOGGER.warning(
"Missing settings for function mobase.{}.".format(setting_name)
)
@@ -266,7 +268,7 @@ class Settings:
class_settings = self._get_class_settings(cls.canonical_name)
if class_settings is None:
LOGGER.warn("Class {} not found in settings.".format(cls.canonical_name))
LOGGER.warning("Class {} not found in settings.".format(cls.canonical_name))
return
if "__doc__" in class_settings and class_settings["__doc__"] is not None:
@@ -304,7 +306,7 @@ class Settings:
if "type" in settings_property:
prop.type = PyType(settings_property["type"])
else:
LOGGER.warn(
LOGGER.warning(
"Missing type for property {}.{}.".format(
cls.canonical_name, prop.name
)
@@ -318,7 +320,7 @@ class Settings:
prop.doc = settings_property["desc"]
else:
LOGGER.warn(
LOGGER.warning(
"Missing description for property {}.{}.".format(
cls.canonical_name, prop.name
)
@@ -364,21 +366,21 @@ class Settings:
if function_settings.args is not None:
method_arguments = m.args if m.is_static() else m.args[1:]
if len(function_settings.args) != len(method_arguments):
LOGGER.warn(
LOGGER.warning(
"Mismatch number of arguments for method {}.{}.".format(
cls.canonical_name, settings_name
)
)
for settings_arg, method_arg in zip(
function_settings.args, method_arguments
function_settings.args, method_arguments, strict=True
):
method_arg.doc = settings_arg.doc
if (
not method_arg.name.startswith("arg")
and method_arg.name != settings_arg.name
):
LOGGER.warn(
LOGGER.warning(
(
"Mismatch argument name for method {}.{}: "
"{} {}, using {}."
@@ -427,7 +429,7 @@ class Settings:
if n_overloads > 0 and missing_settings:
for settings_name in missing_settings:
LOGGER.warn(
LOGGER.warning(
"Missing settings for method {}.{}.".format(
cls.canonical_name, settings_name
)
@@ -445,7 +447,7 @@ class Settings:
# Print items missing in mobase
missing_items = [k for k, v in keys.items() if not v]
if missing_items:
LOGGER.warn(
LOGGER.warning(
"The following members were found in settings but not in the actual"
" class {}: {}.".format(cls.canonical_name, ", ".join(missing_items))
)
+24 -9
View File
@@ -1,16 +1,20 @@
import logging
from typing import Any, Iterable, TextIO, TypeGuard
from typing import Any, TextIO
from .mtypes import Class, Enum, Function, Method, Property, PyTyping
from typing_extensions import TypeIs
from .mtypes import Class, Constant, Enum, Function, Method, Property, PyTyping
from .utils import Settings
LOGGER = logging.getLogger(__package__)
def is_list_of_functions(e: Any | Iterable[Any]) -> TypeGuard[list[Function]]:
if not isinstance(e, list):
return False
return all(isinstance(x, Function) for x in e)
def is_list_of_any(e: Any) -> TypeIs[list[Any]]:
return isinstance(e, list)
def is_list_of[T](e: Any, t: type[T]) -> TypeIs[list[T]]:
return is_list_of_any(e) and all(isinstance(x, t) for x in e)
class Writer:
@@ -163,7 +167,7 @@ class Writer:
if not prop.is_read_only():
self._print("{}@{}.setter".format(indent, prop.name))
self._print(
"{}def {}(self, arg0: {}): ...".format(
"{}def {}(self, arg0: {}) -> None: ...".format(
indent, prop.name, self._fix_typing(prop.type.typing())
)
)
@@ -256,13 +260,24 @@ class Writer:
def print_typing(self, typ: PyTyping):
self._print(f"{typ.name} = {typ.typing}")
def print_object(self, e: object):
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):
elif is_list_of(e, Function):
for fn in e:
self.print_function(fn)
elif isinstance(e, PyTyping):
self.print_typing(e)
else:
self.print_constent(e)
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