Compare commits

..
5 Commits
22 changed files with 555 additions and 11592 deletions
@@ -18,13 +18,12 @@ jobs:
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: |
mkdir -p docs/src
cp -r stubs/2.5.2/mobase-stubs docs/src/mobase
run: cp stubs/2.5.0/mobase-stubs/__init__.pyi docs/mobase.py
- name: Build
run: poetry run sphinx-build -b html docs/source docs/build
env:
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
poetry version ${TAG#v}
poetry build
- name: Store the distribution packages
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: stubs/setup/dist/
@@ -49,7 +49,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v7
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: dist/
+1
View File
@@ -109,6 +109,7 @@ import mobase
import mobase.widgets
```
**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended
for MO2 developers.
+28 -350
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
sphinx-rtd-theme
sphinx-autodoc-typehints
sphinx-automodapi
PyQt6
+1 -5
View File
@@ -27,9 +27,8 @@ 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.
@@ -40,9 +39,6 @@ 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,6 +18,7 @@ This documentation is dedicated to writting MO2 **Python** plugins.
plugin-types
writing-plugins
faq
mobase
+11
View File
@@ -0,0 +1,11 @@
``mobase`` API
==============
.. currentmodule:: mobase
.. automodapi:: mobase
:no-inheritance-diagram:
:no-heading:
.. toctree::
:maxdepth: 4
+7
View File
@@ -0,0 +1,7 @@
mobase
======
.. toctree::
:maxdepth: 4
mobase
+5 -3
View File
@@ -64,8 +64,10 @@ 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 can register only the features they need to MO2 using :meth:`registerFeature<mobase.IGameFeatures.registerFeature>`
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.
As an example for a game feature take BSA invalidation: If the game requires BSA invalidation it will implement
this feature.
@@ -145,4 +147,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
+408 -531
View File
File diff suppressed because it is too large Load Diff
+30 -31
View File
@@ -1,39 +1,35 @@
[project]
name = "mo2-pystubs-generation"
description = ""
license = "MIT"
version = "0.1.0"
readme = "README.md"
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)",
]
[project.scripts]
mo2-stubs-generator = "mo2.stubs.generator.__main__:main"
[build-system]
requires = ['poetry-core (>=2.0,<3.0)']
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "mo2-pystubs-generation"
version = "0.1.0"
description = ""
authors = ["Holt59 <capelle.mikael@gmail.com>"]
license = "MIT"
readme = "README.md"
packages = [{ include = "mo2", from = "src" }]
[tool.poetry.scripts]
mo2-stubs-generator = "mo2.stubs.generator.__main__:main"
[tool.poetry.dependencies]
python = "^3.12"
pyqt6 = "^6.7.0"
pyyaml = "^6.0.1"
[tool.poetry.group.dev.dependencies]
pyright = "^1.1.409"
ruff = "^0.15.12"
types-pyyaml = "^6.0.12.20260508"
poethepoet = "^0.45.0"
pyright = "^1.1.365"
ruff = "^0.4.7"
types-pyyaml = "^6.0.12.20240311"
poethepoet = "^0.26.1"
[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"
sphinx-rtd-theme = "^2.0.0"
sphinx-autodoc-typehints = "^2.1.0"
sphinx-automodapi = "^0.16.0"
sphinx = "^7.3.7"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poe.tasks]
format-imports = "ruff check --select I src --fix"
@@ -46,11 +42,14 @@ lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
lint.ignore_fail = "return_non_zero"
[tool.ruff]
target-version = "py312"
target-version = "py310"
[tool.ruff.lint]
extend-select = ["B", "Q", "I"]
[tool.ruff.lint.isort]
known-first-party = ['tas']
[tool.pyright]
typeCheckingMode = "strict"
reportMissingTypeStubs = true
+13 -25
View File
@@ -8,19 +8,19 @@ from pathlib import Path
from typing import Callable
from .loader import load_mobase
from .mtypes import Class, Constant, Enum, Function, PyTyping
from .mtypes import Class, PyTyping
from .parser import is_enum
from .register import MobaseRegister
from .utils import Settings, clean_class
from .writer import Writer, is_list_of
from .writer import Writer, is_list_of_functions
LOGGER = logging.getLogger(__package__)
def extract_objects(
module: object, skips: Sequence[str] = []
) -> list[tuple[str, object]]:
objects: list[tuple[str, object]] = []
) -> list[tuple[str, type]]:
objects: list[tuple[str, type]] = []
assert hasattr(module, "__name__")
module_name: str = module.__name__ # type: ignore
@@ -140,7 +140,7 @@ def main() -> None:
}
# list of objects directly in mobase
module_objects: dict[str, list[tuple[str, object]]] = {
module_objects: dict[str, list[tuple[str, type]]] = {
"mobase": extract_objects(
mobase,
[
@@ -178,10 +178,10 @@ def main() -> None:
# Path the class using the configuration:
settings.patch_class(c)
elif isinstance(c, (PyTyping, Constant)):
elif isinstance(c, PyTyping):
...
elif is_list_of(c, Function):
elif is_list_of_functions(c):
settings.patch_functions(c)
else:
@@ -198,21 +198,6 @@ 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)
@@ -223,22 +208,25 @@ def main() -> None:
module_headers[name](writer)
for c in stub_objects:
for n, _o in objects:
# Get the corresponding object:
c = register.get_object(n)
writer.print_object(c)
subprocess.run(
[
"ruff",
"format",
"--silent",
"format",
output_folder.joinpath("__init__.pyi").as_posix(),
]
)
subprocess.run(
[
"ruff",
"check",
"--silent",
"check",
"--select",
"I",
"--fix",
+12 -15
View File
@@ -4,7 +4,7 @@ import re
import types
from collections import OrderedDict, defaultdict
from itertools import chain
from typing import Any, Callable, Iterable, cast
from typing import Any, 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: object) -> bool:
def is_enum(e: type) -> bool:
"""Check if the given class is an enumeration.
Args:
@@ -144,18 +144,17 @@ class Overload:
self.arguments = arguments
def parse_pybind11_function_docstring(name: str, doc: str | None) -> list[Overload]:
def parse_pybind11_function_docstring(e: type) -> list[Overload]:
"""
Parse the docstring of a Pybind11 function.
Parse the docstring of the given element.
Args:
name: Name of the function.
doc: The docstring of the function generated by Pybind11.
e: The function to "parse".
Returns:
A list of overloads for the given function.
"""
lines = (doc or "").strip().split("\n")
lines = (e.__doc__ or "").strip().split("\n")
signatures: list[str]
if len(lines) == 1:
@@ -163,7 +162,7 @@ def parse_pybind11_function_docstring(name: str, doc: str | None) -> list[Overlo
else:
signatures = []
for line in lines:
m = re.match(rf"^[0-9]+[.]\s+({name}.*)$", line)
m = re.match(rf"^[0-9]+[.]\s+({e.__name__}.*)$", line)
if m:
signatures.append(m.group(1).strip())
@@ -175,16 +174,16 @@ def parse_pybind11_function_docstring(name: str, doc: str | None) -> list[Overlo
signature = signature.replace("MOBase::", "mobase.").replace("::", ".")
try:
return_type, arguments = parse_python_signature(signature, name)
return_type, arguments = parse_python_signature(signature, e.__name__)
except ValueError as err:
raise ValueError(f"invalid signature: {name}, {doc}") from err
raise ValueError(f"invalid signature: {e.__name__}, {e.__doc__}") from err
overloads.append(Overload(return_type=return_type, arguments=arguments))
return overloads
def make_functions(e: Callable[..., Any]) -> list[Function]:
overloads = parse_pybind11_function_docstring(e.__name__, e.__doc__)
def make_functions(e: type) -> list[Function]:
overloads = parse_pybind11_function_docstring(e)
return [
Function(
@@ -296,9 +295,7 @@ def make_class(e: type, register: MobaseRegister) -> Class:
# otherwise we parse the docstring
else:
overloads = parse_pybind11_function_docstring(
method.__name__, method.__doc__
)
overloads = parse_pybind11_function_docstring(method)
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, Constant, Function, PyType, PyTyping
from .mtypes import Class, Function, PyTyping
class MobaseRegister:
@@ -12,20 +12,20 @@ class MobaseRegister:
Class that register classes.
"""
objects: dict[str, Class | Constant | list[Function] | PyTyping]
objects: dict[str, Class | list[Function] | PyTyping]
def __init__(self) -> None:
self.raw_objects: dict[str, object] = OrderedDict()
self.raw_objects: dict[str, type] = OrderedDict()
self.objects = {}
def add_object(self, n: str, o: object, /) -> None:
self.raw_objects[n] = o
def add_object(self, name: str, object: type) -> None:
self.raw_objects[name] = object
def make_object(
self, name: str, e: object | None = None
) -> Class | list[Function] | Constant | PyTyping:
self, name: str, e: type | None = None
) -> Class | list[Function] | PyTyping:
"""
Construct a Function, Class, Constant or Enum for the given object.
Construct a Function, Class 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 | Constant | list[Function] | PyTyping:
def get_object(self, name: str) -> Class | 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).
+9 -9
View File
@@ -215,7 +215,7 @@ class Settings:
# Check the args:
if function_settings.args is not None:
if len(function_settings.args) != len(fn.args):
LOGGER.warning(
LOGGER.warn(
f"Mismatch number of arguments for function "
f"mobase.{setting_name}."
)
@@ -243,7 +243,7 @@ class Settings:
fn.deprecated = function_settings.deprecated
else:
LOGGER.warning(
LOGGER.warn(
"Missing settings for function mobase.{}.".format(setting_name)
)
@@ -268,7 +268,7 @@ class Settings:
class_settings = self._get_class_settings(cls.canonical_name)
if class_settings is None:
LOGGER.warning("Class {} not found in settings.".format(cls.canonical_name))
LOGGER.warn("Class {} not found in settings.".format(cls.canonical_name))
return
if "__doc__" in class_settings and class_settings["__doc__"] is not None:
@@ -306,7 +306,7 @@ class Settings:
if "type" in settings_property:
prop.type = PyType(settings_property["type"])
else:
LOGGER.warning(
LOGGER.warn(
"Missing type for property {}.{}.".format(
cls.canonical_name, prop.name
)
@@ -320,7 +320,7 @@ class Settings:
prop.doc = settings_property["desc"]
else:
LOGGER.warning(
LOGGER.warn(
"Missing description for property {}.{}.".format(
cls.canonical_name, prop.name
)
@@ -366,7 +366,7 @@ 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.warning(
LOGGER.warn(
"Mismatch number of arguments for method {}.{}.".format(
cls.canonical_name, settings_name
)
@@ -380,7 +380,7 @@ class Settings:
not method_arg.name.startswith("arg")
and method_arg.name != settings_arg.name
):
LOGGER.warning(
LOGGER.warn(
(
"Mismatch argument name for method {}.{}: "
"{} {}, using {}."
@@ -429,7 +429,7 @@ class Settings:
if n_overloads > 0 and missing_settings:
for settings_name in missing_settings:
LOGGER.warning(
LOGGER.warn(
"Missing settings for method {}.{}.".format(
cls.canonical_name, settings_name
)
@@ -447,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.warning(
LOGGER.warn(
"The following members were found in settings but not in the actual"
" class {}: {}.".format(cls.canonical_name, ", ".join(missing_items))
)
+8 -23
View File
@@ -1,20 +1,16 @@
import logging
from typing import Any, TextIO
from typing import Any, Iterable, TextIO, TypeGuard
from typing_extensions import TypeIs
from .mtypes import Class, Constant, Enum, Function, Method, Property, PyTyping
from .mtypes import Class, Enum, Function, Method, Property, PyTyping
from .utils import Settings
LOGGER = logging.getLogger(__package__)
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)
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)
class Writer:
@@ -260,24 +256,13 @@ class Writer:
def print_typing(self, typ: PyTyping):
self._print(f"{typ.name} = {typ.typing}")
def print_constent(self, constant: Constant):
assert constant.type is not None
self._print(
"{}: {} = ...".format(
constant.name, self._fix_typing(constant.type.typing())
)
)
def print_object(self, e: Class | Constant | list[Function] | PyTyping):
def print_object(self, e: object):
if isinstance(e, Class):
self.print_class(e)
elif is_list_of(e, Function):
elif is_list_of_functions(e):
for fn in e:
self.print_function(fn)
elif isinstance(e, PyTyping):
self.print_typing(e)
else:
self.print_constent(e)
File diff suppressed because it is too large Load Diff
@@ -1,172 +0,0 @@
from __future__ import annotations
__version__ = "2.5.2"
from typing import List, Tuple, Union, overload
import PyQt6.QtCore
import PyQt6.QtGui
import PyQt6.QtWidgets
class TaskDialog:
"""
Customizable choice dialog.
"""
def __init__(
self: TaskDialog,
parent: PyQt6.QtWidgets.QWidget | None = None,
title: str = "",
main: str = "",
content: str = "",
details: str = "",
icon: PyQt6.QtWidgets.QMessageBox.Icon = PyQt6.QtWidgets.QMessageBox.Icon.NoIcon,
buttons: list[TaskDialogButton] = [],
remember: Union[str, tuple[str, str]] = "",
) -> None:
"""
Construct a new TaskDialog.
Args:
parent: Parent widget of the dialog.
title: Title of the dialog.
main: Header of the dialog (big text at the top).
content: Main message of the dialog (text below main).
details: Details for the dialog, initially collapsed (bottom of the dialog).
icon: Icon for the dialog.
buttons: List of buttons for the dialog.
remember: Remember the choice for this dialog.
"""
...
def addButton(self: TaskDialog, button: TaskDialogButton) -> TaskDialog:
"""
Add a custom button to this TaskDialog.
Args:
button: Button to add to the dialog.
"""
...
def addContent(self: TaskDialog, widget: PyQt6.QtWidgets.QWidget) -> None:
"""
Add a custom widget content to this TaskDialog. Widget content are put between
content and buttons (above buttons).
Args:
widget: Widget to add.
"""
...
def exec(self: TaskDialog) -> PyQt6.QtWidgets.QMessageBox.StandardButton:
"""
Display this dialog and wait for user-interaction to return. This is a blocking
function.
Returns:
The button clicked by the user. Without custom buttons, this return Ok,
otherwise it returns the button set in the TaskDialogButton.
"""
...
def setContent(self: TaskDialog, content: str) -> TaskDialog:
"""
Set the top-level message of this dialog.
Args:
content: Top-level message to set.
"""
...
def setDetails(self: TaskDialog, details: str) -> TaskDialog:
"""
Set the details for this TaskDialog.
The details are hidden by default and the user can display them by clicking
the "Details" button at the bottom of the TaskDialog.
Args:
details: Details content to display. Can be a multi-line string.
"""
...
def setIcon(self: TaskDialog, icon: PyQt6.QtWidgets.QMessageBox.Icon) -> TaskDialog:
"""
Set the icon of the dialog.
Args:
icon: Icon of the dialog.
"""
...
def setMain(self: TaskDialog, main: str) -> TaskDialog:
"""
Set the main message of the dialog. The main message is displayed at the top of
the dialog in large font.
Args:
main: Main message of the dialog.
"""
...
def setRemember(self: TaskDialog, action: str, file: str = "") -> TaskDialog:
"""
Configure the dialog to remember user-choice.
"""
...
def setTitle(self: TaskDialog, title: str) -> TaskDialog:
"""
Set the title of the dialog.
Args:
title: Title of the dialog.
"""
...
def setWidth(self: TaskDialog, width: int) -> None:
"""
Set the width of the dialog.
Args:
width: Width of the dialog.
"""
...
class TaskDialogButton:
"""
Special button to be used inside TaskDialog widgets.
"""
@property
def button(self) -> PyQt6.QtWidgets.QMessageBox.StandardButton: ...
@button.setter
def button(self, arg0: PyQt6.QtWidgets.QMessageBox.StandardButton) -> None: ...
@property
def description(self) -> str: ...
@description.setter
def description(self, arg0: str) -> None: ...
@property
def text(self) -> str: ...
@text.setter
def text(self, arg0: str) -> None: ...
@overload
def __init__(
self: TaskDialogButton,
text: str,
description: str,
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
) -> None:
"""
Create a TaskDialogButton.
Args:
text: Label of the button.
description: Description of the button.
button: Value returned by TaskDialog.exec() if this button is clicked.
"""
...
@overload
def __init__(
self: TaskDialogButton,
text: str,
button: PyQt6.QtWidgets.QMessageBox.StandardButton,
) -> None:
"""
Create a TaskDialogButton without description.
Args:
text: Label of the button.
button: Value returned by TaskDialog.exec() if this button is clicked.
"""
...
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