diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index a7eeebf..985cd8d 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -1,36 +1,45 @@ name: Build Documentation on: + pull_request: push: branches: - master jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - # Standard drop-in approach that should work for most people. - - uses: ammaraskar/sphinx-action@master - env: - PYTHONPATH: . - with: - pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py" - docs-folder: "docs/" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - uses: abatilo/actions-poetry@v2 + - name: Install + run: | + poetry install + - name: Install libgl1 + run: sudo apt install -y libgl1 libegl1 libglib2.0-0 libxkbcommon0 libdbus-1-3 + - name: Copy stubs + run: cp stubs/2.5.0/mobase-stubs/__init__.pyi docs/mobase.py + - name: Build + run: poetry run sphinx-build -b html docs/source docs/build + env: + PYTHONPATH: docs - - name: Install SSH Client 🔑 - uses: webfactory/ssh-agent@v0.4.1 - with: - ssh-private-key: ${{ secrets.DEPLOY_KEY }} + - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + name: Install SSH Client 🔑 + uses: webfactory/ssh-agent@v0.4.1 + with: + ssh-private-key: ${{ secrets.DEPLOY_KEY }} - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@3.7.1 - with: - SSH: true - REPOSITORY_NAME: ModOrganizer2/python-plugins-doc - BRANCH: master - FOLDER: docs/build/html + - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + name: Deploy Documentation + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + SSH: true + REPOSITORY_NAME: ModOrganizer2/python-plugins-doc + BRANCH: master + FOLDER: docs/build diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index c2af639..ff91ab5 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -5,20 +5,20 @@ on: [push, pull_request] jobs: checks: runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: [3.8] - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Test with tox - run: tox -e py38-lint + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - 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 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml deleted file mode 100644 index d09baff..0000000 --- a/.github/workflows/pull_request.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Check Documentation - -on: [pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - # Standard drop-in approach that should work for most people. - - uses: ammaraskar/sphinx-action@master - env: - PYTHONPATH: . - with: - pre-build-command: "apt-get update -y && apt-get install -y libgl1-mesa-glx && cp stubs/2.4.0/mobase.pyi docs/mobase.py" - docs-folder: "docs/" diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index e2db923..6b062f6 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -4,37 +4,37 @@ name: Upload Python Package on: - release: - types: [published] + push: + tags: ["*"] jobs: deploy: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Replace string - uses: frabert/replace-string-action@v1.1 - id: version - with: - string: ${{ github.event.release.tag_name }} - pattern: "v?([0-9][.][0-9][.][0-9]).*" - replace-with: "$1" - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - cd stubs/setup - cp ../${{ steps.version.outputs.replaced }}/mobase.pyi mobase-stubs/__init__.pyi - python setup.py sdist bdist_wheel - twine upload dist/* \ No newline at end of file + - uses: actions/checkout@v4 + - name: Replace string + uses: frabert/replace-string-action@v1.1 + id: version + with: + string: ${{ github.ref_name }} + pattern: "v?([0-9][.][0-9][.][0-9]).*" + replace-with: "$1" + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + cd stubs/setup + cp -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/* diff --git a/.gitignore b/.gitignore index faa97aa..cdea324 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,10 @@ .mypy_cache __pycache__ .vscode +**/*.egg-info # The 'bin/' directory: bin docs/build docs/mobase.py -docs/source/api \ No newline at end of file +docs/source/api diff --git a/README.md b/README.md index a5a64b3..4af9f23 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,12 @@ MO2. You can install stubs for a specific version of MO2: ```bash -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 . +pip install mobase-stubs==2.5.* ``` 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 +poetry install + +# change the output folder to whatever you want +mo2-stubs-generator -c configs/config-2.4.yml -o mobase-stubs ${MO2_INSTALL_PATH} ``` -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,37 @@ 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. +You can also import `mobase` in your code using the following (after installing +this package): -# License +```python +from mo2.stubs.generator import load_mobase + +mobase = load_mobase(MO2_INSTALL_PATH) + +# the above will probably not give you type-completion in your IDE or typing, so +# you can use the following (if the stubs are installed) +load_mobase(MO2_INSTALL_PATH) +import mobase +import mobase.widgets +``` + + +**Note:** Most classes in `mobase` cannot be instantiated, so this is mostly intended +for MO2 developers. + +## License The MIT License (MIT) diff --git a/configs/config-2.5.yml b/configs/config-2.5.yml new file mode 100644 index 0000000..8436b55 --- /dev/null +++ b/configs/config-2.5.yml @@ -0,0 +1,3269 @@ +--- +# version of the configuration +version: 2 + +# version of the stubs - this is overridden when publishing +__version__: "2.5.0" + +# This is the root of the mobase module and will contain everything +# related to functions / classes, including their documentation. +mobase: + + getFileVersion: + __doc__: Retrieve the file version of the given executable. + args: + filepath: Absolute path to the executable. + returns: | + The file version, or an empty string if the file version could not be retrieved. + + getIconForExecutable: + __doc__: Retrieve the icon of an executable. Currently this always extracts the biggest icon. + args: + executable: Absolute path to the executable. + returns: The icon for this executable, if any. + + getProductVersion: + __doc__: Retrieve the product version of the given executable. + args: + executable: Absolute path to the executable. + returns: | + The product version, or an empty string if the product version could not be retrieved. + + EndorsedState: + ENDORSED_TRUE: + ENDORSED_FALSE: + ENDORSED_UNKNOWN: + ENDORSED_NEVER: + + TrackedState: + TRACKED_FALSE: + TRACKED_TRUE: + TRACKED_UNKNOWN: + + GuessQuality: + __doc__: | + Describes how good the code considers a guess (i.e. for a mod name) this is used to + determine if a name from another source should overwrite or not. + + INVALID: No valid value has been set yet. + FALLBACK: The guess is very basic and should only be used if no other source is available. + GOOD: Considered a good guess. + META: The value comes from metadata and is usually what the author intended. + PRESET: | + The value comes from a previous installation of the same data/mod and usually represents + what the user chose before. + USER: The user selection, always overrules other sources. + + InstallResult: + __doc__: + SUCCESS: + FAILED: + CANCELED: + MANUAL_REQUESTED: + NOT_ATTEMPTED: + + LoadOrderMechanism: + __doc__: + FILE_TIME: Order of plugins is determined by the filetime of the plugins. + PLUGINS_TXT: Order of plugins is determined by the plugins.txt file. + + ModState: + __doc__: + EXISTS: + ACTIVE: + ESSENTIAL: + EMPTY: + ENDORSED: + VALID: + ALTERNATE: + + PluginState: + __doc__: + MISSING: + INACTIVE: + ACTIVE: + + ProfileSetting: + __doc__: + MODS: + CONFIGURATION: + SAVEGAMES: + PREFER_DEFAULTS: + + ReleaseType: + __doc__: + PRE_ALPHA: + ALPHA: + BETA: + CANDIDATE: + FINAL: + + SortMechanism: + __doc__: + NONE: + MLOX: + BOSS: + LOOT: + + VersionScheme: + __doc__: + DISCOVER: + REGULAR: + DECIMAL_MARK: + NUMBERS_AND_LETTERS: + DATE: + LITERAL: + + # TODO: + BSAInvalidation: + __doc__: + __abstract__: true + __init__: + __doc__: + activate: + __doc__: + args: + profile: + deactivate: + __doc__: + args: + profile: + isInvalidationBSA: + __doc__: + args: + name: + returns: + + DataArchives: + __abstract__: true + + addArchive: + __doc__: Add an archive to the archive list. + args: + profile: Profile to add the archive to. + index: | + Index to insert before. Use 0 for the beginning of the list or INT_MAX for + the end of the list). + name: Name of the archive to add. + + archives: + __doc__: Retrieve the list of archives in the given profile. + args: + profile: Profile to retrieve archives from. + returns: The list of archives in the given profile. + + removeArchive: + __doc__: Remove the given archive from the given profile. + args: + profile: Profile to remove the archive from. + name: Name of the archive to remove. + + vanillaArchives: + __doc__: | + Retrieve the list of vanilla archives. + + Vanilla archives are archive files that are shipped with the original + game. + returns: The list of vanilla archives. + + ExecutableForcedLoadSetting: + __doc__: + __init__: + __doc__: + args: + process: + library: + enabled: + __doc__: + returns: + forced: + __doc__: + returns: + library: + __doc__: + returns: + process: + __doc__: + returns: + withEnabled: + __doc__: + args: + enabled: + returns: + withForced: + __doc__: + args: + forced: + returns: + + ExecutableInfo: + __doc__: + __init__: + __doc__: + args: + title: + binary: + arguments: + __doc__: + returns: + asCustom: + __doc__: + returns: + binary: + __doc__: + returns: + isCustom: + __doc__: + returns: + isValid: + __doc__: + returns: + steamAppID: + __doc__: + returns: + title: + __doc__: + returns: + withArgument: + __doc__: + args: + argument: + returns: + withSteamAppId: + __doc__: + args: + app_id: + returns: + withWorkingDirectory: + __doc__: + args: + directory: + returns: + workingDirectory: + __doc__: + returns: + + FileInfo: + __doc__: Information about a virtualized file + properties[]: + archive: + type: str + desc: | + Name of the archive if this file is in an archive (e.g. BSA), otherwise an + empty string. + filePath: + type: str + desc: Full path to the file. + origins: + type: List[str] + desc: | + List of origins containing providing this file. The first origin in the list + is the highest priority one (actually providing the file). + + __init__: + __doc__: Creates an uninitialized FileInfo. + + FileTreeEntry: + __doc__: | + Represent an entry in a file tree, either a file or a directory. This class + inherited by IFileTree so that operations on entry are the same for a file or + a directory. + + This class provides convenience methods to query information on the file, like its + name or the its last modification time. It also provides a convenience astree() method + that can be used to retrieve the tree corresponding to its entry in case the entry + represent a directory. + + FileTypes: + __doc__: Enumeration of the different file type or combinations. + DIRECTORY: + FILE: + FILE_OR_DIRECTORY: + + detach: + __doc__: Detach this entry from its parent tree. + returns: True if the entry was removed correctly, False otherwise. + + fileType: + returns: The filetype of this entry. + + hasSuffix.1: + __doc__: Check if this entry has one of the given suffixes. + args: + suffixes: Suffixes to check. + returns: True if this entry is a file and has one of the given suffix. + + hasSuffix.2: + __doc__: Check if this entry has the given suffix. + args: + suffix: Suffix to check. + returns: True if this entry is a file and has the given suffix. + + isDir: + returns: True if this entry is a directory, False otherwise. + + isFile: + returns: True if this entry is a file, False otherwise. + + moveTo: + __doc__: Move this entry to the given tree. + args: + tree: The tree to move this entry to. + returns: True if the entry was moved correctly, False otherwise. + + name: + returns: The name of this entry. + + parent: + returns: + __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] + + path: + __doc__: | + Retrieve the path from this entry up to the root of the tree. + + This method propagate up the tree so is not constant complexity as + the full path is never stored. + args: + sep: The type of separator to use to create the path. + returns: The path from this entry to the root, including the name of this entry. + + pathFrom: + __doc__: Retrieve the path from the given tree to this entry. + args: + tree: The tree to reach, must be a parent of this entry. + sep: The type of separator to use to create the path. + returns: | + The path from the given tree to this entry, including the name of this entry, or + an empty string if the given tree is not a parent of this entry. + + suffix: + __doc__: | + Retrieve the "last" extension of this entry. + + The "last" extension is everything after the last dot in the file name. + returns: | + The last extension of this entry, or an empty string if the file has no extension + or is directory. + + GamePlugins: + __abstract__: true + getLoadOrder: + __doc__: + returns: + lightPluginsAreSupported: + returns: True if light plugins are supported, False otherwise. + overridePluginsAreSupported: + returns: True if override plugins are supported, False otherwise. + readPluginLists: + __doc__: + args: + plugin_list: + writePluginLists: + __doc__: + args: + plugin_list: + + GuessedString: + __doc__: | + Represents a string that may be set from different places. Each time the value is + changed a "quality" is specified to say how probable it is the value is the best choice. + Only the best choice should be used in the end but alternatives can be queried. This + class also allows a filter to be set. If a "guess" doesn't pass the filter, it is ignored. + + __init__.1: + __doc__: Creates a GuessedString with no associated value. + + __init__.2: + __doc__: Creates a GuessedString with the given value and quality. + args: + value: Initial value of the GuessedString. + quality: Quality of the initial value. + + reset.1: + __doc__: Reset this GuessedString to an invalid state. + returns: This GuessedString object. + + reset.2: + __doc__: | + Reset this GuessedString object with the given value and quality, only + if the given quality is better than the current one. + args: + value: New value for this GuessedString. + quality: Quality of the new value. + returns: This GuessedString object. + + reset.3: + __doc__: | + Reset this GuessedString object by copying the given one, only + if the given one has better quality. + args: + other: The GuessedString to copy. + returns: This GuessedString object. + + setFilter: + __doc__: | + Set the filter for this GuessedString. + + The filter is applied on every `update()` and can reject the new value + altogether or modify it (by returning a new value). + args: + filter: The new filter. + + update.1: + __doc__: | + Update this GuessedString by adding the given value to the list of variants + and setting the actual value without changing the current quality of this + GuessedString. + + The GuessedString is only updated if the given value passes the filter. + args: + value: The new value for this string. + returns: This GuessedString object. + + update.2: + __doc__: | + Update this GuessedString by adding a new variants with the given quality. + + If the specified quality is better than the current one, the actual value of + the GuessedString is also updated. + + The GuessedString is only updated if the given value passes the filter. + args: + value: The new variant to add. + quality: The quality of the variant. + returns: This GuessedString object. + + variants: + returns: The list of variants for this GuessedString. + + IDownloadManager: + + downloadPath: + __doc__: Retrieve the (absolute) path of the specified download. + args: + id: ID of the download. + returns: | + The absolute path to the file corresponding to the given download. This file + may not exist yet if the download is incomplete. + + startDownloadNexusFile: + __doc__: | + Download a file from www.nexusmods.com/. is always the game + currently being managed. + args: + mod_id: ID of the mod to download the file from. + file_id: ID of the file to download. + returns: An ID identifying the download. + + startDownloadURLs: + __doc__: | + Download a file by url. + + The list can contain alternative URLs to allow the download manager to switch + in case of download problems + args: + urls: List of urls to download from. + returns: An ID identifying the download. + + onDownloadComplete: + __doc__: Installs a handler to be called when a download completes. + args: + callback: | + The function to be called when a download complete. The parameter is the download ID. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onDownloadPaused: + __doc__: Installs a handler to be called when a download is paused. + args: + callback: | + The function to be called when a download is paused. The parameter is the download ID. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onDownloadFailed: + __doc__: Installs a handler to be called when a download fails. + args: + callback: | + The function to be called when a download fails. The parameter is the download ID. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onDownloadRemoved: + __doc__: Installs a handler to be called when a download is removed. + args: + callback: | + The function to be called when a download is removed. The parameter is the download ID. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + IFileTree: + __doc__: | + Interface to classes that provides way to visualize and alter file trees. The tree + may not correspond to an actual file tree on the disk (e.g., inside an archive, + from a QTree Widget, ...). + + Read-only operations on the tree are thread-safe, even when the tree has not been populated + yet. + + In order to prevent wrong usage of the tree, implementing classes may throw + UnsupportedOperationException if an operation is not supported. By default, all operations + are supported, but some may not make sense in many situations. + + The goal of this is not reflect the change made to a IFileTree to the disk, but child + classes may override relevant methods to do so. + + The tree is built upon FileTreeEntry. A given tree holds shared pointers to its entries + while each entry holds a weak pointer to its parent, this means that the descending link + are strong (shared pointers) but the uplink are weak. + + Accessing the parent is always done by locking the weak pointer so that returned pointer + or either null or valid. This structure implies that as long as the initial root lives, + entry should not be destroyed, unless the entry are detached from the root and no shared + pointers are kept. + + However, it is not guarantee that one can go up the tree from a single node entry. If the + root node is destroyed, it will not be possible to go up the tree, even if we still have + a valid shared pointer. + + InsertPolicy: + __doc__: + FAIL_IF_EXISTS: Operation will fail if the destination already exists. + REPLACE: | + If the destination exists, it will be replaced (even if the source is a file and + the destination a directory). + MERGE: | + If the destination exists, and the source and destination are of the same type (e.g. + two files or two folders), a merge is performed: if both entries are files, the source + replaces the destination, otherwise the source is merged into the destination. If the + destination exists but the source and the destination are of different type, the + operation fails. + + WalkReturn: + __doc__: | + Enumeration that can be returned by the callback for the `walk()` method to stop the + walking operation early. + CONTINUE: Continue walking normally. + STOP: Stop the walking operation. + SKIP: Skip this folder (no effect if the entry is a file). + + __bool__: + returns: True if this tree is not empty, False otherwise. + + __getitem__: + __doc__: Retrieve the entry at the given index in this tree. + args: + index: Index of the entry to retrieve, must be less than the size. + returns: The entry at the given index. + raises: + IndexError: If the given index is not in range for this tree. + + __iter__: + __doc__: | + Retrieves an iterator for entries directly under this tree. + + This method does not recurse into subtrees, see `walk()` for this. + + # Force the list of arguments to be empty (this will still include self): + args: + + # We are forcing the return type because parsing C++ iterators is a pain: + returns: + __doc__: An iterator object that can be used to iterate over entries in this tree. + type: "Iterator[FileTreeEntry]" + + __len__: + returns: The number of entries directly under this tree. + + addDirectory: + __doc__: | + Create a new directory tree under this tree. + + This method will create missing folders in the given path and will + not fail if the directory already exists but will fail if the given + path contains "." or "..". + This method invalidates iterators to this tree and all the subtrees + present in the given path. + args: + path: Path to the directory to create. + returns: An IFileTree corresponding to the created directory. + raises: + RuntimeError: If the directory could not be created. + + addFile: + __doc__: | + Create a new file directly under this tree. + + This method will fail if the file already exists and `replace_if_exists` is `False`. + This method invalidates iterators to this tree and all the subtrees present in the + given path. + args: + path: Path to the file to create. + replace_if_exists: | + If True and an entry already exists at the given location, it will be replaced by + a new entry. This will replace both files and directories. + returns: A FileTreeEntry corresponding to the created file. + raises: + RuntimeError: If the file could not be created. + + clear: + __doc__: | + Delete (detach) all the entries from this tree. + + This method will go through the entries in this tree and stop at the first + entry that cannot be deleted, this means that the tree can be partially cleared. + returns: True if all entries have been detached, False otherwise. + + copy: + __doc__: | + Move the given entry to the given path under this tree. + + The entry must not be a parent tree of this tree. This method can also be used + to rename entries. + + If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry + at the same location already exists. If the policy is REPLACE, an existing + entry will be replaced. If MERGE, the entry will be merged with the existing + one (if the entry is a file, and a file exists, the file will be replaced). + + This method invalidates iterator to this tree, to the parent tree of the given + entry, and to subtrees of this tree if the insert policy is MERGE. + args: + entry: Entry to copy. + path: | + 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. + 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. + + createOrphanTree: + __doc__: Create a new orphan empty tree. + args: + name: Name of the tree. + returns: A new tree without any parent. + + exists: + __doc__: Check if the given entry exists. + args: + path: Path to the entry, separated by / or \\. + type: The type of the entry to check. + returns: True if the entry was found, False otherwise. + + find: + __doc__: | + Retrieve the given entry. + + If no entry exists at the given path, or if the entry is not of the right + type, `None` is returned. + args: + path: Path to the entry, separated by / or \\. + type: The type of the entry to check. + returns: + __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]] + + insert: + __doc__: | + Insert the given entry in this tree, removing it from its + previous parent. + + The entry must not be this tree or a parent entry of this tree. + + - If the insert policy if `FAIL_IF_EXISTS`, the call will fail if an entry + with the same name already exists. + - If the policy is `REPLACE`, an existing entry will be replaced by the given entry. + - If the policy is `MERGE`: + + - If there is no entry with the same name, the new entry is inserted. + - If there is an entry with the same name: + + - If both entries are files, the old file is replaced by the given entry. + - If both entries are directories, a merge is performed as if using merge(). + - Otherwise the insertion fails (two entries with different types). + + This method invalidates iterator to this tree, to the parent tree of the given + entry, and to subtrees of this tree if the insert policy is MERGE. + args: + entry: Entry to insert. + policy: Policy to use to resolve conflicts. + returns: True if the entry was insert, False otherwise. + + merge: + __doc__: | + Merge the given tree with this tree, i.e., insert all entries + of the given tree into this tree. + + The tree must not be this tree or a parent entry of this tree. Files present in both tree + will be replaced by files in the given tree. After a merge, the source tree will be + empty but still attached to its parent. + + If `overwrites` is `True`, a map from overridden files to new files will be returned. + + Note that the merge process makes no distinction between files and directories + when merging: if a directory is present in this tree and a file from source + is in conflict with it, the tree will be removed and the file inserted; if a file + is in this tree and a directory from source is in conflict with it, the file will + be replaced with the directory. + + This method invalidates iterators to this tree, all the subtrees under this tree + present in the given path, and all the subtrees of the given source. + args: + other: Tree to merge. + overwrites: If True, a mapping from overridden files to new files will be returned. + returns: | + If `overwrites` is True, a mapping from overridden files to new files, otherwise + the number of overwritten entries. + raises: + RuntimeError: If the merge failed. + + move: + __doc__: | + Move the given entry to the given path under this tree. + + The entry must not be a parent tree of this tree. This method can also be used + to rename entries. + + If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry + at the same location already exists. If the policy is REPLACE, an existing + entry will be replaced. If MERGE, the entry will be merged with the existing + one (if the entry is a file, and a file exists, the file will be replaced). + + This method invalidates iterator to this tree, to the parent tree of the given + entry, and to subtrees of this tree if the insert policy is MERGE. + args: + entry: Entry to move. + path: | + The path to move the entry to. If the path ends with / or \\, the entry will + be inserted in the corresponding directory instead of replacing it. If the + given path is empty (`""`), this is equivalent to `insert()`. + policy: Policy to use to resolve conflicts. + returns: True if the entry was moved correctly, False otherwise. + + pathTo: + __doc__: Retrieve the path from this tree to the given entry. + args: + entry: The entry to reach, must be in this tree. + sep: The type of separator to use to create the path. + returns: | + The path from this tree to the given entry, including the name of the entry, or + an empty string if the given entry was not found under this tree. + + remove.1: + __doc__: | + Delete the entry with the given name. + + This method does not recurse into subtrees, so the entry should be + accessible directly from this tree. + args: + name: Name of the entry to delete. + returns: True if the entry was deleted, False otherwise. + + remove.2: + __doc__: Delete the given entry. + args: + entry: Entry to delete. The entry must belongs to this tree (and not to a subtree). + returns: True if the entry was deleted, False otherwise. + + removeAll: + __doc__: | + Delete the entries with the given names from the tree. + + This method does not recurse into subtrees, so only entries accessible + directly from this tree will be removed. This method invalidates iterators. + args: + names: Names of the entries to delete. + returns: The number of deleted entry. + + removeIf: + __doc__: | + Delete entries matching the given predicate from the tree. + + This method does not recurse into subtrees, so only entries accessible + directly from this tree will be removed. This method invalidates iterators. + args: + filter: Predicate that should return true for entries to delete. + returns: The number of deleted entry. + + walk: + __doc__: | + Walk this tree, calling the given function for each entry in it. + + The given callback will be called with two parameters: the path from this tree to the given entry + (with a trailing separator, not including the entry name), and the actual entry. The method returns + a `WalkReturn` object to indicates what to do. + args: + callback: Method to call for each entry in the tree. + sep: Type of separator to use to construct the path. + + IInstallationManager: + __doc__: + + createFile: + __doc__: | + Create a new file on the disk corresponding to the given entry. + + This method can be used by installer that needs to create files that are not in the original + archive. At the end of the installation, if there are entries in the final tree that were used + to create files, the corresponding files will be moved to the mod folder. + + Temporary files corresponding to created files are automatically cleaned up at the end of + the installation. + args: + entry: The entry for which a temporary file should be created. + returns: The path to the created file, or an empty string if the file could not be created. + + extractFile: + __doc__: | + Extract the specified file from the currently opened archive to a temporary + location. + + This method cannot be used to extract directory. + + The call will fail with an exception if no archive is open (plugins deriving from + IPluginInstallerSimple can rely on that, custom installers should not). The temporary + file is automatically cleaned up after the installation. This call can be very slow + if the archive is large and "solid". + args: + entry: Entry corresponding to the file to extract. + silent: If true, the dialog showing extraction progress will not be shown. + returns: | + The absolute path to the temporary file, or an empty string if the file was not extracted. + + extractFiles: + __doc__: | + Extract the specified files from the currently opened archive to a temporary + location. + + This method cannot be used to extract directories. + + The call will fail with an exception if no archive is open (plugins deriving from + IPluginInstallerSimple can rely on that, custom installers should not). The temporary + files are automatically cleaned up after the installation. This call can be very slow + if the archive is large and "solid". + args: + entries: Entries corresponding to the files to extract. + silent: If true, the dialog showing extraction progress will not be shown. + returns: A list containing absolute paths to the temporary files. + + getSupportedExtensions: + returns: The extensions of archives supported by this installation manager. + + installArchive: + __doc__: Install the given archive. + args: + mod_name: Suggested name of the mod. + archive: Path to the archive to install. + mod_id: ID of the mod, if available. + returns: The result of the installation. + + IModInterface: + + absolutePath: + returns: Absolute path to the mod to be used in file system operations. + + isBackup: + returns: True if this mod represents a backup. + + isForeign: + returns: True if this mod represents a foreign mod, not managed by MO2. + + isOverwrite: + returns: True if this mod represents the overwrite mod. + + isSeparator: + returns: True if this mod represents a separator. + + name: + returns: The name of this mod. + + comments: + returns: The comments for this mod, if any. + + notes: + returns: The notes for this mod, if any. + + gameName: + __doc__: | + Retrieve the short name of the game associated with this mod. This may differ + from the current game plugin (e.g. you can install a Skyrim LE game in a SSE + installation). + + returns: The name of the game associated with this mod. + + repository: + returns: The name of the repository from which this mod was installed. + + nexusId: + returns: The Nexus ID of this mod. + + version: + returns: The current version of this mod. + + newestVersion: + returns: | + The newest version of this mod (as known by MO2). If this matches version(), + then the mod is up-to-date. + + ignoredVersion: + returns: | + The ignored version of this mod (for update), or an invalid version if the user + did not ignore version for this mod. + + installationFile: + returns: The absolute path to the file that was used to install this mod. + + converted: + __doc__: | + Check if the mod was marked as converted by the user. + + When a mod is for a different game, a flag is shown to users to warn them, but + they can mark mods as converted to remove this flag. + + returns: True if this mod was marked as converted by the user. + + validated: + __doc__: | + Check if the mod was marked as validated by the user. + + MO2 uses ModDataChecker to check the content of mods, but sometimes these fail, in + which case mods are incorrectly marked as 'not containing valid games data'. Users can + choose to mark these mods as valid to hide the warning / flag. + + returns: True if th is mod was marked as containing valid game data. + + color: + returns: The color of the 'Notes' column chosen by the user. + + url: + returns: | + The URL of this mod, or an empty QString() if no URL is associated + with this mod. + + primaryCategory: + returns: The ID of the primary category of this mod. + + categories: + returns: The list of categories this mod belongs to. + + trackedState: + returns: The tracked state of this mod. + + endorsedState: + returns: The endorsement state of this mod. + + fileTree: + __doc__: | + Retrieve a file tree corresponding to the underlying disk content of this mod. + + The file tree should not be cached by plugins since it is already and updated when + required. + returns: A file tree representing the content of this mod. + + addCategory: + __doc__: Assign a category to the mod. If the named category does not exist it is created. + args: + name: Name of the new category to assign. + + addNexusCategory: + __doc__: | + Set the category id from a nexus category id. Conversion to MO ID happens internally. + + If a mapping is not possible, the category is set to the default value. + args: + category_id: The Nexus category ID. + + removeCategory: + __doc__: Unassign a category from this mod. + args: + name: Name of the category to remove. + returns: | + True if the category was removed, False otherwise (e.g. if no such category + was assigned). + + setGameName: + __doc__: Set the source game of this mod. + args: + name: The new source game short name of this mod. + + setIsEndorsed: + __doc__: Set endorsement state of the mod. + args: + endorsed: New endorsement state of this mod. + + setNewestVersion: + __doc__: Set the latest known version of this mod. + args: + version: The latest known version of this mod. + + setNexusID: + __doc__: Set the Nexus ID of this mod. + args: + nexus_id: Thew new Nexus ID of this mod. + + setUrl: + __doc__: Set the URL of this mod. + args: + url: The URL of this mod. + + setVersion: + __doc__: Set the version of this mod. + args: + version: The new version of this mod. + + pluginSetting: + __doc__: Retrieve the specified setting in this mod for a plugin. + + args: + plugin_name: | + Name of the plugin for which to retrieve a setting. This should always be `IPlugin.name()` + unless you have a really good reason to access settings of another plugin. + key: Identifier of the setting. + default: The default value to return if the setting does not exist. + + returns: The setting, if found, or the default value. + + pluginSettings: + __doc__: Retrieve the settings in this mod for a plugin. + + args: + plugin_name: | + Name of the plugin for which to retrieve settings. This should always be `IPlugin.name()` + unless you have a really good reason to access settings of another plugin. + + returns: A map from setting key to value. The map is empty if there are not settings for this mod. + + setPluginSetting: + __doc__: Set the specified setting in this mod for a plugin. + + args: + plugin_name: | + Name of the plugin for which to retrieve a setting. This should always be `IPlugin.name()` + unless you have a really good reason to access settings of another plugin. + key: Identifier of the setting. + value: New value for the setting to set. + + returns: True if the setting was set correctly, False otherwise. + + clearPluginSettings: + __doc__: Remove all the settings of the specified plugin this mod. + + args: + plugin_name: | + Name of the plugin for which settings should be removed. This should always be `IPlugin.name()` + unless you have a really good reason to access settings of another plugin. + + returns: The old settings from the given plugin, as returned by `pluginSettings()`. + + IModList: + __doc__: | + Interface to the mod-list. + + All api functions in this interface work need the internal name of a mod to find a + mod. For regular mods (mods the user installed) the display name (as shown to the user) + and internal name are identical. For other mods (non-MO mods) there is currently no way + to translate from display name to internal name because the display name might not me un-ambiguous. + + allMods: + returns: A list containing the internal names of all installed mods. + + allModsByProfilePriority: + returns: The list of mod (names), sorted according to the current profile priorities. + + displayName: + __doc__: | + Retrieve the display name of a mod from its internal name. + + If you received an internal name from the API (e.g. `IPluginList.origin`) then you should use + that name to identify the mod in all other api calls but use this function to retrieve the name + to show to the user. + args: + name: Internal name of the mod. + returns: The display name of the given mod. + + getMod: + __doc__: Retrieve an interface to a mod using its name. + args: + name: Name of the mod to retrieve. + returns: An interface to the given mod, or `None` if there is no mod with this name. + + removeMod: + __doc__: Remove a mod (from disc and from the UI). + args: + mod: The mod to remove. + returns: True if the mod was removed, False otherwise. + + onModInstalled: + __doc__: Install a new handler to be called when a new mod is installed. + args: + callback: | + The function to call when a mod is installed. The parameter of the function is the name of the + newly installed mod. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onModRemoved: + __doc__: Install a new handler to be called when a mod is removed. + args: + callback: | + The function to call when a mod is removed. The parameter of the function is the name of the + removed mod. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onModMoved: + __doc__: Install a handler to be called when a mod is moved. + args: + callback: | + The function to call when a mod is moved. The first argument is the internal name of the + mod, the second argument the old priority and the third argument the new priority. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onModStateChanged.1: + __doc__: Install a handler to be called when a mod state changes (enabled/disabled, endorsed, ...). + deprecated: true + args: + callback: | + The function to call when the state of a mod changes. The first argument is the internal + mod name, and the second one the new state of the mod. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onModStateChanged.2: + __doc__: Install a handler to be called when mod states change (enabled/disabled, endorsed, ...). + args: + callback: | + The function to call when the states of mod change. The argument is a map containing the + mods whose states have changed. Keys are internal mod names and values are mod states. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + priority: + __doc__: Retrieve the priority of a mod. + args: + name: Internal name of the mod. + returns: The priority of the given mod. + + renameMod: + __doc__: | + Rename the given mod. + + This method usually invalidates the given mod so you should use the returned value + after calling it instead of the passed value. + + args: + mod: The mod to rename. + name: The new name of the mod. + + returns: A valid reference to the given mod after renaming it. + + setActive.1: + __doc__: | + Enable or disable a list of mods. + + Calling this will cause MO to re-evaluate its virtual file system so this is + a fairly expensive call. + args: + names: Internal names of the mod to enable or disable. + active: True to enable the mods, False to disable them. + returns: True on success, False otherwise. + + setActive.2: + __doc__: | + Enable or disable a mod. + + Calling this will cause MO to re-evaluate its virtual file system so this is + a fairly expensive call. + args: + name: Internal name of the mod to enable or disable. + active: True to enable the mod, False to disable it. + returns: True on success, False otherwise. + + setPriority: + __doc__: | + Change the priority of a mod. + + `priority` is the new priority after the move. Keep in mind that the mod disappears from its + old location and all mods with higher priority than the moved mod decrease in priority by one. + args: + name: Internal name of the mod. + priority: The new priority of the mod. + returns: | + True if the priority was changed, False otherwise (if the name or priority were invalid). + + state: + __doc__: Retrieve the state of a mod. + args: + name: Internal name of the mod. + returns: The state of the given mod. + + IModRepositoryBridge: + + __bases__: + - PyQt6.QtCore.QObject + + signals[]: + descriptionAvailable: + __doc__: | + Emitted when the description for a mod is reported by the repository. + + Valid keys in `result_data` might change as the repository page is updated. For nexus, the following + keys are valid as of this writing: + 'allow_view', 'ip', 'one_week_ratings', 'date', 'pm_notify', 'OLD_mid', 'OLD_u_downloads', 'game_id', 'OLD_perm_use', 'mod_page_uri', + 'allow_topics', 'has_hot_image', 'id', 'two_weeks_ratings', 'description', 'lastupdate', 'perm_convert', 'author', 'OLD_image', + 'translation_of', 'OLD_mname', 'version', 'allow_rating', 'perm_useinstructions', 'featured_count', 'donate', 'type', 'perm_credits', + 'hidden_reason', 'OLD_views', 'perm_upload', 'has_back_image', 'adult', 'allow_images', 'OLD_endorsements', 'OLD_size', 'name', + 'commenting', 'moderate', 'language', 'perm_others', 'lastcomment', 'OLD_readme', 'summary', 'perm_modify', 'OLD_downloads', + 'lock_comments', 'suggested_category', 'allow_tagging', 'published', 'perm_notes', 'category_id', 'thread_id', 'perm_use', 'wizard_steps' + + This interface is going to be changed at some point to replace resultData with a less "dynamic" data structure. + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: Dict[str, MoVariant] + desc: The data included in the response. + + filesAvailable: + __doc__: Emitted when the list of files for a mod is reported by the repository. + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: List[ModRepositoryFileInfo] + desc: List of file information objects. + + fileInfoAvailable: + __doc__: | + Emitted when information about a file is reported by the repository. + + Valid keys in `result_data` might change as the repository page is updated. For nexus, the following + keys are valid as of this writing: + 'count', 'requirements_alert', 'u_count', 'description', 'uri', 'size', 'owner_id', 'primary', 'manager', + 'version', 'date', 'game_id', 'mod_id', 'category_id', 'id', 'name' + + If you intend to download this file you do not have to request this information manually, you can call + IDownloadManager.startDownloadNexusFile() and let the download manager figure things out. + + This interface is going to be changed at some point to replace resultData with a less "dynamic" data structure. + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: Dict[str, MoVariant] + desc: The data included in the response. + + downloadURLsAvailable: + __doc__: | + Emitted when the list of download urls for a file is returned by the repository. + + This interface is going to be changed at some point to replace resultData with a less "dynamic" data structure. + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + file_id: + type: int + desc: ID of the file for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: Dict[str, MoVariant] + desc: The data included in the response. + + endorsementsAvailable: + __doc__: Emitted when the endorsement data is returned from the API. + args: + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: bool + desc: The new endorsement state. + + endorsementToggled: + __doc__: Emitted when the endorsement state of a mod was changed (only sent as a result of our request). + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: bool + desc: The new endorsement state. + + trackedModsAvailable: + __doc__: Emitted when the tracked mod data is returned from the API. + args: + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: bool + desc: The new tracking state as a list of maps with keys (domain_name, mod_id). + + trackingToggled: + __doc__: Emitted when the tracking state of a mod was changed (only sent as a result of our request). + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + user_data: + type: MoVariant + desc: The data that was included in the request. + result_data: + type: bool + desc: The new tracking state. + + requestFailed: + __doc__: Emitted when a Nexus request failed. + args: + game_name: + type: str + desc: Name of the game. + mod_id: + type: int + desc: ID of the mod for which the request was made. + file_id: + type: int + desc: ID of the file for which the request was made (ignore if the request was for a mod in general). + user_data: + type: MoVariant + desc: The data that was included in the request. + error: + type: PyQt6.QtNetwork.QNetworkReply.NetworkError + desc: The actual error. + message: + type: str + desc: Textual description of the error. + + _object: + returns: The underlying `QObject` for the bridge. + + requestDescription: + __doc__: Request description of a mod. + args: + game_name: Name of the game containing the mod. + mod_id: Nexus ID of the mod. + user_data: User data to be returned with the result. + + requestDownloadURL: + __doc__: Request download URL for mod file.0 + args: + game_name: Name of the game containing the mod. + mod_id: Nexus ID of the mod. + file_id: ID of the file for which a URL should be returned. + user_data: User data to be returned with the result. + + requestFileInfo: + __doc__: + args: + game_name: Name of the game containing the mod. + mod_id: Nexus ID of the mod. + file_id: ID of the file for which information is requested. + user_data: User data to be returned with the result. + + requestFiles: + __doc__: Request the list of files belonging to a mod. + args: + game_name: Name of the game containing the mod. + mod_id: Nexus ID of the mod. + user_data: User data to be returned with the result. + + requestToggleEndorsement: + __doc__: + args: + game_name: Name of the game containing the mod. + mod_id: Nexus ID of the mod. + mod_version: Version of the mod. + endorse: + user_data: User data to be returned with the result. + + IOrganizer: + __doc__: | + Interface to class that provides information about the running session + of Mod Organizer to be used by plugins. + + getPluginDataPath: + returns: The directory for plugin data, typically plugins/data. + + appVersion: + returns: The running version of Mod Organizer. + + basePath: + returns: The absolute path to the base directory of Mod Organizer. + + createMod: + __doc__: | + Create a new mod with the specified name. + + If a mod with the same name already exists, the user will be queried. If the user chooses + to merge or replace, the call will succeed, otherwise the call will fail. + args: + name: Name of the mod to create. + returns: | + An interface to the newly created mod that can be used to modify it, or `None` if the mod + could not be created. + + createNexusBridge: + __doc__: Create a new Nexus interface. + returns: The newly created Nexus interface. + + downloadManager: + returns: The interface to the download manager. + + downloadsPath: + returns: The absolute path to the download directory. + + findFileInfos: + __doc__: Find files in the virtual directory matching the specified filter. + args: + path: The path to search in (relative to the 'data' folder). + filter: The function to use to filter files. Should return True for the files to keep. + returns: The list of `QFileInfo` corresponding to the matching files. + + findFiles.1: + __doc__: Find files in the given folder that matches the given filter. + args: + path: The path to search in (relative to the 'data' folder). + filter: The function to use to filter files. Should return True for the files to keep. + returns: The list of matching files. + + findFiles.2: + __doc__: Find files in the given folder that matches one of the given glob patterns. + args: + path: The path to search in (relative to the 'data' folder). + patterns: List of glob patterns to match against. + returns: The list of matching files. + + findFiles.3: + __doc__: Find files in the given folder that matches the given glob pattern. + args: + path: The path to search in (relative to the 'data' folder). + pattern: The glob pattern to use to filter files. + returns: The list of matching files. + + getFileOrigins: + __doc__: | + Retrieve the file origins for the specified file. + + The origins are listed with their internal name. The internal name of a mod can differ + from the display name for disambiguation. + args: + filename: Path to the file to retrieve origins for (relative to the 'data' folder). + returns: The list of origins that contain the specified file, sorted by their priority. + + getGame: + __doc__: Retrieve the game plugin matching the given name. + args: + name: Name of the game (short name). + returns: The plugin for the given game, or `None` if none was found. + + installMod: + __doc__: Install a mod archive at the specified location. + args: + filename: Absolute filepath to the archive to install. + name_suggestion: Suggested name for this mod. This can still be changed by the user. + returns: An interface to the new installed mod, or `None` if no installation took place (canceled or failure). + + isPluginEnabled.1: + __doc__: Check if a plugin is enabled. + args: + plugin: The plugin to check. + returns: True if the plugin is enabled, False otherwise. + + isPluginEnabled.2: + __doc__: Check if a plugin is enabled. + args: + plugin: The name of the plugin to check. + returns: True if the plugin is enabled, False otherwise. + + listDirectories: + __doc__: Retrieve the list of (virtual) subdirectories in the given path. + args: + directory: Path to the directory to list (relative to the 'data' folder). + returns: The list of directories in the given directory. + + managedGame: + returns: The plugin corresponding to the current game. + + modDataChanged: + __doc__: Notify the organizer that the given mod has changed. + args: + mod: The mod that has changed. + + modList: + returns: The interface to the mod list. + + modsPath: + returns: The (absolute) path to the mods directory. + + onAboutToRun: + __doc__: | + Install a new handler to be called when an application is about to run. + + Multiple handlers can be installed. If any of the handler returns `False`, the application will + not run. + args: + callback: | + The function to call when an application is about to run. The parameter is the absolute path + to the application to run. The function can return False to prevent the application from running. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onFinishedRun: + __doc__: Install a new handler to be called when an application has finished running. + args: + callback: | + The function to call when an application has finished running. The first parameter is the absolute + path to the application, and the second parameter is the exit code of the application. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginDisabled.1: + __doc__: Install a new handler to be called when a plugin is disabled. + args: + callback: | + The function to call when a plugin is disabled. The parameter is the plugin being disabled. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginDisabled.2: + __doc__: Install a new handler to be called when the given plugin is disabled. + args: + name: Name of the plugin to watch. + callback: | + The function to call when the plugin is disabled. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginEnabled.1: + __doc__: Install a new handler to be called when a plugin is enabled. + args: + callback: | + The function to call when a plugin is enabled. The parameter is the plugin being enabled. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginEnabled.2: + __doc__: Install a new handler to be called when the given plugin is enabled. + args: + name: Name of the plugin to watch. + callback: | + The function to call when the plugin is enabled. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginSettingChanged: + __doc__: Install a new handler to be called when a plugin setting is changed. + args: + callback: | + The function to call when a plugin setting is changed. The parameters are: The name of the plugin, the + name of the setting, the old value (or `None` if the setting did not exist before) and the new value + of the setting (or `None` if the setting has been removed). + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onProfileCreated: + __doc__: | + Install a new handler to be called when a new profile is created. + args: + callback: | + The function to call when a new profile is created. The parameter is the new profile (can be + a temporary object and should not be stored). + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onProfileRenamed: + __doc__: | + Install a new handler to be called when a profile is renamed. + args: + callback: | + The function to call when a profile is renamed. The first parameter is the profile being renamed, + the second parameter the previous name and the third parameter the new name. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onProfileRemoved: + __doc__: | + Install a new handler to be called when a profile is remove. + + The callbacks are called after the profile has been removed so the profile is not accessible + anymore. + args: + callback: | + The function to call when a profile is remove. The parameter is the name of the profile that was + removed. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onProfileChanged: + __doc__: | + Install a new handler to be called when the current profile is changed. + + The function is called when the profile is changed but some operations related to + the profile might not be finished when this is called (e.g., the virtual file system + might not be up-to-date). + args: + callback: | + The function to call when the current profile is changed. The first parameter is the old profile (can + be `None`, e.g. at startup), and the second parameter is the new profile (cannot be `None`). + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onUserInterfaceInitialized: + __doc__: Install a new handler to be called when the UI has been fully initialized. + args: + callback: | + The function to call when the user-interface has been fully initialized. The parameter is the main + window of the application (`QMainWindow`). + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + overwritePath: + returns: The (absolute) path to the overwrite directory. + + persistent: + __doc__: | + Retrieve the specified persistent value for a plugin. + + A persistent is an arbitrary value that the plugin can set and retrieve that is persistently stored + by the main application. There is no UI for the user to change this value but they can directly access + the storage + args: + plugin_name: | + Name of the plugin for which to retrieve the value. This should always be `IPlugin.name()` unless you have a + really good reason to access data of another mod AND if you can verify that plugin is actually installed. + key: Identifier of the setting. + default: Default value to return if the key is not set (yet). + returns: The value corresponding to the given persistent setting, or `def` is the key is not found. + + pluginDataPath: + __doc__: | + Retrieve the path to a directory where plugin data should be stored. + + For python plugins, it is recommended to use a dedicated folder (per plugin) if you need to + store data (resources, or multiple python files). + returns: Path to a directory where plugin data should be stored. + + pluginList: + returns: The plugin list interface. + + pluginSetting: + __doc__: Retrieve settings of plugins. + args: + plugin_name: Name of the plugin to retrieve the setting for. + key: Name of the setting to retrieve the value for. + returns: The value of the setting. + + profile: + returns: The interface to the current profile. + + profileName: + returns: The name of the current profile, or an empty string if no profile has been loaded (yet). + + profilePath: + returns: The absolute path to the active profile or an empty string if no profile has been loaded (yet). + + refresh: + __doc__: | + Refresh the internal mods file structure from disk. This includes the mod list, the plugin + list, data tab and other smaller things like problems button (same as pressing F5). + + The main part of the refresh of the mods file structure, mod list and plugin list is done + asynchronously, so you should not expect them to be up-to-date when this function returns. + args: + save_changes: If True, the relevant profile information is saved first (enabled mods and order of mods). + + resolvePath: + __doc__: Resolves a path relative to the virtual data directory to its absolute real path. + args: + filename: Path to resolve. + returns: The absolute real path, or an empty string if the path was not found. + + setPersistent: + __doc__: | + Set the specified persistent value for a plugin. + + This does not update the in-memory value for this setting, see `setPluginSetting()` for this. + args: + plugin_name: | + Name of the plugin for which to change a value. This should always be `IPlugin.name()` unless you have a + really good reason to access data of another mod AND if you can verify that plugin is actually installed. + key: Identifier of the setting. + value: New value for the setting. + sync: If True, the storage is immediately written to disc. This costs performance but is safer against data loss. + + setPluginSetting: + __doc__: | + Set the specified setting for a plugin. + + This automatically notify handlers register with `onPluginSettingChanged`, so you do not have to do it yourself. + args: + plugin_name: | + Name of the plugin for which to change a value. This should always be `IPlugin.name()` unless you have a + really good reason to access data of another mod AND if you can verify that plugin is actually installed. + key: Identifier of the setting. + value: New value for the setting. + + startApplication: + __doc__: Starts an application with virtual filesystem active. + args: + executable: | + Name or path of the executable. If this is only a filename, it will only work if it has been configured + in MO as an executable. If it is a relative path it is expected to be relative to the game directory. + args: | + Arguments to pass to the executable. If the list is empty, and `executable` refers to a configured executable, + the configured arguments are used. + cwd: | + The working directory for the executable. If this is empty, the path to the executable is used unless `executable` + referred to a configured MO executable, in which case the configured cwd is used. + profile: Profile to use. If this is empty (the default) the current profile is used. + forcedCustomOverwrite: The mod to set as the custom overwrite, regardless of what the profile has configured. + ignoreCustomOverwrite: Set to true to ignore the profile's configured custom overwrite. + returns: The handle to the started application, or 0 if the application failed to start. + + virtualFileTree: + __doc__: Retrieve a IFileTree object representing the virtual file tree. + returns: An IFileTree representing the virtual file tree. + + waitForApplication: + __doc__: | + Wait for the application corresponding to the given handle to finish. + + This will always show the lock overlay, regardless of whether the + user has disabled locking in the setting, so use this with care. + Note that the lock overlay will always allow the user to unlock, in + which case this will return False. + args: + handle: Handle of the application to wait for (as returned by `startApplication()`). + refresh: Whether ModOrganizer should refresh after the process completed or not. + returns: | + A tuple `(result, exitcode)`, where `result` is a boolean indicating if the application + completed successfully, and `exitcode` is the exit code of the application. + + # IOrganizer has a bunch of deprecated functions: + getMod: + deprecated: true + removeMod: + deprecated: true + modsSortedByProfilePriority: + deprecated: true + refreshModList: + deprecated: true + onModInstalled: + deprecated: true + + IPlugin: + __doc__: Base class for all plugins. + + __abstract__: true + + author: + returns: The name of the plugin author. + + description: + returns: The description for this plugin. + + init: + __doc__: | + Initialize this plugin. + + Note that this function may never be called if no `IOrganizer` is available + at that time, such as when creating the first instance in MO. + + Plugins will probably want to store the organizer pointer. It is guaranteed + to be valid as long as the plugin is loaded. + + These functions may be called before `init()`: + + - `name()` + - see `IPluginGame` for more. + + args: + organizer: The main organizer interface. + 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. + + name: + __doc__: | + Retrieve the name of the plugin. + + The name of the plugin is used for internal storage purpose so it should not change, + and it should be static. In particular, you should not use a localized string (`tr()`) + for the plugin name. + + In the future, we will provide a way to localized plugin names using a distinct method, + such as `localizedName()`. + returns: The name of the plugin. + + localizedName: + abstract: false + + __doc__: | + Retrieve the localized name of the plugin. + + Unlike `name()`, this method can (and should!) return a localized name for the plugin. + This method returns name() by default. + + returns: The localized name of the plugin. + + master: + abstract: false + + __doc__: | + Retrieve the master plugin of this plugin. + + It is often easier to implement a functionality as multiple plugins in MO2, but ship the + plugins together, e.g. as a Python module or using `createFunctions()`. In this case, having + a master plugin (one of the plugin, or a separate one) tells MO2 that these plugins are + linked and should also be displayed together in the UI. If MO2 ever implements automatic + updates for plugins, the `master()` plugin will also be used for this purpose. + + returns: | + The master plugin of this plugin, or a null pointer if this plugin does not have a master. + + requirements: + abstract: false + + __doc__: | + Retrieve the requirements for this plugin. + + This method is called right after `init()` and the ownership the requirements is + returns: The list of requirements for this plugin. + + settings: + returns: A list of settings for this plugin. + + version: + returns: The version of this plugin. + + IPluginDiagnose: + __doc__: | + Plugins that create problem reports to be displayed in the UI. + + This can be used to report problems related to the same plugin (which implements further + interfaces) or as a stand-alone diagnosis tool. + + _invalidate: + __doc__: Invalidate the problems corresponding to this plugin. + abstract: false + + activeProblems: + __doc__: | + Retrieve the list of active problems found by this plugin. + + This method returns a list of problem IDs, that are then used when calling other methods + such as `shortDescription()` or `hasGuidedFix()`. + returns: The list of active problems for this plugin. + + fullDescription: + __doc__: Retrieve the full description of the problem corresponding to the given key. + args: + key: ID of the problem. + returns: The full description of the problem. + raises: + IndexError: If the key is not valid. + + hasGuidedFix: + __doc__: Check if the problem corresponding to the given key has a guided fix. + args: + key: ID of the problem. + returns: True if there is a guided fix for the problem, False otherwise. + raises: + IndexError: If the key is not valid. + + shortDescription: + __doc__: Retrieve the short description of the problem corresponding to the given key. + args: + key: ID of the problem. + returns: The short description of the problem. + raises: + IndexError: If the key is not valid. + + startGuidedFix: + __doc__: | + Starts a guided fix for the problem corresponding to the given key. + + This method should throw `ValueError` if there is no guided fix for the corresponding + problem. + args: + key: ID of the problem. + raises: + IndexError: If the key is not valid. + ValueError: If there is no guided fix for this problem. + + IPluginFileMapper: + __doc__: Plugins that adds virtual file links. + + mappings: + returns: Mapping for the virtual file system (VFS). + + IPluginGame: + __doc__: | + Base classes for game plugins. + + Each game requires a specific game plugin. These plugins were initially designed for + Bethesda games, so a lot of methods and attributes are irrelevant for other games. If + you wish to write a plugin for a much simpler game, please consider the `basic_games` + plugin: https://github.com/ModOrganizer2/modorganizer-basic_games + + detectGame: + __doc__: | + Detect the game. + + This method is the first called for game plugins (before `init()`). The following + methods should work properly after the call to `detectGame()` (and before `init()`): + + - gameName() + - isInstalled() + - gameIcon() + - gameDirectory() + - dataDirectory() + - gameVariants() + - looksValid() + + See `IPlugin.init()` for more. + + CCPlugins: + returns: The current list of active Creation Club plugins. + + DLCPlugins: + returns: The list of esp/esm files that are part of known DLCs. + + binaryName: + returns: The name of the default executable to run (relative to the game folder). + + dataDirectory: + returns: The path to the directory containing data (absolute path). + + documentsDirectory: + returns: The directory of the documents folder where configuration files and such for this game reside. + + executableForcedLoads: + returns: A list of automatically discovered libraries that can be force loaded with executables. + + executables: + returns: A list of automatically discovered executables of the game itself and tools surrounding it. + + feature: + __doc__: Retrieve a specified game feature from this plugin. + abstract: false + args: + feature_type: + __doc__: The class of feature to retrieve. + type: Type[GameFeatureType] + returns: + __doc__: | + The game feature corresponding to the given type, or `None` if the feature is + not implemented. + type: GameFeatureType + + featureList: + __doc__: | + Retrieve the list of game features implemented for this plugin. + + Python plugin should not implement this method but `_featureList()`. + abstract: false + returns: + __doc__: A mapping from feature type to actual game features. + type: Dict[Type[GameFeatureType], GameFeatureType] + + gameDirectory: + returns: The directory containing the game installation. + + gameIcon: + returns: The icon representing the game. + + gameName: + returns: The name of the game (as displayed to the user). + + gameNexusName: + returns: The name of the game identifier for Nexus. + + gameShortName: + returns: The short name of the game. + + gameVariants: + __doc__: | + Retrieve the list of variants for this game. + + If there are multiple variants of a game (and the variants make a difference to the + plugin), like a regular one and a GOTY-edition, the plugin can return a list of them + and the user gets to chose which one he owns. + returns: The list of variants of the game. + + gameVersion: + returns: The version of the game. + + getLauncherName: + returns: | + 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 + 'main' INI file. + + initializeProfile: + __doc__: | + Initialize a profile for this game. + + The MO app does not yet support virtualizing only specific aspects but plugins should be written + with this future functionality in mind. + + This function will be used to initially create a profile, potentially to repair it or upgrade/downgrade + it so the implementations have to gracefully handle the case that the directory already contains files. + args: + directory: The directory where the profile is to be initialized. + settings: The parameters for how the profile should be initialized. + + isInstalled: + returns: True if this game has been discovered as installed, False otherwise. + + listSaves: + __doc__: List saves in the given directory. + args: + folder: The folder to list saves from. + returns: The list of game saves in the given folder. + + loadOrderMechanism: + returns: The load order mechanism used by this game. + + looksValid: + __doc__: Check if the given directory looks like a valid game installation. + args: + directory: Directory to check. + returns: True if the directory looks like a valid installation of this game, False otherwise. + + nexusGameID: + __doc__: | + Retrieve the Nexus game ID for this game. + + Example: For Skyrim, the Nexus game ID is 110. + returns: The Nexus game ID for this game. + + nexusModOrganizerID: + __doc__: | + Retrieve the Nexus mod ID of Mod Organizer for this game. + + Example: For Skyrim SE, the mod ID of MO2 is 6194. You can find the mod ID in the URL: + https://www.nexusmods.com/skyrimspecialedition/mods/6194 + returns: The Nexus mod ID of Mod Organizer for this game. + + primaryPlugins: + returns: The list of plugins that are part of the game and not considered optional. + + primarySources: + __doc__: | + Retrieve primary alternative 'short' names for this game. + + This is used to determine if a Nexus (or other) download source should be considered + as a primary source for the game so that it is not flagged as an alternative one. + returns: The list of primary alternative 'short' names for this game, or an empty list. + + savesDirectory: + returns: The directory where save games are stored. + + secondaryDataDirectories: + __doc__: | + Retrieve the list of secondary data directories. Each directories should be + assigned a unique name that differs from "data" which is the name of the main + data directory returned by dataDirectory(). + + returns: A mapping from unique name to secondary data directories. + + setGamePath: + __doc__: | + Set the path to the managed game. + + This is called during instance creation if the game is not auto-detected and the user has + to specify the installation location. This is not called if the game has been auto-detected, + so `isInstalled()` should call this. + args: + path: Path to the game installation. + + setGameVariant: + __doc__: | + Set the game variant. + + If there are multiple variants of game (as returned by `gameVariants()`), this will be + called on start with the user-selected game variant. + args: + variant: The game variant selected by the user. + + sortMechanism: + returns: The sort mechanism for this game. + + steamAPPId: + __doc__: | + Retrieve the Steam app ID for this game. + + If the game is not available on Steam, this should return an empty string. + + If a game is available in multiple versions, those might have different app ids. The plugin + should try to return the right one + returns: The Steam app ID for this game. Should be empty for games not available on steam. + + validShortNames: + __doc__: | + Retrieve the valid 'short' names for this game. + + This is used to determine if a Nexus download is valid for the current game since not all + game variants have their own nexus pages and others can handle downloads from other nexus + game pages and should be allowed to do so (e.g., you can install some Skyrim LE mod even + when using Skyrim SE). + + The short name should be considered the primary handler for a directly supported game + for purposes of auto-launching an instance. + returns: The list of valid short names for this game. + + IPluginInstaller: + __doc__: | + This is the top-level class for installer. Actual installers should inherit either: + + - `IPluginInstallerSimple` if the installer can work directly with the archive. This is what + most installers use. + - `IPluginInstallerCustom` if the installer needs to perform custom operations. This is only + used by the external NCC installer and the OMOD installer. + + isArchiveSupported: + __doc__: Check if the given file tree corresponds to a supported archive for this installer. + args: + tree: The tree representing the content of the archive. + returns: True if this installer can handle the archive, False otherwise. + + isManualInstaller: + __doc__: Check if this installer is a manual installer. + returns: True if this installer is a manual installer, False otherwise. + + onInstallationStart: + abstract: false + __doc__: | + Method calls at the start of the installation process, before any other methods. + This method is only called once per installation process, even for recursive + installations (e.g. with the bundle installer). + + If `reinstallation` is true, then the given mod is the mod being reinstalled (the one + selected by the user). If `reinstallation` is false and `currentMod` is not null, then + it corresponds to a mod MO2 thinks corresponds to the archive (e.g. based on matching Nexus ID + or name). + + The default implementation does nothing. + + args: + archive: Path to the archive that is going to be installed. + reinstallation: True if this is a reinstallation, False otherwise. + current_mod: | + A currently installed mod corresponding to the archive being installed, or None + if there is no such mod. + + onInstallationEnd: + abstract: false + __doc__: | + Method calls at the end of the installation process. This method is only called once + per installation process, even for recursive installations (e.g. with the bundle installer). + + args: + result: The result of the installation. + new_mod: | + If the installation succeeded (result is RESULT_SUCCESS), contains the newly + installed mod, otherwise it contains a null pointer. + + priority: + __doc__: | + Retrieve the priority of this installer. + + If multiple installers are able to handle an archive, the one with the highest priority wins. + returns: The priority of this installer. + + setInstallationManager: + abstract: false + __doc__: | + Set the installation manager for this installer. + + Python plugins usually do not need to re-implement this and can directly access the installation + manager using `_manager()`. + args: + manager: The installation manager. + + setParentWidget: + __doc__: | + Set the parent widget for this installer. + + Python plugins usually do not need to re-implement this and can directly access the parent + widget using `_parentWidget()` once the UI has been initialized. + abstract: false + args: + parent: The parent widget. + + _manager: + abstract: false + returns: The installation manager. + + _parentWidget: + 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. + + The mod needs to be created by calling `IOrganizer.createMod` first. + args: + mod_name: | + Name of the mod to install. As an input parameter this is the suggested name + (e.g. from meta data) The installer may change this parameter to rename the mod). + game_name: Name of the game for which the mod is installed. + archive_name: Name of the archive to install. + version: | + Version of the mod. May be empty if the version is not yet known. The plugin is responsible + for setting the version on the created mod. + nexus_id: | + ID of the mod or -1 if unknown. The plugin is responsible for setting the mod ID for the + created mod. + returns: The result of the installation process. + + isArchiveSupported.1: + __doc__: Check if the given file tree corresponds to a supported archive for this installer. + args: + tree: The tree representing the content of the archive. + returns: True if this installer can handle the archive, False otherwise. + + isArchiveSupported.2: + __doc__: Check if the given file is a supported archive for this installer. + args: + archive_name: Name of the archive. + returns: True if this installer can handle the archive, False otherwise. + + supportedExtensions: + returns: A list of file extensions that this installer can handle. + + IPluginInstallerSimple: + __doc__: | + Simple installer for mods. Simple installers only deal with an in-memory structure + representing the archive and can modify what to install and where by editing this structure. + Actually extracting the archive is handled by the manager. + + install: + __doc__: | + Install a mod from an archive filetree. + + The installer can modify the given tree and use the manager to extract or create new + files. + + This method returns different type of objects depending on the actual result of the + installation. The C++ bindings for this method always returns a tuple (result, tree, + version, id). + args: + name: | + Name of the mod to install. As an input parameter this is the suggested name + (e.g. from meta data) The installer may change this parameter to rename the mod). + tree: In-memory representation of the archive content. + version: Version of the mod, or an empty string is unknown. + nexus_id: ID of the mod, or -1 if unknown. + returns: | + In case of failure, the result of the installation, otherwise the modified tree or + a tuple (result, tree, version, id) containing the result of the installation, the + modified tree, the new version and the new ID. The tuple can be returned even if the + installation did not succeed. + + IPluginList: + + __doc__: Primary interface to the list of plugins. + + isMaster: + deprecated: True + __doc__: | + Check if a plugin is a master file (basically a library, referenced by other plugins). + + In gamebryo games, a master file will usually have a .esm file extension but technically + an esp can be flagged as master and an esm might not be. + args: + name: Filename of the plugin (without path but with file extension). + returns: True if the given plugin is a master plugin, False otherwise or if the file does not exist. + + isMasterFlagged: + __doc__: | + Determine if a plugin is flagged as mater, i.e., a library, reference by + other plugins. + + In gamebryo games, a master file will usually have a .esm file extension but + technically an esp can be flagged as master and an esm might not be. + args: + name: Filename of the plugin (without path but with file extension). + returns: | + True if the given plugin is a master plugin, False otherwise or if the + file does not exist. + + hasMasterExtension: + __doc__: | + Determine if a plugin has a .esm extension. + args: + name: Filename of the plugin (without path but with file extension). + returns: | + True if the given file has a .esm extension, False otherwise or if the + file does not exist. + + isLightFlagged: + __doc__: | + Determine if a plugin is flagged as light + + In gamebryo games, a master file will usually have a .esl file extension but + technically an esp can be flagged as light. + args: + name: Filename of the plugin (without path but with file extension). + returns: | + True if the given plugin is a light plugin, False otherwise or if the + file does not exist. + + hasLightExtension: + __doc__: | + Determine if a plugin has a .esl extension. + args: + name: Filename of the plugin (without path but with file extension). + returns: | + True if the given file has a .esl extension, False otherwise or if the + file does not exist. + + loadOrder: + __doc__: Retrieve the load order of a plugin. + args: + name: Filename of the plugin (without path but with file extension). + returns: | + The load order of the plugin (the order in which the game loads it). If all plugins are enabled this + is the same as the priority but disabled plugins will have a load order of -1. This also returns -1 + if the plugin does not exist. + + masters: + __doc__: Retrieve the list of masters required for a plugin. + args: + name: Filename of the plugin (without path but with file extension). + returns: The list of masters for the plugin (filenames with extension, no path). + + onPluginMoved: + __doc__: Install a new handler to be called when a plugin is moved. + args: + callback: | + The function to call when a plugin is moved. The first parameter is the plugin name, the + second the old priority of the plugin and the third one the new priority. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginStateChanged.1: + __doc__: Install a new handler to be called when a plugin state changes. + deprecated: true + args: + callback: | + The function to call when a plugin state changes. The first parameter is the plugin name, the + second the new state of the plugin. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onPluginStateChanged.2: + __doc__: Install a new handler to be called when plugin states change. + args: + callback: | + The function to call when a plugin states change. The parameter is a map from plugin names to new + plugin states for the plugin whose states have changed. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + onRefreshed: + __doc__: Install a new handler to be called when the list of plugins is refreshed. + args: + callback: The function to call when the list of plugins is refreshed. + returns: True if the handler was installed properly (there are currently no reasons for this to fail). + + origin: + __doc__: | + Retrieve the origin of a plugin. This is either the (internal) name of a mod, `"overwrite"` or `"data"`. + + The internal name of a mod can differ from the display name for disambiguation. + args: + name: Filename of the plugin (without path but with file extension). + returns: The name of the origin of the plugin, or an empty string if the plugin does not exist. + + pluginNames: + returns: The list of all plugin names. + + priority: + __doc__: | + Retrieve the priority of a plugin. + + The higher the priority, the more important. + args: + name: Filename of the plugin (without path but with file extension). + returns: The priority of the given plugin, or -1 if the plugin does not exist. + + setPriority: + __doc__: Change the priority of a plugin. + + args: + name: Filename of the plugin (without path but with file extension). + priority: New priority of the plugin. + + returns: | + True on success, False if the priority change was not possible. This is usually because + one of the parameters is invalid. The function returns true even if the plugin was not moved + at the specified priority (e.g. when trying to move a non-master plugin before a master one). + + setLoadOrder: + __doc__: | + Set the load order. + + Plugins not included in the list will be placed at highest priority in the order they + were before. + args: + loadorder: The new load order, specified by the list of plugin names, sorted. + + setState: + __doc__: Set the state of a plugin. + args: + name: Filename of the plugin (without path but with file extension). + state: New state of the plugin (`INACTIVE` or `ACTIVE`). + + state: + __doc__: Retrieve the state of a plugin. + args: + name: Filename of the plugin (without path but with file extension). + returns: The state of the plugin. + + IPluginModPage: + + _parentWidget: + abstract: false + returns: The parent widget. + + displayName: + returns: The name of the page as displayed in the UI. + + handlesDownload: + __doc__: Check if the plugin handles the specified download. + args: + page_url: URL of the page that contains the download link. + download_url: The download URL. + fileinfo: Not usable in python. + returns: True if this plugin wants to handle the specified download, False otherwise. + + icon: + returns: The icon to display with the page. + + pageURL: + returns: The URL to open when the user wants to visit this mod page. + + setParentWidget: + __doc__: | + Set the parent widget for this mod page. + + Python plugins usually do not need to re-implement this and can directly access the parent + widget using `_parentWidget()` once the UI has been initialized. + abstract: false + args: + parent: The parent widget. + + useIntegratedBrowser: + __doc__: | + Indicates if the page should be displayed in the integrated browser. + + Unless the page provides a special means of starting downloads (like the nxm:// url schema + on nexus), it will not be possible to handle downloads unless the integrated browser is used! + returns: True if the page should be opened in the integrated browser, False otherwise. + + IPluginPreview: + __doc__: | + These plugins add support for previewing files in the data pane. Right now all image formats supported + by qt are implemented (including dds) but no audio files and no 3d mesh formats. + + genFilePreview: + __doc__: Generate a preview for the specified file. + args: + filename: Path to the file to preview. + max_size: Maximum size of the generated widget. + returns: The widget showing a preview of the file. + + supportedExtensions: + returns: The list of file extensions that are supported by this preview plugin. + + IPluginRequirement: + __doc__: Class representing requirements for plugins. + + Problem: + __doc__: Class representing a problem found by a requirement. + + __init__: + args: + short_description: Short description of the problem. + long_description: Long description of the problem. + + shortDescription: + returns: A short description of the problem. + + longDescription: + returns: A long description of the problem. + + check: + __doc__: Check if the requirement is met, and return a problem if not. + args: + organizer: The IOrganizer instance. + returns: The problem found if the requirement is not met, otherwise None. + + PluginRequirementFactory: + __doc__: + + basic: + __doc__: Create a basic requirement. + args: + checker: | + The callable to use to check if the requirement is met. Should return True + if the requirement is met, False otherwise. + description: The description of the problem, when the requirement is not met. + returns: The constructed requirement. + + diagnose: + __doc__: | + Construct a requirement from a diagnose plugin. + + If the wrapped diagnose plugin reports a problem, the requirement fails + and the associated message is the one from the diagnose plugin (or the + list of messages if multiple problems were reported). + args: + diagnose: The diagnose plugin to wrap in this requirement. + returns: The constructed requirement. + + gameDependency.1: + __doc__: | + Create a new game dependency requirement. + + The requirement is met when the managed game is one of the specified game. + args: + games: The names of the required games. + returns: The constructed requirement. + + gameDependency.2: + __doc__: | + Create a new game dependency requirement. + + The requirement is met when the managed game is the specified game. + args: + game: The name of the required game. + returns: The constructed requirement. + + pluginDependency.1: + __doc__: | + Create a new plugin dependency requirement. + + The requirement is met when one of the specified plugins is enabled. + args: + plugins: The name of the plugins. + returns: The constructed requirement. + + pluginDependency.2: + __doc__: | + Create a new plugin dependency requirement. + + The requirement is met when the specified plugin is enabled. + args: + plugin: The name of the plugin that must be enabled. + returns: The constructed requirement. + + + IPluginTool: + __doc__: | + This is the simplest of plugin interfaces. Such plugins simply place an icon inside the tools sub-menu + and get invoked when the user clicks it. They are expected to have a user interface of some sort. These + are almost like independent applications except they can access all Mod Organizer interfaces like querying + and modifying the current profile, mod list, load order, use MO to install mods and so on. A tool plugin + can (and should!) integrate its UI as a window inside MO and thus doesn't have to initialize a windows + application itself. + + _parentWidget: + abstract: false + returns: The parent widget. + + display: + __doc__: Called when the user starts the tool. + + displayName: + returns: The display name for this tool, as shown in the tool menu. + + icon: + returns: The icon for this tool, or a default-constructed QICon(). + + setParentWidget: + __doc__: | + Set the parent widget for this tool. + + Python plugins usually do not need to re-implement this and can directly access the parent + widget using `_parentWidget()` once the UI has been initialized. + abstract: false + args: + parent: The parent widget. + + tooltip: + returns: The tooltip for this tool. + + IProfile: + __doc__: Interface to interact with Mod Organizer 2 profiles. + + absolutePath: + returns: The absolute path to the profile folder. + + absoluteIniFilePath: + __doc__: | + Retrieve the absolute file path to the corresponding INI file for this profile. + + If iniFile does not correspond to a file in the list of INI files for the + current game (as returned by IPluginGame::iniFiles), the path to the global + file will be returned (if iniFile is absolute, iniFile is returned, otherwise + the path is assumed relative to the game documents directory). + args: + inifile: | + INI file to retrieve a path for. This can either be the name of a file or a path to the + absolute file outside of the profile. + returns: The absolute path for the given INI file for this profile. + + invalidationActive: + returns: True if automatic archive invalidation is enabled for this profile, False otherwise. + + localSavesEnabled: + returns: True if profile-specific saves are enabled for this profile, False otherwise. + + localSettingsEnabled: + returns: True if profile-specific game settings are enabled for this profile, False otherwise. + + name: + returns: The name of this profile. + + ISaveGame: + __doc__: Base class for information about what is in a save game. + + allFiles: + returns: The list of all files related to this save. + + getCreationTime: + __doc__: | + Retrieve the creation time of the save. + + The creation time of a save is not always the same as the creation time of + the file containing the save. + returns: The creation time of the save. + + getFilepath: + returns: The path name to the (main) file or folder for the save. + + getName: + returns: The name of this save, for display purpose. + + getSaveGroupIdentifier: + __doc__: | + Retrieve the name of the group this files belong to. + + The name can be used to identify sets of saves to transfer between profiles. For + RPG games, this is usually the name of a character. + returns: The group identifier for this save game. + + ISaveGameInfoWidget: + __doc__: Base class for a save game info widget. + # Class not abstract because it conflicts with Qt meta-class: + # __abstract__: true + __init__: + args: + parent: Parent widget. + _widget: + returns: The underlying `QWidget`. + setSave: + abstract: true + __doc__: Set the save file to display in this widget. + args: + save: The save to display in the widget + + LocalSavegames: + __doc__: + __abstract__: true + __init__: + __doc__: + mappings: + __doc__: + args: + profile_save_dir: + returns: + prepareProfile: + __doc__: + args: + profile: + returns: + + Mapping: + + __init__.1: + __doc__: Creates an empty Mapping. + + __init__.2: + __doc__: Creates a Mapping with the given parameters. + args: + source: The source of this mapping (absolute path), i.e. the path to the actual file. + destination: Destination of this mapping (absolute path), i.e. the path in the virtual file system. + is_directory: True if this mapping corresponds to a directory, False otherwise. + create_target: True if file creation (including move or copy) should be redirected to source. + + properties[]: + createTarget: + type: bool + desc: True if file creation (including move or copy) should be redirected to source. + destination: + type: str + desc: The destination of this mapping (absolute path). + isDirectory: + type: bool + desc: True if this mapping corresponds to a directory, False otherwise. + source: + type: str + desc: The source of this mapping (absolute path). + + ModDataChecker: + __doc__: Game feature that is used to check the content of a data tree. + + __abstract__: true + + CheckReturn: + __doc__: + INVALID: The data tree looks invalid and cannot be fixed (automatically). + FIXABLE: The data tree looks valid. + VALID: The data tree looks invalid but can be automatically fixed. + + __init__: + __doc__: + + dataLooksValid: + __doc__: | + Check that the given filetree represent a valid mod layout, or can be easily + fixed. + + This method is mainly used during installation (to find which installer should + be used or to recurse into multi-level archives), or to quickly indicates to a + user if a mod looks valid. + + This method does not have to be exact, it only has to indicate if the given tree + looks like a valid mod or not by quickly checking the structure (heavy operations + should be avoided). + + If the tree can be fixed by the `fix()` method, this method should return `FIXABLE`. + `FIXABLE` should only be returned when it is guaranteed that `fix()` can fix the tree. + args: + filetree: The tree starting at the root of the "data" folder. + returns: Whether the tree is invalid, fixable or valid. + + fix: + __doc__: | + Try to fix the given tree. + + This method is used during installation to try to fix invalid archives and will only be + called if dataLooksValid returned `FIXABLE`. + abstract: false + args: + filetree: The tree to try to fix. Can be modified during the process. + returns: + __doc__: The fixed tree, or a null pointer if the tree could not be fixed. + type: Optional["IFileTree"] + + ModDataContent: + __doc__: | + The ModDataContent feature is used (when available) to indicate to users the content + of mods in the "Content" column. + + The feature exposes a list of possible content types, each associated with an ID, a name + and an icon. The icon is the path to either: + + - A Qt resource or; + - A file on the disk. + + In order to facilitate the implementation, MO2 already provides a set of icons that can + be used. Those icons are all under ``:/MO/gui/content`` (e.g. ``:/MO/gui/content/plugin`` or ``:/MO/gui/content/music`` `). + + The list of available icons is: + + - ``plugin``: |plugin-icon| + - ``skyproc``: |skyproc-icon| + - ``texture``: |texture-icon| + - ``music``: |music-icon| + - ``sound``: |sound-icon| + - ``interface``: |interface-icon| + - ``skse``: |skse-icon| + - ``script``: |script-icon| + - ``mesh``: |mesh-icon| + - ``string``: |string-icon| + - ``bsa``: |bsa-icon| + - ``menu``: |menu-icon| + - ``inifile``: |inifile-icon| + - ``modgroup``: |modgroup-icon| + + .. |plugin-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/jigsaw-piece.png + .. |skyproc-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/hand-of-god.png + .. |texture-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/empty-chessboard.png + .. |music-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/double-quaver.png + .. |sound-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/lyre.png + .. |interface-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/usable.png + .. |skse-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/checkbox-tree.png + .. |script-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/tinker.png + .. |mesh-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/breastplate.png + .. |string-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/conversation.png + .. |bsa-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/locked-chest.png + .. |menu-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/config.png + .. |inifile-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/feather-and-scroll.png + .. |modgroup-icon| image:: https://raw.githubusercontent.com/ModOrganizer2/modorganizer/master/src/resources/contents/xedit.png + + __abstract__: true + + Content: + __doc__: + + __init__: + __doc__: + + args: + id: ID of this content. + name: Name of this content. + icon: | + Path to the icon for this content. Can be either a path + to an image on the disk, or to a resource. Can be an empty string if filterOnly + is true. + filter_only: | + Indicates if the content should only be show in the filter + criteria and not in the actual Content column. + + isOnlyForFilter: + __doc__: + returns: True if this content is only meant to be used as a filter criteria. + + properties[]: + id: + type: int + desc: The ID of this content. + name: + type: str + desc: The name of this content. + icon: + type: str + desc: The path to the icon of this content (can be a Qt resource path). + + __init__: + __doc__: + + getAllContents: + __doc__: + returns: The list of all possible contents for the corresponding game. + + getContentsFor: + __doc__: Retrieve the list of contents in the given tree. + args: + filetree: The tree corresponding to the mod to retrieve contents for. + returns: The IDs of the content in the given tree. + + ModRepositoryFileInfo: + __doc__: + properties[]: + categoryID: + type: int + desc: + description: + type: str + desc: + fileCategory: + type: int + desc: + fileID: + type: int + desc: + fileName: + type: str + desc: + fileSize: + type: int + desc: + fileTime: + type: PyQt6.QtCore.QDateTime + desc: + gameName: + type: str + desc: + modID: + type: int + desc: + modName: + type: str + desc: + name: + type: str + desc: + newestVersion: + type: VersionInfo + desc: + repository: + type: str + desc: + uri: + type: str + desc: + userData: + type: MoVariant + desc: + version: + type: VersionInfo + desc: + + __init__.1: + __doc__: + args: + other: + __init__.2: + __doc__: + args: + game_name: + mod_id: + file_id: + __str__: + __doc__: + returns: + createFromJson: + __doc__: + args: + data: + returns: + + PluginSetting: + __doc__: | + Class to hold the user-configurable parameters a plugin accepts. The purpose of this class is + only to inform the application what settings to offer to the user, it does not hold the actual value. + properties[]: + default_value: + type: MoVariant + desc: Default value of the setting. + description: + type: str + desc: Description of the setting. + key: + type: str + desc: Name of the setting. + + __init__: + __doc__: + args: + key: Name of the setting. + description: Description of the setting. + default_value: Default value of the setting. + + SaveGameInfo: + __doc__: Feature to get hold of stuff to do with save games. + __abstract__: true + __init__: + __doc__: + + getMissingAssets: + __doc__: Retrieve missing assets from the save. + args: + save: The save to find missing assets for. + returns: | + A collection of missing assets and the modules that can supply those assets. + + getSaveGameWidget: + __doc__: | + Retrieve a widget to display over the save game list. + + This method is allowed to return `None` in case no widget has been implemented. + args: + parent: The parent widget. + returns: + __doc__: A SaveGameInfoWidget to display information about save game. + type: Optional[ISaveGameInfoWidget] + + ScriptExtender: + __doc__: + __abstract__: true + + binaryName: + __doc__: + returns: The name of the script extender binary. + + pluginPath: + __doc__: + returns: The script extender plugin path, relative to the data folder. + + getArch: + __doc__: + returns: The CPU platform of the extender. + + getExtenderVersion: + __doc__: + returns: The version of the script extender. + + isInstalled: + __doc__: + returns: True if the script extender is installed, False otherwise. + + loaderName: + __doc__: + returns: The loader to use to ensure the game runs with the script extender. + + loaderPath: + __doc__: + returns: The full path to the script extender loader. + + savegameExtension: + __doc__: Retrieve the extension of script extender save files. + returns: The extension of script extender save files (e.g. "skse"). + + UnmanagedMods: + __doc__: + __abstract__: true + + displayName: + __doc__: Retrieve the display name of a given mod. + args: + mod_name: Internal name of the mod. + returns: The display name of the mod. + + mods: + __doc__: Retrieve the list of unmanaged mods for the corresponding game. + args: + official_only: Retrieve only unmanaged official mods. + returns: The list of unmanaged mods (internal names). + + referenceFile: + __doc__: | + Retrieve the reference file for the requested mod. + + Example: For Bethesda games, the reference file may be the main + plugin (esp or esm) for the game or a DLCs. + args: + mod_name: Internal name of the mod. + returns: The reference file (absolute path) for the requested mod. + + secondaryFiles: + __doc__: | + Retrieve the secondary files for the requested mod. + + Example: For Bethesda games, the secondary files may be the archives + corresponding to the reference file. + args: + mod_name: Internal name of the mod. + returns: The secondary files (absolute paths) for the request mod. + + VersionInfo: + __doc__: Represents the version of a mod or plugin. + + __init__.1: Construct an invalid VersionInfo. + + __init__.2: + __doc__: Construct a VersionInfo by parsing the given string according to the given scheme. + + args: + value: String to parse. + scheme: Scheme to use to parse the string. + + __init__.3: + __doc__: Construct a VersionInfo using the given elements. + + args: + major: Major version. + minor: Minor version. + subminor: Subminor version. + subsubminor: Subsubminor version. + release_type: Type of release. + + __init__.4: + __doc__: Construct a VersionInfo using the given elements. + + args: + major: Major version. + minor: Minor version. + subminor: Subminor version. + release_type: Type of release. + + __str__: + returns: > + See `canonicalString()`. + + canonicalString: + returns: > + A canonical string representing this version, that can be stored and then parsed using + the parse() method. + + clear: Resets this VersionInfo to an invalid version. + + displayString: + args: + forced_segments: | + The number of version segments to display even if the version is 0. 1 is major, 2 is major + and minor, etc. The only implemented ranges are (-inf,2] for major/minor, [3] for major/minor/subminor, + and [4,inf) for major/minor/subminor/subsubminor. This only versions with a regular scheme. + returns: > + A string for display to the user. The returned string may not contain enough information + to reconstruct this version info. + + isValid: + returns: True if this VersionInfo is valid, False otherwise. + + parse: + __doc__: Update this VersionInfo by parsing the given string using the given scheme. + + args: + value: String to parse. + scheme: Scheme to use to parse the string. + is_manual: True if the given string should be treated as user input. + + scheme: + returns: The version scheme in effect for this VersionInfo. + +mobase.widgets: + + TaskDialog: + __doc__: Customizable choice dialog. + + __init__: + __doc__: 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. + + addButton: + __doc__: Add a custom button to this TaskDialog. + + args: + button: Button to add to the dialog. + + addContent: + __doc__: | + Add a custom widget content to this TaskDialog. Widget content are put between + content and buttons (above buttons). + + args: + widget: Widget to add. + + exec: + __doc__: | + 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. + + setContent: + __doc__: Set the top-level message of this dialog. + + args: + content: Top-level message to set. + + setDetails: + __doc__: | + 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. + + setIcon: + __doc__: Set the icon of the dialog. + + args: + icon: Icon of the dialog. + + setMain: + __doc__: | + 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. + + setRemember: + __doc__: Configure the dialog to remember user-choice. + + args: + action: + file: + + setTitle: + __doc__: Set the title of the dialog. + + args: + title: Title of the dialog. + + setWidth: + __doc__: Set the width of the dialog. + + args: + width: Width of the dialog. + + + TaskDialogButton: + __doc__: Special button to be used inside TaskDialog widgets. + + __init__.1: + __doc__: 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. + + __init__.2: + __doc__: Create a TaskDialogButton without description. + + args: + text: Label of the button. + button: Value returned by TaskDialog.exec() if this button is clicked. + + properties[]: + text: + type: str + desc: Label of the button. + description: + type: str + desc: Description of the button. + button: + type: PyQt6.QtWidgets.QMessageBox.StandardButton + desc: Value returned by TaskDialog.exec() if this button is clicked. diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 9534b01..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt index 9017ef6..eb2a156 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ sphinx-rtd-theme sphinx-autodoc-typehints sphinx-automodapi -PyQt5 \ No newline at end of file +PyQt6 diff --git a/docs/source/conf.py b/docs/source/conf.py index 4343d85..eecbf6b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,21 +10,13 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os -import sys - -sys.path.insert(0, os.path.abspath("../../stubs/2.4.0/")) - # -- Project information ----------------------------------------------------- project = "MO2 Python Plugin API" -copyright = "2020, Holt59" +copyright = "2023, Holt59" author = "Holt59" -# The full version, including alpha/beta/rc tags -release = "2.3rc1" - # -- General configuration --------------------------------------------------- @@ -69,5 +61,5 @@ html_favicon = "mo2.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] html_extra_path = [".nojekyll"] diff --git a/generator/__init__.py b/generator/__init__.py deleted file mode 100644 index 4e3743d..0000000 --- a/generator/__init__.py +++ /dev/null @@ -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) diff --git a/generator/loader.py b/generator/loader.py deleted file mode 100644 index 343391f..0000000 --- a/generator/loader.py +++ /dev/null @@ -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) diff --git a/generator/mtypes.py b/generator/mtypes.py deleted file mode 100644 index 412270d..0000000 --- a/generator/mtypes.py +++ /dev/null @@ -1,714 +0,0 @@ -# -*- encoding: utf-8 -*- - -from typing import Optional, List, Any, Dict, Union - -from . import logger -from . import utils - - -class Type: - """ - 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]]""" - - name: str - - def __init__(self, name: Union[str, type]): - # Import only here since we change the path to find them: - from PyQt5 import QtCore, QtGui, QtWidgets - - if isinstance(name, type): - name = name.__name__ - - self.name = name.strip() - - # We replace QVariant with MoVariant which is valid python type: - if self.name == "QVariant": - self.name = "MoVariant" - - # 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, settings: utils.Settings) -> str: - """ - Returns: - A valid typing representation for this type. - """ - from .register import MOBASE_REGISTER - - # Check if this is a mobase object, in which case we escape: - if self.name in MOBASE_REGISTER.objects: - return '"{}"'.format(self.name) - - # Check in existing objects (also inner classes) - This may cause - # issue with conflicts, but those should not be present: - for k in MOBASE_REGISTER.objects: - if k.split(".")[-1] == self.name: - return '"{}"'.format(k) - - 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, Type): - return NotImplemented - return self.name == other.name - - -class CType(Type): - """ - Class representing a C++ type from boost::python. - """ - - # List of smart pointer types - Not including pointer that should not - # be exposed (unique_ptr, weak_ptr): - SMART_POINTERS = ["std::shared_ptr", "boost::shared_ptr", "QSharedPointer"] - - # Standard conversions (usually, the python signature is sufficient for - # those, unless we found them inside a tuple): - STANDARD_TYPES = { - "short": "int", - "int": "int", - "long": "int", - "float": "float", - "double": "float", - } - - # Replacements for typing (without warnings): - REPLACEMENTS = { - "QString": "str", - "QStringList": "List[str]", - "QWidget *": "PyQt5.QtWidgets.QWidget", - "QMainWindow *": "PyQt5.QtWidgets.QMainWindow", - "QObject *": "PyQt5.QtCore.QObject", - "void *": "object", - "api::object": "object", - } - - _optional: bool - - def __init__(self, name: str, optional: bool = False): - super().__init__(name) - self._optional = optional - - def _is_not_valid(self, str): - for x in str: - if x in "<>():*": - return True - return False - - def _try_fix(self, name, settings: utils.Settings): - - from .parser import parse_ctype, magic_split, parse_csig - from .register import MOBASE_REGISTER - - pname = name - - # Unconverted QFlags are int in python: - if name.startswith("QFlags"): - name = "int" - - # If pointer, try to fix the corresponding python name: - if self.is_pointer(): - - newname: Optional[str] = None - - # For display purpose: - optstr = "" - if self.is_optional(): - optstr = " [optional]" - - # Check if there is a type registered: - if self.name in MOBASE_REGISTER.cpp2py: - newtype = MOBASE_REGISTER.cpp2py[self.name] - - if newtype.is_object(): - logger.critical( - ( - "Found {} pointer but did not found any corresponding " - "python type, the interface is likely missing." - ).format(name) - ) - return "InterfaceNotImplemented" - - if self._is_builtin_python_type(newtype): - logger.critical( - "Found {} which is a pointer to a built-in python type.".format( - self.name - ) - ) - return "InterfaceNotImplemented" - - newname = newtype.name - - # Note: No WARNING here as this is safe: - logger.info("Replacing {} with {}{}.".format(name, newname, optstr)) - - # "Tricky" fix for pointer raw pointer and smart pointers: - elif self.is_raw_pointer(): - newname = name.strip("*").replace("const", "").strip() - logger.warning("Replacing {} with {}{}.".format(name, newname, optstr)) - - else: - for ptr in self.SMART_POINTERS: - if name.startswith(ptr): - newname = name[len(ptr) :][1:-1].replace("const", "").strip() - logger.warning( - "Replacing {} with {}{}.".format(name, newname, optstr) - ) - - if newname is not None: - if self.is_optional(): - if newname in MOBASE_REGISTER.py2cpp: - newname = '"{}"'.format(newname) - return "Optional[{}]".format(newname) - else: - return newname - - # Variant: - for c in ("boost::variant", "std::variant"): - if name.startswith(c): - name = name[len(c) :].strip()[1:-1].strip() - args = [parse_ctype(c).typing(settings) for c in magic_split(name)] - name = "Union[{}]".format(", ".join(args)) - - for c in ( - "std::vector", - "std::set", - "std::unordered_set", - "std::list", - "QList", - "QVector", - "QSet", - ): - if name.startswith(c): - name = name[len(c) :].strip()[1:-1].strip() - arg = parse_ctype(magic_split(name)[0]).typing(settings) - name = "List[{}]".format(arg) - - for c in ("std::map", "std::unordered_map", "QMap"): - if name.startswith(c): - name = name[len(c) :].strip()[1:-1].strip() - a1, a2 = [ - parse_ctype(x).typing(settings) for x in magic_split(name)[:2] - ] - name = "Dict[{}, {}]".format(a1, a2) - - for c in ("boost::tuples::tuple", "std::tuple"): - if name.startswith(c): - name = name[len(c) :].strip()[1:-1].strip() - args = [parse_ctype(c).typing(settings) for c in magic_split(name)] - args = [a for a in args if a != "boost::tuples::null_type"] - name = "Tuple[{}]".format(", ".join(args)) - - # Fix for optional: - if name.startswith("std::optional"): - name = name[13:].strip()[1:-1].strip() - name = "Optional[{}]".format(parse_ctype(name).typing(settings)) - - # Fix for function: - if name.startswith("std::function"): - name = name[13:].strip()[1:-1].strip() - rtype, vargs = parse_csig(name, "") - name = "Callable[[{}], {}]".format( - ", ".join(a.type.typing(settings) for a in vargs), - rtype.typing(settings), - ) - - if name.find("::") != -1: - parts = name.split("::") - - # We are going to check if there is an exact python match for - # this class (replacing :: by .): - if parts[0] in MOBASE_REGISTER.py2cpp: - cp: List[Class] = [MOBASE_REGISTER.objects[parts[0]]] # type: ignore - for p in parts[1:]: - for ic in cp[-1].inner_classes: - if ic.name == p: - cp.append(ic) - break - if len(cp) == len(parts): - name = '"' + ".".join(parts) + '"' - - if pname != name: - logger.info("Fixed {} to {}. ".format(pname, name)) - else: - logger.critical( - "Failed to fix {}, a custom rule is probably required.".format(name) - ) - - return name - - def _is_builtin_python_type(self, t: Type) -> bool: - """ - Check if the given type is a 'raw' python type, i.e., a type that cannot - be a C++ reference. This is mainly used to report errors when pointers to such - type are present in the interface. - - Args: - t: The type to check. - - Returns: - True if the given type is a raw python type, False otherwise. - """ - return t.name in ["bool", "int", "float", "str", "list", "std", "dict", "bytes"] - - def typing(self, settings: utils.Settings) -> str: - """ - Create a valid typing representation for this type. - - args: - settings: The settings to use. - - Returns: - A valid typing representing for this type. - """ - from .register import MOBASE_REGISTER - - name = self.name - - if self.is_none(): - return "None" - - if name in CType.STANDARD_TYPES: - name = CType.STANDARD_TYPES[name] - - if name in CType.REPLACEMENTS: - name = CType.REPLACEMENTS[name] - - if name in settings.replacements: - logger.warning( - "Replacing {} with {}.".format(name, settings.replacements[name]) - ) - name = settings.replacements[name] - - # If the name contains stuff that should not be there, try some - # "magic" conversion: - if self._is_not_valid(name): - name = self._try_fix(name, settings) - - if name in MOBASE_REGISTER.objects: - name = '"{}"'.format(name) - - return name - - def is_raw_pointer(self) -> bool: - """ - Check if this type is a raw pointer type. - - Returns: - True if this type is a raw point type, False otherwise. - """ - return self.name.endswith("*") - - def is_smart_pointer(self) -> bool: - """ - Check if this type is a smart pointer type. - - Returns: - True if this type is a smart pointer type, False otherwise. - """ - - for ptr in self.SMART_POINTERS: - if self.name.startswith(ptr): - return True - - return False - - def is_pointer(self) -> bool: - """ - Check if this type corresponds to a pointer type. - - Returns: - True if this type represents a pointer type (raw or smart), False - otherwise. - """ - return self.is_raw_pointer() or self.is_smart_pointer() - - def is_optional(self) -> bool: - """ - Check if this type is optional (i.e., can be None in python). - - Returns: - True if this type can be optional, False otherwise. - """ - return self._optional - - def is_none(self) -> bool: - return self.name.lower() == "void" - - def is_object(self) -> bool: - return self.name.lower() == "_object *" - - def __str__(self) -> str: - return "CType({})".format(self.name) - - def __repr__(self) -> str: - return str(self) - - -class Ret: - """ - Class representing the return value of a function (type and documentation). - """ - - type: Type - doc: str - - def __init__(self, type: Type, doc: str = ""): - self.type = type - self.doc = doc - - -class Arg: - """ - 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: Type - _value: Optional[str] - doc: str - - def __init__( - self, name: str, type: Type, value: Optional[str] = None, doc: str = "" - ): - self.name = name - self.type = type - self._value = value - self.doc = doc - - @property - def value(self) -> Optional[str]: - - value = self._value - - if value is None: - return None - - # Boost has a tendency to put `mobase.` in front of default values... - if value is not None and value.startswith("mobase."): - value = value[7:] - - 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, Arg): - return NotImplemented - return self.type == other.type - - -class Exc: - - """ - Small class representing exception that can be raised from functions. - """ - - type: Type - doc: str - - def __init__(self, type: Type, doc: str = ""): - self.type = type - self.doc = doc - - -class Function: - """ - Class representing a function. - """ - - name: str - ret: Ret - args: List[Arg] - overloads: bool - raises: List[Exc] - doc: str - deprecated: bool - - def __init__( - self, - name: str, - ret: Ret, - args: List[Arg], - 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: Union[str, bool] - static: bool - - def __init__( - self, - name: str, - ret: Ret, - args: List[Arg], - 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: Optional[Type] - value: Any - doc: Optional[str] - - def __init__( - self, name: str, type: Optional[Type], value: Any, doc: Optional[str] = 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: Type - doc: str - read_only: bool - - def __init__(self, name: str, type: Type, 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: Optional["Class"] - doc: str - abstract: bool - deprecated: bool - - def __init__( - self, - name: str, - bases: List["Class"], - methods: List[Method], - constants: List[Constant] = [], - properties: List[Property] = [], - inner_classes: List["Class"] = [], - doc: str = "", - ): - - 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 all_bases(self): - """ - 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 - - def __str__(self): - return self.canonical_name - - -class PyClass(Class): - - """ - Class use to wrap Python class to be used as parent class for some classes - in mobase. - """ - - def __init__( - self, - name: str, - ): - super().__init__(name, [], []) - self.abstract = False - - -class Enum(Class): - - """ - Class representing an enum. - """ - - def __init__(self, name: str, values: Dict[str, int]): - # 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__( - name, - [PyClass("Enum")], - [ - Method( - "__{}__".format(mname), - Ret(Type(bool)), - [Arg("", Type(name)), Arg("other", Type(int))], - static=False, - ) - for mname in ["and", "or", "rand", "ro"] - ], - inner_classes=[], - constants=[Constant(k, None, v) for k, v in values.items()], - ) - - def is_abstract(self): - return False diff --git a/generator/parser.py b/generator/parser.py deleted file mode 100644 index f0bea05..0000000 --- a/generator/parser.py +++ /dev/null @@ -1,600 +0,0 @@ -# -*- encoding: utf-8 -*- - -import inspect -import re - -from collections import defaultdict, OrderedDict - -from typing import List, Tuple, Optional, Dict, Union - -from .register import MobaseRegister -from .mtypes import ( - Type, - CType, - Class, - PyClass, - Enum, - Arg, - Ret, - Method, - Constant, - Property, - Function, -) -from . import logger - - -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,d(e,),p) -> ["a", "d(e,)", "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 - - return r - - -def parse_ctype(s: str) -> CType: - """Parse a C++ type from the given string. - - Args: - s: String to parse. - - Returns: A C++ type parsed from the given string. - """ - - # List of strings that can be removed from the names: - for d in [ - "__64", - "__cdecl", - "__ptr64", - "{lvalue}", - "class", - "struct", - "enum", - "unsigned", - ]: - s = s.replace(d, "") - - # Remove the namespace remaing: - for d in ["MOBase", "boost::python"]: - s = s.replace(d + "::", "") - - # Specific replacement: - s = s.replace("__int64", "int") - s = s.replace(" const &", "") - s = s.replace("&", "") - - return CType(s.strip()) - - -def parse_carg(s: str, has_default: bool) -> Arg: - """Parse the given C++ argument. - - Args: - s: The string to parse. - has_default: Indicates if this argument as a default. - - Returns: An argument parsed from the given string. - """ - v, d = s, None - if s.find("=") != -1: - v, d = [x.strip() for x in s.split("=")] - - if d is None and has_default: - d = Arg.DEFAULT_NONE - - return Arg("", parse_ctype(v), d) - - -def parse_csig(s, name) -> Tuple[CType, List[Arg]]: - """Parse a boost::python C++ signature. - - Args: - s: The signature to parse. - name: Name of the function, or "" if the signature correspond to a type. - - Returns: (RType, Args) where RType is a CType object, and Args is a list of Arg - objects containing CType. - """ - # Remove the [ and ] which specifies default arguments but are useless since - # we already have = to tell us - The replacement is weird because the way boost - # present these is weird, and to avoid breaking default argument such as = []: - c = s.count("[,") - s = s.replace("[,", ",") - s = s.replace("]" * c, "") - - # Split return type/arguments: - if name: - rtype_s, args_s = s.split(name) - - # Remove the ( and ). - args_s = args_s.strip()[1:-1] - else: - rtype_s, args_s = magic_split(s, "(", "(<", ")>") - - # Only remove the last ) because the first one is removed by magic_split: - args_s = args_s.strip()[:-1] - - # Parse return type: - rtype = parse_ctype(rtype_s.strip()) - - # Parse arguments: - - # Strip spaces and remove the first and last (): - args_s = args_s.strip() - args_ss = magic_split(args_s, ",", "(<", ")>") - args = [parse_carg(v, i > len(args_ss) - c - 1) for i, v in enumerate(args_ss)] - - return rtype, [arg for arg in args if not arg.type.is_none()] - - -def parse_psig(s: str, name: str) -> Tuple[Type, List[Arg]]: - """Parse a boost::python 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. - """ - - c = s.count("[,") - s = s.replace("[,", ",") - s = s.replace("]" * c, "") - - # This is pretty brutal way of extracting stuff... But most things can be - # retrieve from the C++ signature, here we are mainly interested in extracting - # the python type if possible: - m: re.Match[str] = re.search( - r"{}\((.*)\)\s*->\s*([^\s]+)\s*:".format(name), s - ) # type: ignore - pargs = [] - args = list(filter(bool, m.group(1).strip().split(","))) - for i, pa in enumerate(args): - pa = pa.strip() - - # Index of the right bracket: - irbrack = pa.find(")") - - # The type is within the brackets: - t = pa[1:irbrack] - - pa = pa[irbrack + 1 :] - n: str = pa.strip() - d: Optional[str] = None - if pa.find("=") != -1: - n, d = pa.split("=") - n = n.strip() - d = d.strip() - elif i > len(args) - c - 1: - d = Arg.DEFAULT_NONE - pargs.append(Arg(n, Type(t), d)) - return Type(m.group(2)), pargs - - -def find_best_argname(iarg: int, pname: str, cname: str): - """Find the best name for the ith argument of a function. - - Args: - iarg: Index of the argument, use for default. - pname: Name of the argument in the python signature. - cname: Name of the argument in the C++ signature. - - Returns: - The best name for the corresponding argument. - """ - if not cname and not pname: - return "arg{}".format(iarg + 1) - - return pname - - -def find_best_type(ptype: Type, ctype: CType) -> Type: - """Find the best type from the given python and C++ type. - - - Args: - ptype: The python type. - ctype: The C++ type. - - Returns: The best of the two types. - """ - from .register import MOBASE_REGISTER - - if ptype.name == ctype.name: - return ptype - elif ptype.is_none() and ctype.is_none(): - return ptype - - assert ptype.is_none() == ctype.is_none() - - MOBASE_REGISTER.register_type(ptype, ctype) - - if ptype.is_object(): - if ctype.is_object(): - return ptype - return ctype - - # Returned pointer are treated differently because they can often be null: - if ctype.is_pointer(): - return ctype - - return ptype - - -def find_best_value(pvalue: str, cvalue: str) -> str: - """Find the best value (default value) from the given python and C++ one. - - WARNING: This currently always return pvalue and only warns the user if - the two values are not identical. - - Args: - pvalue: Python default value. - cvalue: C++ default value. - - Returns: The best of the two values. - """ - if pvalue != cvalue: - logger.warning("Mismatch default value: {} {}.".format(pvalue, cvalue)) - return pvalue - - -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 otherwize. - """ - # Yet to find a better way... - if not isinstance(e, type): - return False - return any( - "{}.{}".format(c.__module__, c.__name__) == "Boost.Python.enum" - for c in inspect.getmro(e) - ) - - -def make_enum(fullname: str, e: type) -> Enum: - """Construct a Enum object from the given class. - - Args: - fullname: Fully qualified name of the enumeration. - e: The class representing a boost::python enumeration. - - Returns: An Enum object representing the given enumeration. - """ - # All boost enums have a .values attributes: - values = e.values # type: ignore - - return Enum( - e.__name__, - OrderedDict((values[k].name, k) for k in sorted(values.keys())), - ) - - -class Overload: - - """ Small class to avoid mypy issues... """ - - rtype: Type - args: List[Arg] - - def __init__(self, rtype, args): - self.rtype = rtype - self.args = args - - -def parse_bpy_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, where each overload is - a dictionary with a "rtype" entry containing the return type and a "args" - entry containing the list of arguments. - """ - lines = e.__doc__.split("\n") - - # Find the various overloads: - so = [i for i, line in enumerate(lines) if line.strip().startswith(e.__name__)] - so.append(len(lines)) - - # We are going to parse the python and C++ signature, and try to merge - # them... - overloads: List[Overload] = [] - for i, j in zip(so[:-1], so[1:]): - - psig = lines[i].strip() - for k in range(i, j): - if lines[k].strip().startswith("C++ signature"): - csig = lines[k + 1].strip() - - prtype, pargs = parse_psig(psig, e.__name__) - crtype, cargs = parse_csig(csig, e.__name__) - - # Currently there is no way to automatically check so we add [optional] - # in the doc: - if e.__doc__.find("[optional]") != -1: - crtype._optional = True - - assert len(pargs) == len(cargs) - - # Now we need to find the "best" type from both signatures: - rtype = find_best_type(prtype, crtype) - args = [] - for iarg, (parg, carg) in enumerate(zip(pargs, cargs)): - args.append( - Arg( - find_best_argname(iarg, parg.name, carg.name), - find_best_type(parg.type, carg.type), # type: ignore - find_best_value(parg.value, carg.value), # type: ignore - ) - ) # type: ignore - - overloads.append(Overload(rtype=rtype, args=args)) - - return overloads - - -def make_functions(name: str, e) -> List[Function]: - overloads = parse_bpy_function_docstring(e) - - return [ - Function( - e.__name__, - Ret(ovld.rtype), - ovld.args, - has_overloads=len(overloads) > 1, - ) - for ovld in overloads - ] - - -def make_class(fullname: str, e: type, register: MobaseRegister) -> Class: - """Constructs a Class objecgt from the given python class. - - Args: - fullname: Name of the class (might be different from __name__ for inner - classes). - 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__ == "Boost.Python": - break - - # Keep as a comment but this is/should be fixed in the actual C++ code: - # Lots of class exposed do not inherit IPlugin while they should: - # if "IPlugin" not in base_classes_s and e.__name__.startswith("IPlugin") \ - # and e.__name__ != "IPlugin": - # base_classes_s.append("IPlugin") - - # 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... The hasattr is required but I don't know why: - all_attrs = [(n, getattr(e, n)) for n in dir(e) if hasattr(e, n)] - - # Some exclusions: - EXCLUDED_MEMBERS = [ - "__weakref__", - "__dict__", - "__doc__", - "__instance_size__", - "__module__", - "__getattr__", - ] - all_attrs = [ - a - for a in all_attrs - # Using getattr() here since some attribute do not have name (e.g. constants): - if a[0] not in EXCLUDED_MEMBERS - ] - - # Fetch all attributes from the base classes: - base_attrs: Dict[str, List[Union[Constant, Property, Method, Class]]] = defaultdict( - list - ) - for bc in base_classes: - # Thanks mypy for the naming... - for a1 in bc.constants: - base_attrs[a1.name].append(a1) - for a2 in bc.methods: - base_attrs[a2.name].append(a2) - for a3 in bc.properties: - base_attrs[a3.name].append(a3) - for a4 in bc.inner_classes: - base_attrs[a4.name].append(a4) - - # Retrieve the enumerations and classes: - inner_classes = [ - ic[1] - for ic in all_attrs - if isinstance(ic[1], type) - and ic[1].__name__ != "class" - and ic[0] not in base_attrs - ] - - pinner_classes: List[Class] = [ - register.make_object("{}.{}".format(fullname, ic.__name__), ic) # type: ignore - for ic in inner_classes - ] - - # Find the methods: - methods = [m[1] for m in all_attrs if callable(m[1])] - methods = sorted(methods, key=lambda m: str(m.__name__)) - - # Filter out methods not provided or implemented: - methods = [ - m - for m in methods - if m.__doc__ is not None and m.__doc__.find("C++ signature") != -1 - ] - - # List of methods that must return bool: - BOOL_METHODS = ["__eq__", "__lt__", "__le__", "__ne__", "__gt__", "__ge__"] - - pmethods = [] - for method in methods: - if method.__doc__ is None: - continue - overloads = parse_bpy_function_docstring(method) - - # __eq__ must accept an object in python, so we need to add an overload: - if method.__name__ in ["__eq__", "__ne__"]: - overloads.append( - Overload( - rtype=Type("bool"), - args=[Arg("", Type(e.__name__)), Arg("other", Type("object"))], - ) - ) - - cmethods = [] - for ovld in overloads: - args = ovld.args - - # This is a very heuristic way of checking if the method is static but I did - # not find anything better yet... - static = False - if len(args) == 0: - static = True - elif method.__name__.startswith("__"): # Special method cannot be static - static = False - else: - arg0_name = args[0].type.name - if arg0_name in register.cpp2py: - arg0_name = register.cpp2py[arg0_name].name - arg0_name = ( - arg0_name.replace("*", "") - .replace("&", "") - .replace("const", "") - .strip() - ) - - static = ( - arg0_name - not in [e.__name__, e.__name__ + "Wrapper"] + base_classes_s - ) - - # We need to fix some default values (basically default values that - # comes from inner enum): - for arg in ovld.args: - if arg.has_default_value(): - value: str = arg.value # type: ignore - bname = value.split(".")[0] - for bclass in base_classes: - for biclass in bclass.inner_classes: - if isinstance(biclass, Enum) and biclass.name == bname: - arg._value = bclass.name + "." + value - - pmethod = Method( - method.__name__, - Ret(ovld.rtype), - ovld.args, - static=static, - has_overloads=len(overloads) > 1, - ) - - if method.__name__ in BOOL_METHODS: - pmethod.ret = Ret(Type("bool")) - - cmethods.append(pmethod) - - pmethods.extend(cmethods) - - # 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, Type("Any"), attr.fset is None)) - elif not hasattr(attr, "__name__"): - constants.append(Constant(name, Type(type(attr).__name__), attr)) - - direct_bases: List[Class] = [] - for c in e.__bases__: - if c.__module__ != "Boost.Python": - 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( - "PyQt5.QtWidgets.QWidget", e.__name__ - ) - ) - direct_bases.append(PyClass("PyQt5.QtWidgets.QWidget")) - - return Class( - e.__name__, - direct_bases, - pmethods, - inner_classes=pinner_classes, - properties=properties, - constants=constants, - ) diff --git a/generator/register.py b/generator/register.py deleted file mode 100644 index 0fa413e..0000000 --- a/generator/register.py +++ /dev/null @@ -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() diff --git a/generator/utils.py b/generator/utils.py deleted file mode 100644 index b33488d..0000000 --- a/generator/utils.py +++ /dev/null @@ -1,515 +0,0 @@ -# -*- encoding: utf-8 -*- - -from collections import OrderedDict, defaultdict -from typing import ( - Any, - Dict, - List, - NamedTuple, - Optional, - Set, - TextIO, - Tuple, - Union, - TYPE_CHECKING, -) - -from . import logger -from . import mtypes - -import yaml - -if TYPE_CHECKING: - from .register import MobaseRegister - - -class Settings: - class FunctionSettings(NamedTuple): - - doc: str - args: Optional[List["mtypes.Arg"]] = None - ret: Optional["mtypes.Ret"] = None - raises: List["mtypes.Exc"] = [] - static: Optional[bool] = None - abstract: Optional[bool] = None - deprecated: bool = False - - register: "MobaseRegister" - - # Name to ignore: - _ignore_names: List[str] - - # Extra replacements (with warnings): - _replacements: Dict[str, str] - - # Content of mobase: - _mobase: Dict[str, Dict[str, Any]] - - def __init__(self, register: "MobaseRegister", fp: Optional[TextIO] = None): - - self.register = register - - if fp is None: - self._ignore_names = [] - self._replacements = {} - self._mobase = {} - else: - data = yaml.load(fp, yaml.FullLoader) - assert data["version"] == 1 - - self._ignore_names = data.get("ignores", []) - self._replacements = data.get("replacements", {}) - self._mobase = data.get("mobase", {}) - - @property - def ignore_names(self) -> List[str]: - return self._ignore_names - - @property - def replacements(self) -> Dict[str, str]: - return self._replacements - - @property - def mobase(self) -> Dict[str, Dict[str, Any]]: - return self._mobase - - def _get_class_settings(self, canonical_name: str) -> Optional[Dict[str, Any]]: - """ - Retrieve the settings for the given class. - - Args: - canonical_name: Canonical name of the class. - - Returns: - The settings for the corresponding class, or None if the - settings where not found. - """ - parts = canonical_name.split(".") - base: Dict[str, Any] = self._mobase - for part in parts: - if part in base: - base = base[part] - else: - return None - return base - - def _parse_function_settings( - self, settings: Union[str, Dict[str, Any]] - ) -> "FunctionSettings": - """ - Parse settings for a function or method. - - Args: - settings: Settings corresponding to a function. - - Returns: - The parsed settings for the function. - """ - FunctionSettings = Settings.FunctionSettings - - if settings is None: - return FunctionSettings("") - - if isinstance(settings, str): - return FunctionSettings(settings) - - doc = settings.get("__doc__", "") - static = settings.get("static", None) - abstract = settings.get("abstract", None) - deprecated = settings.get("deprecated", False) - - # Arguments: - args: Optional[List[mtypes.Arg]] = None - - # If "args" is in settings, it can either be None (args: ) or - # a list of args: - if "args" in settings: - args = [] - if settings["args"] is not None: - - # For each argument, we either have a None value (name: ), - # or a string (name: Description) or a dictionary that can contain - # __doc__ and type. - for aname, avalue in settings["args"].items(): - t: mtypes.Type = mtypes.Type("None") - d: str = "" - if avalue is None: - pass - elif isinstance(avalue, str): - d = avalue - else: - d = avalue.get("__doc__", "") - t = mtypes.Type(avalue.get("type", "None")) - args.append(mtypes.Arg(aname, t, doc=d)) - - ret: Optional[mtypes.Ret] = None - if "returns" in settings and settings["returns"] is not None: - if isinstance(settings["returns"], str): - ret = mtypes.Ret(mtypes.Type("None"), settings["returns"]) - else: - ret = mtypes.Ret( - mtypes.Type(settings["returns"].get("type", "None")), - settings["returns"].get("__doc__", ""), - ) - - excs: List[mtypes.Exc] = [] - if "raises" in settings and settings["raises"] is not None: - for r, v in settings["raises"].items(): - if v is None: - v = "" - excs.append(mtypes.Exc(mtypes.Type(r), v)) - - return FunctionSettings(doc, args, ret, excs, static, abstract, deprecated) - - def patch_functions(self, fns: List["mtypes.Function"]): - for i, fn in enumerate(fns): - - # Find the name in settings: - if fn.has_overloads(): - sname = "{}.{}".format(fn.name, i + 1) - else: - sname = fn.name - - # If the name is in the settings: - if sname in self._mobase: - fsettings = self._parse_function_settings(self._mobase[sname]) - - # Force raises: - fn.raises = fsettings.raises - - # Check the args: - if fsettings.args is not None: - if len(fsettings.args) != len(fn.args): - logger.warn( - f"Mismatch number of arguments for function mobase.{sname}." - ) - - for sarg, marg in zip(fsettings.args, fn.args): - marg.doc = sarg.doc - if not sarg.type.is_none(): - marg.type = sarg.type - - # Check the return type: - if fsettings.ret is not None: - - # Force the doc anyway: - fn.ret.doc = fsettings.ret.doc - - # Update the type if specified: - if not fsettings.ret.type.is_none(): - fn.ret.type = fsettings.ret.type - - # Force the doc: - fn.doc = fsettings.doc - - # Force depreciation: - fn.deprecated = fsettings.deprecated - - else: - logger.warn("Missing settings for function mobase.{}.".format(sname)) - - def patch_class(self, cls: "mtypes.Class"): - """ - Patch the given class using the given overwrites. - - See config.json for some examples of valid overwrites. - - Args: - cls: The class to patch. - settings: The settings. - """ - - logger.info("Patching class {}.".format(cls.name)) - - # Find the class in mobase: - csettings = self._get_class_settings(cls.canonical_name) - - if csettings is None: - logger.warn("Class {} not found in settings.".format(cls.canonical_name)) - return - - if "__doc__" in csettings and csettings["__doc__"] is not None: - cls.doc = csettings["__doc__"] - csettings.pop("__doc__", None) - - # Check bases: - if "__bases__" in csettings: - for bc in csettings["__bases__"]: - if bc.startswith("PyQt"): - cls.bases.append(mtypes.PyClass(bc)) - else: - cls.bases.append(self.register.get_object(bc)) - del csettings["__bases__"] - - if "__abstract__" in csettings and csettings["__abstract__"]: - cls.abstract = True - del csettings["__abstract__"] - - # Patch properties - Everything should be in config since property are poorly - # documented by boost::python. - properties: Dict[str, Any] = csettings.pop("properties[]", {}) - for prop in cls.properties: - if prop.name in properties: - sprop = properties[prop.name] - - # If we have a type: - if "type" in sprop: - prop.type = mtypes.Type(sprop["type"]) - else: - logger.warn( - "Missing type for property {}.{}.".format( - cls.canonical_name, prop.name - ) - ) - - # If we have a description: - if "desc" in sprop: - - # If desc is None, we do not warn user, because the entry is in - # settings, just empty: - if sprop["desc"] is not None: - prop.doc = sprop["desc"] - - else: - logger.warn( - "Missing description for property {}.{}.".format( - cls.canonical_name, prop.name - ) - ) - - # Patch signals - Everything should be in config since signals are not really - # exposed by boost::python. - signals: List[str] = csettings.pop("signals[]", []) - for signal in signals: - cls.constants.append( - mtypes.Constant(signal, mtypes.Type("pyqtSignal"), None) - ) - - # List of all items in csettings: - keys = {k: False for k in csettings} - - # Group method by name: - methods: Dict[str, List[mtypes.Method]] = defaultdict(list) - for m in cls.methods: - methods[m.name].append(m) - - for k, ms in methods.items(): - for i, m in enumerate(ms): - - # Find the name in settings: - if m.has_overloads(): - sname = "{}.{}".format(m.name, i + 1) - else: - sname = m.name - - missing_settings: Set[str] = set() - - # If the name is in the settings: - if sname in csettings: - keys[sname] = True - fsettings = self._parse_function_settings(csettings[sname]) - - # Force raises: - m.raises = fsettings.raises - - # Force static: - if fsettings.static is not None: - if m.is_static() != fsettings.static: - logger.warn( - "Forcing method {}.{} to be {}.".format( - cls.canonical_name, - sname, - "static" if fsettings.static else "non static", - ) - ) - m.static = fsettings.static - - if fsettings.abstract is not None: - m.abstract = fsettings.abstract - - # Check the args: - if fsettings.args is not None: - margs = m.args if m.is_static() else m.args[1:] - if len(fsettings.args) != len(margs): - logger.warn( - "Mismatch number of arguments for method {}.{}.".format( - cls.canonical_name, sname - ) - ) - - for sarg, marg in zip(fsettings.args, margs): - marg.doc = sarg.doc - if ( - not marg.name.startswith("arg") - and marg.name != sarg.name - ): - logger.warn( - "Mismatch argument name for method {}.{}: {} {}, using {}.".format( # noqa: E501 - cls.canonical_name, - sname, - marg.name, - sarg.name, - sarg.name, - ) - ) - - marg.name = sarg.name - if not sarg.type.is_none(): - marg.type = sarg.type - - # Check the return type: - if fsettings.ret is not None: - - # Force the doc anyway: - m.ret.doc = fsettings.ret.doc - - # Update the type if specified: - if not fsettings.ret.type.is_none(): - m.ret.type = fsettings.ret.type - - # Force the doc: - m.doc = fsettings.doc - - # Force deprecated: - m.deprecated = fsettings.deprecated - - # Only warn for "normal" methods: - elif not m.name.startswith("__"): - missing_settings.add(sname) - - # Remove the deprecated methods: - noverloads = 0 - for m in ms: - overriden: bool = False - for bc in cls.all_bases: - for bm in bc.methods: - if bm.name == "__init__" or bm.name != m.name: - continue - if len(bm.args) == len(m.args) and all( - ba.type == ma.type - for ba, ma in zip(bm.args[1:], m.args[1:]) - ): - overriden = True - if m.is_deprecated() or overriden: - cls.methods.remove(m) - else: - noverloads += 1 - - for m in ms: - m.overloads = noverloads > 1 - - if noverloads > 0 and missing_settings: - for sname in missing_settings: - logger.warn( - "Missing settings for method {}.{}.".format( - cls.canonical_name, sname - ) - ) - - # Patch inner classes: - for ic in cls.inner_classes: - keys[ic.name] = True - self.patch_class(ic) - - # Mark the constant: - for cc in cls.constants: - keys[cc.name] = True - - # Print items missing in mobase - missings = [k for k, v in keys.items() if not v] - if missings: - logger.warn( - "The following members were found in settings but not in the actual" - " class {}: {}.".format(cls.canonical_name, ", ".join(missings)) # noqa - ) - - -def clean_class(cls: "mtypes.Class", settings: Settings): - """ - Clean the given class object. - - Args: - cls: The class object to clean. - settings: The settings. - """ - - from .register import MOBASE_REGISTER - - # Remove duplicate methods (based on name and argument types): - methods: Dict[ - Tuple[str, Tuple[mtypes.Arg, ...]], List[mtypes.Method] - ] = OrderedDict() - methods_by_name = defaultdict(list) - for m in cls.methods: - k = (m.name, tuple(m.args if m.is_static() else m.args[1:])) - if k not in methods: - methods[k] = [] - methods[k].append(m) - methods_by_name[m.name].append(m) - - clean_methods: List[mtypes.Method] = [] - for name, args in methods: - ms = methods[name, args] - method: mtypes.Method = ms[0] - if len(ms) > 1: - - # If we have more than two methods, there is a problem... - assert len(methods[name, args]) == 2 - assert ( - ms[0].ret.type.is_none() - or ms[1].ret.type.is_none() - or ms[0].ret.type.name == ms[1].ret.type.name - ) - - if ms[0].ret.type.is_none(): - # If both are None, we need to take the first one because the second - # one does not contains the name of the arguments, for whatever reason. - if ms[1].ret.type.is_none(): - method = ms[0] - else: - method = ms[1] - else: - method = ms[0] - - # If those were the only two, we need to remove the overload: - if len(methods_by_name[name]) == 2: - method.overloads = False - - # Filter methods from parent class: - if method.is_static(): - clean_methods.append(method) - else: - arg0_name = method.args[0].type.name - if arg0_name in MOBASE_REGISTER.cpp2py: - arg0_name = MOBASE_REGISTER.cpp2py[arg0_name].name - if arg0_name in [cls.name, "object"]: - clean_methods.append(method) - else: - logger.info( - "Removing {}({}) from {} (already in base {}).".format( - name, - ", ".join(a.type.typing(settings) for a in args), - cls.name, - method.args[0].type.typing(settings), - ) - ) - - # We need to filter-out __eq__(X, object) and __ne__(X, object) because these won't - # be filtered since the first arg is not of the right type: - for name in ("__eq__", "__ne__"): - fns = [m for m in clean_methods if m.name == name] - if len(fns) == 1 and fns[0].args[1].type.is_object(): - clean_methods.remove(fns[0]) - - cls.methods = clean_methods - - # Remove all non-uppercases enum names - This is a temporary fix to avoid breaking - # old plugins that uses old enum values: - if isinstance(cls, mtypes.Enum): - newc = [c for c in cls.constants if c.name.isupper()] - if newc: - cls.constants = newc - - # Clean inner classes: - for ic in cls.inner_classes: - clean_class(ic, settings) diff --git a/main.py b/main.py deleted file mode 100644 index c842820..0000000 --- a/main.py +++ /dev/null @@ -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, -) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2dd8f5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = "mo2-pystubs-generation" +version = "0.1.0" +description = "" +authors = ["Holt59 "] +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.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"] +build-backend = "poetry.core.masonry.api" + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203"] + +[tool.isort] +profile = "black" +multi_line_output = 3 + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.mypy] +warn_return_any = true +warn_unused_configs = true +namespace_packages = true + +[tool.pyright] +# reportMissingTypeStubs = true +# reportUntypedBaseClass = false +typeCheckingMode = "strict" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 69a1a66..0000000 --- a/setup.cfg +++ /dev/null @@ -1,33 +0,0 @@ -[flake8] -# Use black line length: -max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, -per-file-ignores = - *.pyi: E301, E302, E305, E501, E701, E741, F401, F403, F405, F822 - # Since typing.pyi defines "overload" this is not recognized by flake8 as typing.overload. - # Unfortunately, flake8 does not allow to "noqa" just a specific error inside the file itself. - typing.pyi: E301, E302, E305, E501, E701, E741, F401, F403, F405, F811, F822 - -[mypy] -warn_return_any = True -warn_unused_configs = True -namespace_packages = True - -[tox:tox] -skipsdist = true -envlist = py38-lint - -[testenv:py38-lint] -skip_install = true -deps = - black - mypy - flake8 - flake8-black - PyQt5-stubs -commands = - black generator main.py --check --diff - flake8 generator main.py - mypy generator main.py diff --git a/src/mo2/stubs/generator/__init__.py b/src/mo2/stubs/generator/__init__.py new file mode 100644 index 0000000..c58777e --- /dev/null +++ b/src/mo2/stubs/generator/__init__.py @@ -0,0 +1,7 @@ +import logging + +from .loader import load_mobase + +LOGGER = logging.getLogger(__name__) + +__all__ = ["load_mobase", "LOGGER"] diff --git a/src/mo2/stubs/generator/__main__.py b/src/mo2/stubs/generator/__main__.py new file mode 100644 index 0000000..8fe0ad0 --- /dev/null +++ b/src/mo2/stubs/generator/__main__.py @@ -0,0 +1,229 @@ +import argparse +import inspect +import logging +import types +from pathlib import Path +from typing import Callable + +import black +import isort + +from .loader import load_mobase +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_functions + +LOGGER = logging.getLogger(__package__) + + +def extract_objects(module: object, skips: list[str] = []) -> list[tuple[str, type]]: + objects: list[tuple[str, type]] = [] + + assert hasattr(module, "__name__") + module_name: str = module.__name__ # type: ignore + + for name in dir(module): + if name.startswith("__") or name in skips: + continue + + obj = getattr(module, name) + + # skip submodules + if inspect.ismodule(obj): + continue + + # skip imports - type object have wrong __module__? + if hasattr(obj, "__module__") and obj.__module__ != module_name: + if obj.__module__ != types.__name__ or hasattr(types, name): + continue + + objects.append((name, obj)) + + return objects + + +def add_mobase_header(writer: Writer): + writer.print_imports( + [ + "abc", + ("enum", ["Enum"]), + "os", + ( + "typing", + [ + "Callable", + "Dict", + "Iterator", + "List", + "Optional", + "overload", + "Sequence", + "Set", + "Tuple", + "Type", + "TypeVar", + "Union", + ], + ), + "PyQt6.QtCore", + "PyQt6.QtGui", + "PyQt6.QtWidgets", + ] + ) + + +def add_mobase_widgets_header(writer: Writer): + writer.print_imports( + [ + ( + "typing", + ["List", "Tuple", "Union", "overload"], + ), + "PyQt6.QtCore", + "PyQt6.QtGui", + "PyQt6.QtWidgets", + ] + ) + + +def main() -> None: + 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, type]]] = { + "mobase": extract_objects( + mobase, + [ + # the "real" IPlugin is IPluginBase + "IPlugin", + ], + ), + "mobase.widgets": extract_objects(getattr(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, PyTyping): + ... + + elif is_list_of_functions(c): + 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) + + 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, + ) + isort.api.sort_file(output_folder.joinpath("__init__.pyi")) + + +if __name__ == "__main__": + main() diff --git a/src/mo2/stubs/generator/loader.py b/src/mo2/stubs/generator/loader.py new file mode 100644 index 0000000..dc2d36c --- /dev/null +++ b/src/mo2/stubs/generator/loader.py @@ -0,0 +1,59 @@ +import os +import sys +from modulefinder import Module +from pathlib import Path +from typing import Any + + +def load_mobase(path: os.PathLike[Any]) -> Module: + """ + 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. + """ + + path = Path(path) + + # 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 + os.add_dll_directory(str(path.joinpath("dlls"))) # type: ignore + + # 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 # type: ignore + + +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) diff --git a/src/mo2/stubs/generator/mtypes.py b/src/mo2/stubs/generator/mtypes.py new file mode 100644 index 0000000..103ca8f --- /dev/null +++ b/src/mo2/stubs/generator/mtypes.py @@ -0,0 +1,447 @@ +from __future__ import annotations + +import re +from typing import Final, TypeVar + + +class PyType: + """ + Class representing a python type. + """ + + 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" + + # PathLike should be [] in the stubs + self.name = self.name.replace("os.PathLike", "os.PathLike[str]") + + 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) -> bool: + """ + 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) -> str: + """ + Returns: + The canonical name of this class. + """ + name = self.name + oc = self.outer_class + while oc is not None: + name = f"{oc.name}.{name}" + oc = oc.outer_class + + return name + + @property + def full_name(self) -> str: + """ + 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 + + +class PyTyping: + """ + Class representing a typing object, e.g., MoVariant. + """ + + name: Final[str] + typing: Final[str] + + def __init__(self, name: str, obj: object): + self.name = name + + _typing: str + if obj.__module__ == "types": + _typing = str(obj) + # type-var have a weird name, e.g., ~Name + elif type(obj) is TypeVar: + _typing = f'TypeVar("{name}")' + else: + _typing = str(obj) + + self.typing = _typing diff --git a/src/mo2/stubs/generator/parser.py b/src/mo2/stubs/generator/parser.py new file mode 100644 index 0000000..e42c17a --- /dev/null +++ b/src/mo2/stubs/generator/parser.py @@ -0,0 +1,384 @@ +import inspect +import logging +import re +import types +from collections import OrderedDict, defaultdict +from itertools import chain +from typing import Any, Iterable, cast + +from .mtypes import ( + Argument, + Class, + Constant, + Enum, + Function, + Method, + Property, + PyClass, + PyType, + Return, +) +from .register import MobaseRegister + +LOGGER = logging.getLogger(__package__) + + +def magic_split( + value: str, sep: str = ",", open: str = "(<[", close: str = ")>]" +) -> list[str]: + """ + 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,d(e,),p) -> ["a", "d(e,)", "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[int] = [] + r: list[str] = [] + 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: list[Argument] = [] + for pa in args: + m = re.search( + r"(?P[^:]+)\s*:\s*(?P[^=]+)\s*(=\s*(?P[^,]+))?", + 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: type) -> 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__ or "").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: type) -> 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 = ( # pyright: ignore[reportPrivateUsage] + 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: list[Constant] = [] + properties: list[Property] = [] + 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": + b = register.get_object(c.__name__) + assert isinstance(b, Class) + direct_bases.append(b) + + # 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 = cast(dict[str, tuple[int, Any]], 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, + ) diff --git a/src/mo2/stubs/generator/register.py b/src/mo2/stubs/generator/register.py new file mode 100644 index 0000000..44fb9a8 --- /dev/null +++ b/src/mo2/stubs/generator/register.py @@ -0,0 +1,68 @@ +# -*- encoding: utf-8 -*- + +from __future__ import annotations + +from collections import OrderedDict + +from .mtypes import Class, Function, PyTyping + + +class MobaseRegister: + """ + Class that register classes. + """ + + objects: dict[str, Class | list[Function] | PyTyping] + + def __init__(self) -> None: + self.raw_objects: dict[str, type] = OrderedDict() + self.objects = {} + + def add_object(self, name: str, object: type) -> None: + self.raw_objects[name] = object + + def make_object( + self, name: str, e: type | None = None + ) -> Class | list[Function] | PyTyping: + """ + 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_class, 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 isinstance(e, type): + 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) + + return self.objects[name] + + 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). + + Args: + name: Name of the object to retrieve. + + Returns: + The object with the given name. + """ + return self.objects[name] diff --git a/src/mo2/stubs/generator/utils.py b/src/mo2/stubs/generator/utils.py new file mode 100644 index 0000000..6dc55f2 --- /dev/null +++ b/src/mo2/stubs/generator/utils.py @@ -0,0 +1,568 @@ +from __future__ import annotations + +import logging +from collections import OrderedDict, defaultdict +from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypedDict, cast + +import yaml + +from .mtypes import ( + Argument, + Class, + Constant, + Enum, + Exception, + Function, + Method, + Property, + PyClass, + PyType, + Return, +) + +if TYPE_CHECKING: + from .register import MobaseRegister + +LOGGER = logging.getLogger(__package__) + + +class Settings: + class YamlFunctionArgument(TypedDict, total=False): + __doc__: str + type: str + desc: str + + class YamlFunctionReturn(TypedDict, total=False): + __doc__: str + type: str + + class YamlFunctionSettings(TypedDict, total=False): + __doc__: str + abstract: bool + deprecated: bool + + args: dict[str, Settings.YamlFunctionArgument | str | None] | None + returns: str | Settings.YamlFunctionReturn | None + + raises: dict[str, str | None] | None + + class YamlClassProperty(TypedDict): + type: str + desc: str | None + + # need to use a functional-styled TypeDict due to the invalid Python + # attribute names + YamlClassSettings = TypedDict( + "YamlClassSettings", + { + "__doc__": str | None, + "__bases__": list[str], + "__abstract__": bool, + "properties[]": dict[str, YamlClassProperty], + "signals[]": dict[str, YamlFunctionSettings], + }, + total=False, + ) + + class PyFunctionSettings(NamedTuple): + doc: str + args: list[Argument] | None = None + ret: Return | None = None + raises: list[Exception] = [] + abstract: bool | None = None + deprecated: bool = False + + register: MobaseRegister + + version: Final[str] + + # Name to ignore: + _ignore_names: list[str] + + # Extra replacements (with warnings): + _replacements: dict[str, str] + + # Content of mobase: + _module: dict[str, dict[str, object]] + + def __init__( + self, + register: MobaseRegister, + fp: TextIO | None = None, + module: str | None = None, + ): + self.register = register + + if fp is None: + self._ignore_names = [] + self._replacements = {} + self._version = "" + self._module = {} + else: + data = yaml.load(fp, yaml.FullLoader) + assert data["version"] == 2, "only settings version 2 are supported" + + # retrieve the module version + self.version = data["__version__"] + + assert module is not None + self._module = data.get(module, None) or {} + + def _get_class_settings(self, canonical_name: str) -> YamlClassSettings | None: + """ + Retrieve the settings for the given class. + + Args: + canonical_name: Canonical name of the class. + + Returns: + The settings for the corresponding class, or None if the + settings where not found. + """ + parts = canonical_name.split(".") + base: dict[str, object] = dict(self._module) + for part in parts: + if part in base: + base = base[part] # type: ignore + else: + return None + return base # type: ignore + + def _parse_function_settings( + self, settings: str | YamlFunctionSettings | None + ) -> PyFunctionSettings: + """ + Parse settings for a function or method. + + Args: + settings: Settings corresponding to a function. + + Returns: + The parsed settings for the function. + """ + if settings is None: + return Settings.PyFunctionSettings("") + + if isinstance(settings, str): + return Settings.PyFunctionSettings(settings) + + doc = settings.get("__doc__", "") + # static = settings.get("static", None) + abstract = settings.get("abstract", None) + deprecated = settings.get("deprecated", False) + + # Arguments: + args: list[Argument] | None = None + + # If "args" is in settings, it can either be None (args: ) or + # a list of args: + if "args" in settings: + args = [] + if settings["args"] is not None: + # For each argument, we either have a None value (name: ), + # or a string (name: Description) or a dictionary that can contain + # __doc__ and type. + for name, value in settings["args"].items(): + t: PyType = PyType("None") + d: str = "" + if value is None: + pass + elif isinstance(value, str): + d = value + else: + d = value.get("__doc__", "") + t = PyType(value.get("type", "None")) + args.append(Argument(name, t, doc=d)) + + ret: Return | None = None + if "returns" in settings and settings["returns"] is not None: + if isinstance(settings["returns"], str): + ret = Return(PyType("None"), settings["returns"]) + else: + ret = Return( + PyType(settings["returns"].get("type", "None")), + settings["returns"].get("__doc__", ""), + ) + + exceptions: list[Exception] = [] + if "raises" in settings and settings["raises"] is not None: + for r, v in settings["raises"].items(): + if v is None: + v = "" + exceptions.append(Exception(PyType(r), v)) + + return Settings.PyFunctionSettings( + doc, args, ret, exceptions, abstract, deprecated + ) + + def patch_functions(self, fns: list[Function]): + for i, fn in enumerate(fns): + # Find the name in settings: + if fn.has_overloads(): + setting_name = "{}.{}".format(fn.name, i + 1) + else: + setting_name = fn.name + + # If the name is in the settings: + if setting_name in self._module: + function_settings = self._parse_function_settings( + self._module[setting_name] # type: ignore + ) + + # Force raises: + fn.raises = function_settings.raises + + # Check the args: + if function_settings.args is not None: + if len(function_settings.args) != len(fn.args): + LOGGER.warn( + f"Mismatch number of arguments for function " + f"mobase.{setting_name}." + ) + + for setting_arg, method_arg in zip(function_settings.args, fn.args): + method_arg.doc = setting_arg.doc + if not setting_arg.type.is_none(): + method_arg.type = setting_arg.type + + # Check the return type: + if function_settings.ret is not None: + # Force the doc anyway: + fn.ret.doc = function_settings.ret.doc + + # Update the type if specified: + if not function_settings.ret.type.is_none(): + fn.ret.type = function_settings.ret.type + + # Force the doc: + fn.doc = function_settings.doc + + # Force depreciation: + fn.deprecated = function_settings.deprecated + + else: + LOGGER.warn( + "Missing settings for function mobase.{}.".format(setting_name) + ) + + def patch_class(self, cls: Class): + """ + Patch the given class using the given overwrites. + + See config.json for some examples of valid overwrites. + + Args: + cls: The class to patch. + settings: The settings. + """ + + LOGGER.info("Patching class {}.".format(cls.name)) + + # fix the name + if cls.name == "IPluginBase": + cls.name = "IPlugin" + + # Find the class in mobase: + 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)) + return + + if "__doc__" in class_settings and class_settings["__doc__"] is not None: + cls.doc = class_settings["__doc__"] + class_settings.pop("__doc__", None) + + # Check bases: + if "__bases__" in class_settings: + for bc in class_settings["__bases__"]: + if bc.startswith("PyQt"): + parts = bc.split(".") + cls.bases.append( + PyClass(package=".".join(parts[:-1]), name=parts[-1]) + ) + else: + class_ = self.register.get_object(bc) + assert isinstance(class_, Class) + cls.bases.append(class_) + del class_settings["__bases__"] + + if "__abstract__" in class_settings and class_settings["__abstract__"]: + cls.abstract = True + del class_settings["__abstract__"] + + # Patch properties - Everything should be in config since property are poorly + # documented by boost::python. + properties: dict[str, Settings.YamlClassProperty] = class_settings.pop( + "properties[]", cast(dict[str, Settings.YamlClassProperty], {}) + ) + for prop in cls.properties: + if prop.name in properties: + settings_property = properties[prop.name] + + # If we have a type: + if "type" in settings_property: + prop.type = PyType(settings_property["type"]) + else: + LOGGER.warn( + "Missing type for property {}.{}.".format( + cls.canonical_name, prop.name + ) + ) + + # If we have a description: + if "desc" in settings_property: + # If desc is None, we do not warn user, because the entry is in + # settings, just empty: + if settings_property["desc"] is not None: + prop.doc = settings_property["desc"] + + else: + LOGGER.warn( + "Missing description for property {}.{}.".format( + cls.canonical_name, prop.name + ) + ) + + # patch signals - Everything should be in config since signals are not really + # exposed by pybind11. + signals: list[str] = list(class_settings.pop("signals[]", cast(list[str], []))) + for signal in signals: + cls.constants.append(Constant(signal, PyType("pyqtSignal"), None)) + + # List of all items in class_settings: + keys = {k: False for k in class_settings} + + # Group method by name: + methods: dict[str, list[Method]] = defaultdict(list) + for m in cls.methods: + methods[m.name].append(m) + + for ms in methods.values(): + missing_settings: set[str] = set() + for i, m in enumerate(ms): + # Find the name in settings: + if m.has_overloads(): + settings_name = "{}.{}".format(m.name, i + 1) + else: + settings_name = m.name + + # If the name is in the settings: + if settings_name in class_settings: + keys[settings_name] = True + function_settings = self._parse_function_settings( + class_settings[settings_name] # type: ignore + ) + + # Force raises: + m.raises = function_settings.raises + + if function_settings.abstract is not None: + m.abstract = function_settings.abstract + + # Check the args: + 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( + "Mismatch number of arguments for method {}.{}.".format( + cls.canonical_name, settings_name + ) + ) + + for settings_arg, method_arg in zip( + function_settings.args, method_arguments + ): + method_arg.doc = settings_arg.doc + if ( + not method_arg.name.startswith("arg") + and method_arg.name != settings_arg.name + ): + LOGGER.warn( + ( + "Mismatch argument name for method {}.{}: " + "{} {}, using {}." + ).format( + cls.canonical_name, + settings_name, + method_arg.name, + settings_arg.name, + settings_arg.name, + ) + ) + + method_arg.name = settings_arg.name + if not settings_arg.type.is_none(): + method_arg.type = settings_arg.type + + # Check the return type: + if function_settings.ret is not None: + # Force the doc anyway: + m.ret.doc = function_settings.ret.doc + + # Update the type if specified: + if not function_settings.ret.type.is_none(): + m.ret.type = function_settings.ret.type + + # Force the doc: + m.doc = function_settings.doc + + # Force deprecated: + m.deprecated = function_settings.deprecated + + # Only warn for "normal" methods: + elif not m.name.startswith("__"): + missing_settings.add(settings_name) + + # remove the deprecated methods and count overloads + n_overloads = 0 + for m in ms: + if m.is_deprecated(): + cls.methods.remove(m) + else: + n_overloads += 1 + + for m in ms: + m.overloads = n_overloads > 1 + + if n_overloads > 0 and missing_settings: + for settings_name in missing_settings: + LOGGER.warn( + "Missing settings for method {}.{}.".format( + cls.canonical_name, settings_name + ) + ) + + # Patch inner classes: + for ic in cls.inner_classes: + keys[ic.name] = True + self.patch_class(ic) + + # Mark the constant: + for cc in cls.constants: + keys[cc.name] = True + + # Print items missing in mobase + missing_items = [k for k, v in keys.items() if not v] + if missing_items: + LOGGER.warn( + "The following members were found in settings but not in the actual" + " class {}: {}.".format(cls.canonical_name, ", ".join(missing_items)) + ) + + +def clean_class(cls: Class): + """ + Clean the given class object. + + Args: + cls: The class object to clean. + """ + + # Remove duplicate methods (based on name and argument types): + methods: dict[tuple[str, tuple[Argument, ...]], list[Method]] = OrderedDict() + methods_by_name: dict[str, list[Method]] = defaultdict(list) + for m in cls.methods: + k = (m.name, tuple(m.args if m.is_static() else m.args[1:])) + if k not in methods: + methods[k] = [] + methods[k].append(m) + methods_by_name[m.name].append(m) + + clean_methods: list[Method] = [] + for name, args in methods: + ms = methods[name, args] + method: Method = ms[0] + if len(ms) > 1: + # If we have more than two methods, there is a problem... + assert len(methods[name, args]) == 2 + assert ( + ms[0].ret.type.is_none() + or ms[1].ret.type.is_none() + or ms[0].ret.type.name == ms[1].ret.type.name + ) + + if ms[0].ret.type.is_none(): + # If both are None, we need to take the first one because the second + # one does not contains the name of the arguments, for whatever reason. + if ms[1].ret.type.is_none(): + method = ms[0] + else: + method = ms[1] + else: + method = ms[0] + + # If those were the only two, we need to remove the overload: + if len(methods_by_name[name]) == 2: + method.overloads = False + + # Filter methods from parent class: + if method.is_static(): + clean_methods.append(method) + else: + arg0_name = method.args[0].type.name + # print(arg0_name, [cls.name, cls.canonical_name, cls.full_name, "object"]) + if arg0_name in [cls.full_name, "object"]: + clean_methods.append(method) + else: + LOGGER.info( + "Removing {}({}) from {} (already in base {}).".format( + name, + ", ".join(a.type.typing() for a in method.args), + cls.name, + method.args[0].type.typing(), + ) + ) + + if method.name != "__init__": + # we need to fix the overload from the base class + for base_class in cls.all_bases: + for base_method in base_class.methods: + if base_method.name == method.name: + # we need to bring all overloads + base_method = Method( + base_method.name, + ret=base_method.ret, + args=base_method.args, + static=False, + has_overloads=True, + doc=base_method.doc, + ) + base_method.cls = cls + clean_methods.insert(-1, base_method) + + method.overloads = True + + cls.methods = clean_methods + + # Remove all non-uppercases enum names - This is a temporary fix to avoid breaking + # old plugins that uses old enum values: + if isinstance(cls, Enum): + new_constants = [c for c in cls.constants if c.name.isupper()] + if new_constants: + cls.constants = new_constants + + # pybind11 do not properly type arithmetic methods so we fix them here, so we + # replace every "object" to the type when possible + + cls_type = PyType(cls.canonical_name) + for method in cls.methods: + if method.name in ["__eq__", "__ne__"]: + continue + + for arg in method.args: + if arg.type.is_object(): + arg.type = cls_type + + if method.ret.type.is_object(): + method.ret.type = cls_type + + # add two properties name, value + cls.properties = [ + Property("value", PyType(int), read_only=True), + Property("name", PyType(str), read_only=True), + ] + + # Clean inner classes: + for ic in cls.inner_classes: + clean_class(ic) diff --git a/generator/writer.py b/src/mo2/stubs/generator/writer.py similarity index 67% rename from generator/writer.py rename to src/mo2/stubs/generator/writer.py index 43a4059..f7e93d4 100644 --- a/generator/writer.py +++ b/src/mo2/stubs/generator/writer.py @@ -1,24 +1,40 @@ -# -*- encoding: utf-8 -*- +import logging +from typing import Any, Iterable, TextIO, TypeGuard -from typing import TextIO, List, Union, Tuple - -from . import logger -from .mtypes import Function, Class, Method, Property, Enum +from .mtypes import Class, 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) + class Writer: - _output: TextIO _settings: Settings - def __init__(self, output: TextIO, settings: Settings): + def __init__(self, package: str, output: TextIO, settings: Settings): + self._package = package.split(".") self._output = output self._settings = settings - def _print(self, *args, **kwargs): - kwargs["file"] = self._output - print(*args, **kwargs) + def _fix_typing(self, value: str) -> str: + for pkg in self._package: + value = value.replace(pkg + ".", "") + return value + + def _print( + self, + *values: object, + sep: str | None = " ", + end: str | None = "\n", + flush: bool = False, + ) -> None: + print(*values, sep=sep, end=end, flush=flush, file=self._output) def _print_doc(self, doc: str, indent: str): """ @@ -36,7 +52,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 +71,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 = " -> " + self._fix_typing(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 +82,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 arg in fn.args: + tmp = "{}: {}".format(arg.name, self._fix_typing(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 +109,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" @@ -105,7 +124,7 @@ class Writer: for rai in fn.raises: doc += ( " " - + rai.type.typing(self._settings) + + self._fix_typing(rai.type.typing()) + ": " + rai.doc.strip() + "\n" @@ -127,7 +146,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 ) @@ -136,14 +155,14 @@ class Writer: self._print("{}@property".format(indent)) self._print( "{}def {}(self) -> {}: ...".format( - indent, prop.name, prop.type.typing(self._settings) + indent, prop.name, self._fix_typing(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, self._fix_typing(prop.type.typing()) ) ) self._print() @@ -155,7 +174,10 @@ class Writer: bc = "" if cls.bases or cls.is_abstract(): - bases = [str(bc) for bc in cls.bases] + bases: list[str] = [ + bc.canonical_name if bc.package.startswith("mobase") else bc.full_name + for bc in cls.bases + ] if cls.is_abstract() and not any(bc.is_abstract() for bc in cls.bases): bases.insert(0, "abc.ABC") bc = "(" + ", ".join(bases) + ")" @@ -179,8 +201,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 +213,7 @@ class Writer: typing = "" if constant.type is not None: - typing = ": {}".format(constant.type.typing(self._settings)) + typing = ": {}".format(self._fix_typing(constant.type.typing())) # Note: We do not print the value, we use ... self._print( @@ -217,6 +239,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 + " ") @@ -227,3 +250,17 @@ class Writer: if isinstance(cls, Enum): self._print() self._print() + + def print_typing(self, typ: PyTyping): + self._print(f"{typ.name} = {typ.typing}") + + def print_object(self, e: object): + if isinstance(e, Class): + self.print_class(e) + + elif is_list_of_functions(e): + for fn in e: + self.print_function(fn) + + elif isinstance(e, PyTyping): + self.print_typing(e) diff --git a/stubs/setup/mobase-stubs/__init__.pyi b/stubs/2.5.0/mobase-stubs/__init__.pyi similarity index 73% rename from stubs/setup/mobase-stubs/__init__.pyi rename to stubs/2.5.0/mobase-stubs/__init__.pyi index 245a2be..d86f028 100644 --- a/stubs/setup/mobase-stubs/__init__.pyi +++ b/stubs/2.5.0/mobase-stubs/__init__.pyi @@ -1,30 +1,35 @@ -__version__ = "2.4.0" +from __future__ import annotations + +__version__ = "2.5.0" import abc +import os from enum import Enum from typing import ( + Callable, Dict, Iterator, List, - Tuple, - Union, - Any, Optional, - Callable, - overload, - TypeVar, + Sequence, + Set, + Tuple, Type, + TypeVar, + Union, + overload, ) -import PyQt5.QtCore -import PyQt5.QtGui -import PyQt5.QtWidgets -MoVariant = Union[None, bool, int, str, List[Any], Dict[str, Any]] +import PyQt6.QtCore +import PyQt6.QtGui +import PyQt6.QtWidgets + GameFeatureType = TypeVar("GameFeatureType") +MoVariant = None | bool | int | str | list[object] | dict[str, object] -class InterfaceNotImplemented: ... - -def getFileVersion(filepath: str) -> str: +def getFileVersion( + filepath: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo] +) -> str: """ Retrieve the file version of the given executable. @@ -36,7 +41,9 @@ def getFileVersion(filepath: str) -> str: """ ... -def getIconForExecutable(executable: str) -> PyQt5.QtGui.QIcon: +def getIconForExecutable( + executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo] +) -> PyQt6.QtGui.QIcon: """ Retrieve the icon of an executable. Currently this always extracts the biggest icon. @@ -48,7 +55,9 @@ def getIconForExecutable(executable: str) -> PyQt5.QtGui.QIcon: """ ... -def getProductVersion(executable: str) -> str: +def getProductVersion( + executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo] +) -> str: """ Retrieve the product version of the given executable. @@ -65,10 +74,18 @@ class EndorsedState(Enum): ENDORSED_TRUE = ... ENDORSED_UNKNOWN = ... ENDORSED_NEVER = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: EndorsedState, other: object) -> bool: ... + def __ge__(self: EndorsedState, other: EndorsedState) -> bool: ... + def __gt__(self: EndorsedState, other: EndorsedState) -> bool: ... + def __int__(self: EndorsedState) -> int: ... + def __le__(self: EndorsedState, other: EndorsedState) -> bool: ... + def __lt__(self: EndorsedState, other: EndorsedState) -> bool: ... + def __ne__(self: EndorsedState, other: object) -> bool: ... class GuessQuality(Enum): """ @@ -82,10 +99,14 @@ class GuessQuality(Enum): META = ... PRESET = ... USER = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: GuessQuality, other: object) -> bool: ... + def __int__(self: GuessQuality) -> int: ... + def __ne__(self: GuessQuality, other: object) -> bool: ... class InstallResult(Enum): SUCCESS = ... @@ -93,18 +114,27 @@ class InstallResult(Enum): CANCELED = ... MANUAL_REQUESTED = ... NOT_ATTEMPTED = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: InstallResult, other: object) -> bool: ... + def __int__(self: InstallResult) -> int: ... + def __ne__(self: InstallResult, other: object) -> bool: ... class LoadOrderMechanism(Enum): + NONE = ... FILE_TIME = ... PLUGINS_TXT = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: LoadOrderMechanism, other: object) -> bool: ... + def __int__(self: LoadOrderMechanism) -> int: ... + def __ne__(self: LoadOrderMechanism, other: object) -> bool: ... class ModState(Enum): EXISTS = ... @@ -114,59 +144,120 @@ class ModState(Enum): ENDORSED = ... VALID = ... ALTERNATE = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __and__(self: ModState, other: ModState) -> ModState: ... + def __eq__(self: ModState, other: object) -> bool: ... + def __ge__(self: ModState, other: ModState) -> bool: ... + def __gt__(self: ModState, other: ModState) -> bool: ... + def __int__(self: ModState) -> int: ... + def __invert__(self: ModState) -> ModState: ... + def __le__(self: ModState, other: ModState) -> bool: ... + def __lt__(self: ModState, other: ModState) -> bool: ... + def __ne__(self: ModState, other: object) -> bool: ... + def __or__(self: ModState, other: ModState) -> ModState: ... + def __rand__(self: ModState, other: ModState) -> ModState: ... + def __ror__(self: ModState, other: ModState) -> ModState: ... + def __rxor__(self: ModState, other: ModState) -> ModState: ... + def __xor__(self: ModState, other: ModState) -> ModState: ... class PluginState(Enum): MISSING = ... INACTIVE = ... ACTIVE = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __and__(self: PluginState, other: PluginState) -> PluginState: ... + def __eq__(self: PluginState, other: object) -> bool: ... + def __ge__(self: PluginState, other: PluginState) -> bool: ... + def __gt__(self: PluginState, other: PluginState) -> bool: ... + def __int__(self: PluginState) -> int: ... + def __invert__(self: PluginState) -> PluginState: ... + def __le__(self: PluginState, other: PluginState) -> bool: ... + def __lt__(self: PluginState, other: PluginState) -> bool: ... + def __ne__(self: PluginState, other: object) -> bool: ... + def __or__(self: PluginState, other: PluginState) -> PluginState: ... + def __rand__(self: PluginState, other: PluginState) -> PluginState: ... + def __ror__(self: PluginState, other: PluginState) -> PluginState: ... + def __rxor__(self: PluginState, other: PluginState) -> PluginState: ... + def __xor__(self: PluginState, other: PluginState) -> PluginState: ... class ProfileSetting(Enum): MODS = ... CONFIGURATION = ... SAVEGAMES = ... PREFER_DEFAULTS = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __and__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... + def __eq__(self: ProfileSetting, other: object) -> bool: ... + def __ge__(self: ProfileSetting, other: ProfileSetting) -> bool: ... + def __gt__(self: ProfileSetting, other: ProfileSetting) -> bool: ... + def __int__(self: ProfileSetting) -> int: ... + def __invert__(self: ProfileSetting) -> ProfileSetting: ... + def __le__(self: ProfileSetting, other: ProfileSetting) -> bool: ... + def __lt__(self: ProfileSetting, other: ProfileSetting) -> bool: ... + def __ne__(self: ProfileSetting, other: object) -> bool: ... + def __or__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... + def __rand__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... + def __ror__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... + def __rxor__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... + def __xor__(self: ProfileSetting, other: ProfileSetting) -> ProfileSetting: ... class ReleaseType(Enum): - PRE_ALPHA = ... - ALPHA = ... - BETA = ... - CANDIDATE = ... FINAL = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + CANDIDATE = ... + BETA = ... + ALPHA = ... + PRE_ALPHA = ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: ReleaseType, other: object) -> bool: ... + def __int__(self: ReleaseType) -> int: ... + def __ne__(self: ReleaseType, other: object) -> bool: ... class SortMechanism(Enum): NONE = ... MLOX = ... BOSS = ... LOOT = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: SortMechanism, other: object) -> bool: ... + def __int__(self: SortMechanism) -> int: ... + def __ne__(self: SortMechanism, other: object) -> bool: ... class TrackedState(Enum): TRACKED_FALSE = ... TRACKED_TRUE = ... TRACKED_UNKNOWN = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: TrackedState, other: object) -> bool: ... + def __ge__(self: TrackedState, other: TrackedState) -> bool: ... + def __gt__(self: TrackedState, other: TrackedState) -> bool: ... + def __int__(self: TrackedState) -> int: ... + def __le__(self: TrackedState, other: TrackedState) -> bool: ... + def __lt__(self: TrackedState, other: TrackedState) -> bool: ... + def __ne__(self: TrackedState, other: object) -> bool: ... class VersionScheme(Enum): DISCOVER = ... @@ -175,24 +266,28 @@ class VersionScheme(Enum): NUMBERS_AND_LETTERS = ... DATE = ... LITERAL = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: VersionScheme, other: object) -> bool: ... + def __int__(self: VersionScheme) -> int: ... + def __ne__(self: VersionScheme, other: object) -> bool: ... class BSAInvalidation(abc.ABC): - def __init__(self): ... + def __init__(self: BSAInvalidation): ... @abc.abstractmethod - def activate(self, profile: "IProfile"): ... + def activate(self: BSAInvalidation, profile: IProfile): ... @abc.abstractmethod - def deactivate(self, profile: "IProfile"): ... + def deactivate(self: BSAInvalidation, profile: IProfile): ... @abc.abstractmethod - def isInvalidationBSA(self, name: str) -> bool: ... + def isInvalidationBSA(self: BSAInvalidation, name: str) -> bool: ... class DataArchives(abc.ABC): - def __init__(self): ... + def __init__(self: DataArchives): ... @abc.abstractmethod - def addArchive(self, profile: "IProfile", index: int, name: str): + def addArchive(self: DataArchives, profile: IProfile, index: int, name: str): """ Add an archive to the archive list. @@ -204,7 +299,7 @@ class DataArchives(abc.ABC): """ ... @abc.abstractmethod - def archives(self, profile: "IProfile") -> List[str]: + def archives(self: DataArchives, profile: IProfile) -> Sequence[str]: """ Retrieve the list of archives in the given profile. @@ -216,7 +311,7 @@ class DataArchives(abc.ABC): """ ... @abc.abstractmethod - def removeArchive(self, profile: "IProfile", name: str): + def removeArchive(self: DataArchives, profile: IProfile, name: str): """ Remove the given archive from the given profile. @@ -226,7 +321,7 @@ class DataArchives(abc.ABC): """ ... @abc.abstractmethod - def vanillaArchives(self) -> List[str]: + def vanillaArchives(self: DataArchives) -> Sequence[str]: """ Retrieve the list of vanilla archives. @@ -239,29 +334,37 @@ class DataArchives(abc.ABC): ... class ExecutableForcedLoadSetting: - def __init__(self, process: str, library: str): ... - def enabled(self) -> bool: ... - def forced(self) -> bool: ... - def library(self) -> str: ... - def process(self) -> str: ... - def withEnabled(self, enabled: bool) -> "ExecutableForcedLoadSetting": ... - def withForced(self, forced: bool) -> "ExecutableForcedLoadSetting": ... + def __init__(self: ExecutableForcedLoadSetting, process: str, library: str): ... + def enabled(self: ExecutableForcedLoadSetting) -> bool: ... + def forced(self: ExecutableForcedLoadSetting) -> bool: ... + def library(self: ExecutableForcedLoadSetting) -> str: ... + def process(self: ExecutableForcedLoadSetting) -> str: ... + def withEnabled( + self: ExecutableForcedLoadSetting, enabled: bool + ) -> ExecutableForcedLoadSetting: ... + def withForced( + self: ExecutableForcedLoadSetting, forced: bool + ) -> ExecutableForcedLoadSetting: ... class ExecutableInfo: - def __init__(self, title: str, binary: PyQt5.QtCore.QFileInfo): ... - def arguments(self) -> List[str]: ... - def asCustom(self) -> "ExecutableInfo": ... - def binary(self) -> PyQt5.QtCore.QFileInfo: ... - def isCustom(self) -> bool: ... - def isValid(self) -> bool: ... - def steamAppID(self) -> str: ... - def title(self) -> str: ... - def withArgument(self, argument: str) -> "ExecutableInfo": ... - def withSteamAppId(self, app_id: str) -> "ExecutableInfo": ... + def __init__( + self: ExecutableInfo, + title: str, + binary: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo], + ): ... + def arguments(self: ExecutableInfo) -> Sequence[str]: ... + def asCustom(self: ExecutableInfo) -> ExecutableInfo: ... + def binary(self: ExecutableInfo) -> PyQt6.QtCore.QFileInfo: ... + def isCustom(self: ExecutableInfo) -> bool: ... + def isValid(self: ExecutableInfo) -> bool: ... + def steamAppID(self: ExecutableInfo) -> str: ... + def title(self: ExecutableInfo) -> str: ... + def withArgument(self: ExecutableInfo, argument: str) -> ExecutableInfo: ... + def withSteamAppId(self: ExecutableInfo, app_id: str) -> ExecutableInfo: ... def withWorkingDirectory( - self, directory: PyQt5.QtCore.QDir - ) -> "ExecutableInfo": ... - def workingDirectory(self) -> PyQt5.QtCore.QDir: ... + self: ExecutableInfo, directory: Union[str, os.PathLike[str], PyQt6.QtCore.QDir] + ) -> ExecutableInfo: ... + def workingDirectory(self: ExecutableInfo) -> PyQt6.QtCore.QDir: ... class FileInfo: """ @@ -280,7 +383,7 @@ class FileInfo: def origins(self) -> List[str]: ... @origins.setter def origins(self, arg0: List[str]): ... - def __init__(self): + def __init__(self: FileInfo): """ Creates an uninitialized FileInfo. """ @@ -303,24 +406,55 @@ class FileTreeEntry: Enumeration of the different file type or combinations. """ - DIRECTORY = ... FILE = ... + DIRECTORY = ... FILE_OR_DIRECTORY = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... - DIRECTORY: "FileTreeEntry.FileTypes" = ... - FILE: "FileTreeEntry.FileTypes" = ... - FILE_OR_DIRECTORY: "FileTreeEntry.FileTypes" = ... - @overload - def __eq__(self, arg2: str) -> bool: ... - @overload - def __eq__(self, arg2: "FileTreeEntry") -> bool: ... - @overload - def __eq__(self, other: object) -> bool: ... - def __repr__(self) -> str: ... - def detach(self) -> bool: + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __and__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + def __eq__(self: FileTreeEntry.FileTypes, other: object) -> bool: ... + def __ge__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> bool: ... + def __gt__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> bool: ... + def __int__(self: FileTreeEntry.FileTypes) -> int: ... + def __invert__(self: FileTreeEntry.FileTypes) -> FileTreeEntry.FileTypes: ... + def __le__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> bool: ... + def __lt__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> bool: ... + def __ne__(self: FileTreeEntry.FileTypes, other: object) -> bool: ... + def __or__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + def __rand__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + def __ror__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + def __rxor__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + def __xor__( + self: FileTreeEntry.FileTypes, other: FileTreeEntry.FileTypes + ) -> FileTreeEntry.FileTypes: ... + + DIRECTORY: FileTypes = ... + FILE: FileTypes = ... + FILE_OR_DIRECTORY: FileTypes = ... + + def __eq__(self: FileTreeEntry, other: object) -> bool: ... + def detach(self: FileTreeEntry) -> bool: """ Detach this entry from its parent tree. @@ -328,14 +462,14 @@ class FileTreeEntry: True if the entry was removed correctly, False otherwise. """ ... - def fileType(self) -> "FileTreeEntry.FileTypes": + def fileType(self: FileTreeEntry) -> FileTreeEntry.FileTypes: """ Returns: The filetype of this entry. """ ... @overload - def hasSuffix(self, suffixes: List[str]) -> bool: + def hasSuffix(self: FileTreeEntry, suffixes: Sequence[str]) -> bool: """ Check if this entry has one of the given suffixes. @@ -347,7 +481,7 @@ class FileTreeEntry: """ ... @overload - def hasSuffix(self, suffix: str) -> bool: + def hasSuffix(self: FileTreeEntry, suffix: str) -> bool: """ Check if this entry has the given suffix. @@ -358,19 +492,19 @@ class FileTreeEntry: True if this entry is a file and has the given suffix. """ ... - def isDir(self) -> bool: + def isDir(self: FileTreeEntry) -> bool: """ Returns: True if this entry is a directory, False otherwise. """ ... - def isFile(self) -> bool: + def isFile(self: FileTreeEntry) -> bool: """ Returns: True if this entry is a file, False otherwise. """ ... - def moveTo(self, tree: "IFileTree") -> bool: + def moveTo(self: FileTreeEntry, tree: IFileTree) -> bool: """ Move this entry to the given tree. @@ -381,20 +515,20 @@ class FileTreeEntry: True if the entry was moved correctly, False otherwise. """ ... - def name(self) -> str: + def name(self: FileTreeEntry) -> str: """ Returns: The name of this entry. """ ... - def parent(self) -> Optional["IFileTree"]: + def parent(self: FileTreeEntry) -> Optional[IFileTree]: """ Returns: The parent tree containing this entry, or a `None` if this entry is the root or the parent tree is unreachable. """ ... - def path(self, sep: str = "\\") -> str: + def path(self: FileTreeEntry, sep: str = "\\") -> str: """ Retrieve the path from this entry up to the root of the tree. @@ -408,7 +542,7 @@ class FileTreeEntry: The path from this entry to the root, including the name of this entry. """ ... - def pathFrom(self, tree: "IFileTree", sep: str = "\\") -> str: + def pathFrom(self: FileTreeEntry, tree: IFileTree, sep: str = "\\") -> str: """ Retrieve the path from the given tree to this entry. @@ -421,7 +555,7 @@ class FileTreeEntry: an empty string if the given tree is not a parent of this entry. """ ... - def suffix(self) -> str: + def suffix(self: FileTreeEntry) -> str: """ Retrieve the "last" extension of this entry. @@ -434,20 +568,27 @@ class FileTreeEntry: ... class GamePlugins(abc.ABC): - def __init__(self): ... + def __init__(self: GamePlugins): ... @abc.abstractmethod - def getLoadOrder(self) -> List[str]: ... + def getLoadOrder(self: GamePlugins) -> Sequence[str]: ... @abc.abstractmethod - def lightPluginsAreSupported(self) -> bool: + def lightPluginsAreSupported(self: GamePlugins) -> bool: """ Returns: True if light plugins are supported, False otherwise. """ ... @abc.abstractmethod - def readPluginLists(self, plugin_list: "IPluginList"): ... + def overridePluginsAreSupported(self: GamePlugins) -> bool: + """ + Returns: + True if override plugins are supported, False otherwise. + """ + ... @abc.abstractmethod - def writePluginLists(self, plugin_list: "IPluginList"): ... + def readPluginLists(self: GamePlugins, plugin_list: IPluginList): ... + @abc.abstractmethod + def writePluginLists(self: GamePlugins, plugin_list: IPluginList): ... class GuessedString: """ @@ -458,13 +599,15 @@ class GuessedString: """ @overload - def __init__(self): + def __init__(self: GuessedString): """ Creates a GuessedString with no associated value. """ ... @overload - def __init__(self, value: str, quality: "GuessQuality" = GuessQuality.USER): + def __init__( + self: GuessedString, value: str, quality: GuessQuality = GuessQuality.USER + ): """ Creates a GuessedString with the given value and quality. @@ -473,9 +616,9 @@ class GuessedString: quality: Quality of the initial value. """ ... - def __str__(self) -> str: ... + def __str__(self: GuessedString) -> str: ... @overload - def reset(self) -> "GuessedString": + def reset(self: GuessedString) -> GuessedString: """ Reset this GuessedString to an invalid state. @@ -484,7 +627,7 @@ class GuessedString: """ ... @overload - def reset(self, value: str, quality: "GuessQuality") -> "GuessedString": + def reset(self: GuessedString, value: str, quality: GuessQuality) -> GuessedString: """ Reset this GuessedString object with the given value and quality, only if the given quality is better than the current one. @@ -498,7 +641,7 @@ class GuessedString: """ ... @overload - def reset(self, other: "GuessedString") -> "GuessedString": + def reset(self: GuessedString, other: GuessedString) -> GuessedString: """ Reset this GuessedString object by copying the given one, only if the given one has better quality. @@ -510,7 +653,7 @@ class GuessedString: This GuessedString object. """ ... - def setFilter(self, filter: Callable[[str], Union[str, bool]]): + def setFilter(self: GuessedString, filter: Callable[[str], Union[str, bool]]): """ Set the filter for this GuessedString. @@ -522,7 +665,7 @@ class GuessedString: """ ... @overload - def update(self, value: str) -> "GuessedString": + def update(self: GuessedString, value: str) -> GuessedString: """ Update this GuessedString by adding the given value to the list of variants and setting the actual value without changing the current quality of this @@ -538,7 +681,7 @@ class GuessedString: """ ... @overload - def update(self, value: str, quality: "GuessQuality") -> "GuessedString": + def update(self: GuessedString, value: str, quality: GuessQuality) -> GuessedString: """ Update this GuessedString by adding a new variants with the given quality. @@ -555,7 +698,7 @@ class GuessedString: This GuessedString object. """ ... - def variants(self) -> List[str]: + def variants(self: GuessedString) -> Set[str]: """ Returns: The list of variants for this GuessedString. @@ -563,7 +706,7 @@ class GuessedString: ... class IDownloadManager: - def downloadPath(self, id: int) -> str: + def downloadPath(self: IDownloadManager, id: int) -> str: """ Retrieve the (absolute) path of the specified download. @@ -575,7 +718,9 @@ class IDownloadManager: may not exist yet if the download is incomplete. """ ... - def onDownloadComplete(self, callback: Callable[[int], None]) -> bool: + def onDownloadComplete( + self: IDownloadManager, callback: Callable[[int], None] + ) -> bool: """ Installs a handler to be called when a download completes. @@ -586,7 +731,9 @@ class IDownloadManager: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onDownloadFailed(self, callback: Callable[[int], None]) -> bool: + def onDownloadFailed( + self: IDownloadManager, callback: Callable[[int], None] + ) -> bool: """ Installs a handler to be called when a download fails. @@ -597,7 +744,9 @@ class IDownloadManager: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onDownloadPaused(self, callback: Callable[[int], None]) -> bool: + def onDownloadPaused( + self: IDownloadManager, callback: Callable[[int], None] + ) -> bool: """ Installs a handler to be called when a download is paused. @@ -608,7 +757,9 @@ class IDownloadManager: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onDownloadRemoved(self, callback: Callable[[int], None]) -> bool: + def onDownloadRemoved( + self: IDownloadManager, callback: Callable[[int], None] + ) -> bool: """ Installs a handler to be called when a download is removed. @@ -619,7 +770,9 @@ class IDownloadManager: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def startDownloadNexusFile(self, mod_id: int, file_id: int) -> int: + def startDownloadNexusFile( + self: IDownloadManager, mod_id: int, file_id: int + ) -> int: """ Download a file from www.nexusmods.com/. is always the game currently being managed. @@ -632,7 +785,7 @@ class IDownloadManager: An ID identifying the download. """ ... - def startDownloadURLs(self, urls: List[str]) -> int: + def startDownloadURLs(self: IDownloadManager, urls: Sequence[str]) -> int: """ Download a file by url. @@ -681,10 +834,15 @@ class IFileTree(FileTreeEntry): FAIL_IF_EXISTS = ... REPLACE = ... MERGE = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: IFileTree.InsertPolicy, other: object) -> bool: ... + def __int__(self: IFileTree.InsertPolicy) -> int: ... + def __ne__(self: IFileTree.InsertPolicy, other: object) -> bool: ... + class WalkReturn(Enum): """ Enumeration that can be returned by the callback for the `walk()` method to stop the @@ -694,23 +852,29 @@ class IFileTree(FileTreeEntry): CONTINUE = ... STOP = ... SKIP = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... - CONTINUE: "IFileTree.WalkReturn" = ... - FAIL_IF_EXISTS: "IFileTree.InsertPolicy" = ... - MERGE: "IFileTree.InsertPolicy" = ... - REPLACE: "IFileTree.InsertPolicy" = ... - SKIP: "IFileTree.WalkReturn" = ... - STOP: "IFileTree.WalkReturn" = ... - def __bool__(self) -> bool: + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: IFileTree.WalkReturn, other: object) -> bool: ... + def __int__(self: IFileTree.WalkReturn) -> int: ... + def __ne__(self: IFileTree.WalkReturn, other: object) -> bool: ... + + CONTINUE: WalkReturn = ... + FAIL_IF_EXISTS: InsertPolicy = ... + MERGE: InsertPolicy = ... + REPLACE: InsertPolicy = ... + SKIP: WalkReturn = ... + STOP: WalkReturn = ... + + def __bool__(self: IFileTree) -> bool: """ Returns: True if this tree is not empty, False otherwise. """ ... - def __getitem__(self, index: int) -> "FileTreeEntry": + def __getitem__(self: IFileTree, index: int) -> FileTreeEntry: """ Retrieve the entry at the given index in this tree. @@ -724,7 +888,7 @@ class IFileTree(FileTreeEntry): IndexError: If the given index is not in range for this tree. """ ... - def __iter__(self) -> Iterator[FileTreeEntry]: + def __iter__(self: IFileTree) -> Iterator[FileTreeEntry]: """ Retrieves an iterator for entries directly under this tree. @@ -734,13 +898,13 @@ class IFileTree(FileTreeEntry): An iterator object that can be used to iterate over entries in this tree. """ ... - def __len__(self) -> int: + def __len__(self: IFileTree) -> int: """ Returns: The number of entries directly under this tree. """ ... - def addDirectory(self, path: str) -> "IFileTree": + def addDirectory(self: IFileTree, path: str) -> IFileTree: """ Create a new directory tree under this tree. @@ -760,7 +924,9 @@ class IFileTree(FileTreeEntry): RuntimeError: If the directory could not be created. """ ... - def addFile(self, path: str, replace_if_exists: bool = False) -> "FileTreeEntry": + def addFile( + self: IFileTree, path: str, replace_if_exists: bool = False + ) -> FileTreeEntry: """ Create a new file directly under this tree. @@ -780,7 +946,7 @@ class IFileTree(FileTreeEntry): RuntimeError: If the file could not be created. """ ... - def clear(self) -> bool: + def clear(self: IFileTree) -> bool: """ Delete (detach) all the entries from this tree. @@ -792,11 +958,11 @@ class IFileTree(FileTreeEntry): """ ... def copy( - self, - entry: "FileTreeEntry", + self: IFileTree, + entry: FileTreeEntry, path: str = "", - insert_policy: "IFileTree.InsertPolicy" = InsertPolicy.FAIL_IF_EXISTS, - ) -> "FileTreeEntry": + insert_policy: IFileTree.InsertPolicy = InsertPolicy.FAIL_IF_EXISTS, + ) -> FileTreeEntry: """ Move the given entry to the given path under this tree. @@ -825,7 +991,7 @@ class IFileTree(FileTreeEntry): RuntimeError: If the entry could not be copied. """ ... - def createOrphanTree(self, name: str = "") -> "IFileTree": + def createOrphanTree(self: IFileTree, name: str = "") -> IFileTree: """ Create a new orphan empty tree. @@ -837,9 +1003,9 @@ class IFileTree(FileTreeEntry): """ ... def exists( - self, + self: IFileTree, path: str, - type: "FileTreeEntry.FileTypes" = FileTreeEntry.FileTypes.FILE_OR_DIRECTORY, + type: FileTreeEntry.FileTypes = FileTreeEntry.FileTypes.FILE_OR_DIRECTORY, ) -> bool: """ Check if the given entry exists. @@ -853,10 +1019,10 @@ class IFileTree(FileTreeEntry): """ ... def find( - self, + self: IFileTree, path: str, - type: "FileTreeEntry.FileTypes" = FileTreeEntry.FileTypes.FILE_OR_DIRECTORY, - ) -> Optional[Union["IFileTree", "FileTreeEntry"]]: + type: FileTreeEntry.FileTypes = FileTreeEntry.FileTypes.FILE_OR_DIRECTORY, + ) -> Optional[Union[IFileTree, FileTreeEntry]]: """ Retrieve the given entry. @@ -873,9 +1039,9 @@ class IFileTree(FileTreeEntry): """ ... def insert( - self, - entry: "FileTreeEntry", - policy: "IFileTree.InsertPolicy" = InsertPolicy.FAIL_IF_EXISTS, + self: IFileTree, + entry: FileTreeEntry, + policy: IFileTree.InsertPolicy = InsertPolicy.FAIL_IF_EXISTS, ) -> bool: """ Insert the given entry in this tree, removing it from its @@ -907,8 +1073,8 @@ class IFileTree(FileTreeEntry): """ ... def merge( - self, other: "IFileTree", overwrites: bool = False - ) -> Union[Dict["FileTreeEntry", "FileTreeEntry"], int]: + self: IFileTree, other: IFileTree, overwrites: bool = False + ) -> Union[Dict[FileTreeEntry, FileTreeEntry], int]: """ Merge the given tree with this tree, i.e., insert all entries of the given tree into this tree. @@ -941,10 +1107,10 @@ class IFileTree(FileTreeEntry): """ ... def move( - self, - entry: "FileTreeEntry", + self: IFileTree, + entry: FileTreeEntry, path: str, - policy: "IFileTree.InsertPolicy" = InsertPolicy.FAIL_IF_EXISTS, + policy: IFileTree.InsertPolicy = InsertPolicy.FAIL_IF_EXISTS, ) -> bool: """ Move the given entry to the given path under this tree. @@ -971,7 +1137,7 @@ class IFileTree(FileTreeEntry): True if the entry was moved correctly, False otherwise. """ ... - def pathTo(self, entry: "FileTreeEntry", sep: str = "\\") -> str: + def pathTo(self: IFileTree, entry: FileTreeEntry, sep: str = "\\") -> str: """ Retrieve the path from this tree to the given entry. @@ -985,7 +1151,7 @@ class IFileTree(FileTreeEntry): """ ... @overload - def remove(self, name: str) -> bool: + def remove(self: IFileTree, name: str) -> bool: """ Delete the entry with the given name. @@ -1000,7 +1166,7 @@ class IFileTree(FileTreeEntry): """ ... @overload - def remove(self, entry: "FileTreeEntry") -> bool: + def remove(self: IFileTree, entry: FileTreeEntry) -> bool: """ Delete the given entry. @@ -1011,7 +1177,7 @@ class IFileTree(FileTreeEntry): True if the entry was deleted, False otherwise. """ ... - def removeAll(self, names: List[str]) -> int: + def removeAll(self: IFileTree, names: Sequence[str]) -> int: """ Delete the entries with the given names from the tree. @@ -1025,7 +1191,7 @@ class IFileTree(FileTreeEntry): The number of deleted entry. """ ... - def removeIf(self, filter: Callable[["FileTreeEntry"], bool]) -> int: + def removeIf(self: IFileTree, filter: Callable[[FileTreeEntry], bool]) -> int: """ Delete entries matching the given predicate from the tree. @@ -1040,8 +1206,8 @@ class IFileTree(FileTreeEntry): """ ... def walk( - self, - callback: Callable[[str, "FileTreeEntry"], "IFileTree.WalkReturn"], + self: IFileTree, + callback: Callable[[str, FileTreeEntry], IFileTree.WalkReturn], sep: str = "\\", ): """ @@ -1058,7 +1224,7 @@ class IFileTree(FileTreeEntry): ... class IInstallationManager: - def createFile(self, entry: "FileTreeEntry") -> str: + def createFile(self: IInstallationManager, entry: FileTreeEntry) -> str: """ Create a new file on the disk corresponding to the given entry. @@ -1076,7 +1242,9 @@ class IInstallationManager: The path to the created file, or an empty string if the file could not be created. """ ... - def extractFile(self, entry: "FileTreeEntry", silent: bool = False) -> str: + def extractFile( + self: IInstallationManager, entry: FileTreeEntry, silent: bool = False + ) -> str: """ Extract the specified file from the currently opened archive to a temporary location. @@ -1097,8 +1265,8 @@ class IInstallationManager: """ ... def extractFiles( - self, entries: List["FileTreeEntry"], silent: bool = False - ) -> List[str]: + self: IInstallationManager, entries: List[FileTreeEntry], silent: bool = False + ) -> Sequence[str]: """ Extract the specified files from the currently opened archive to a temporary location. @@ -1118,15 +1286,18 @@ class IInstallationManager: A list containing absolute paths to the temporary files. """ ... - def getSupportedExtensions(self) -> List[str]: + def getSupportedExtensions(self: IInstallationManager) -> Sequence[str]: """ Returns: The extensions of archives supported by this installation manager. """ ... def installArchive( - self, mod_name: Union[str, "GuessedString"], archive: str, mod_id: int = 0 - ) -> Tuple["InstallResult", str, int]: + self: IInstallationManager, + mod_name: GuessedString, + archive: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo], + mod_id: int = 0, + ) -> Tuple[InstallResult, str, int]: """ Install the given archive. @@ -1141,13 +1312,13 @@ class IInstallationManager: ... class IModInterface: - def absolutePath(self) -> str: + def absolutePath(self: IModInterface) -> str: """ Returns: Absolute path to the mod to be used in file system operations. """ ... - def addCategory(self, name: str): + def addCategory(self: IModInterface, name: str): """ Assign a category to the mod. If the named category does not exist it is created. @@ -1155,7 +1326,7 @@ class IModInterface: name: Name of the new category to assign. """ ... - def addNexusCategory(self, category_id: int): + def addNexusCategory(self: IModInterface, category_id: int): """ Set the category id from a nexus category id. Conversion to MO ID happens internally. @@ -1165,13 +1336,15 @@ class IModInterface: category_id: The Nexus category ID. """ ... - def categories(self) -> List[str]: + def categories(self: IModInterface) -> Sequence[str]: """ Returns: The list of categories this mod belongs to. """ ... - def clearPluginSettings(self, plugin_name: str) -> Dict[str, MoVariant]: + def clearPluginSettings( + self: IModInterface, plugin_name: str + ) -> Dict[str, MoVariant]: """ Remove all the settings of the specified plugin this mod. @@ -1183,19 +1356,19 @@ class IModInterface: The old settings from the given plugin, as returned by `pluginSettings()`. """ ... - def color(self) -> PyQt5.QtGui.QColor: + def color(self: IModInterface) -> PyQt6.QtGui.QColor: """ Returns: The color of the 'Notes' column chosen by the user. """ ... - def comments(self) -> str: + def comments(self: IModInterface) -> str: """ Returns: The comments for this mod, if any. """ ... - def converted(self) -> bool: + def converted(self: IModInterface) -> bool: """ Check if the mod was marked as converted by the user. @@ -1206,13 +1379,13 @@ class IModInterface: True if this mod was marked as converted by the user. """ ... - def endorsedState(self) -> "EndorsedState": + def endorsedState(self: IModInterface) -> EndorsedState: """ Returns: The endorsement state of this mod. """ ... - def fileTree(self) -> "IFileTree": + def fileTree(self: IModInterface) -> IFileTree: """ Retrieve a file tree corresponding to the underlying disk content of this mod. @@ -1223,7 +1396,7 @@ class IModInterface: A file tree representing the content of this mod. """ ... - def gameName(self) -> str: + def gameName(self: IModInterface) -> str: """ Retrieve the short name of the game associated with this mod. This may differ from the current game plugin (e.g. you can install a Skyrim LE game in a SSE @@ -1233,70 +1406,70 @@ class IModInterface: The name of the game associated with this mod. """ ... - def ignoredVersion(self) -> "VersionInfo": + def ignoredVersion(self: IModInterface) -> VersionInfo: """ Returns: The ignored version of this mod (for update), or an invalid version if the user did not ignore version for this mod. """ ... - def installationFile(self) -> str: + def installationFile(self: IModInterface) -> str: """ Returns: The absolute path to the file that was used to install this mod. """ ... - def isBackup(self) -> bool: + def isBackup(self: IModInterface) -> bool: """ Returns: True if this mod represents a backup. """ ... - def isForeign(self) -> bool: + def isForeign(self: IModInterface) -> bool: """ Returns: True if this mod represents a foreign mod, not managed by MO2. """ ... - def isOverwrite(self) -> bool: + def isOverwrite(self: IModInterface) -> bool: """ Returns: True if this mod represents the overwrite mod. """ ... - def isSeparator(self) -> bool: + def isSeparator(self: IModInterface) -> bool: """ Returns: True if this mod represents a separator. """ ... - def name(self) -> str: + def name(self: IModInterface) -> str: """ Returns: The name of this mod. """ ... - def newestVersion(self) -> "VersionInfo": + def newestVersion(self: IModInterface) -> VersionInfo: """ Returns: The newest version of this mod (as known by MO2). If this matches version(), then the mod is up-to-date. """ ... - def nexusId(self) -> int: + def nexusId(self: IModInterface) -> int: """ Returns: The Nexus ID of this mod. """ ... - def notes(self) -> str: + def notes(self: IModInterface) -> str: """ Returns: The notes for this mod, if any. """ ... def pluginSetting( - self, plugin_name: str, key: str, default: MoVariant = None + self: IModInterface, plugin_name: str, key: str, default: MoVariant = None ) -> MoVariant: """ Retrieve the specified setting in this mod for a plugin. @@ -1311,7 +1484,7 @@ class IModInterface: The setting, if found, or the default value. """ ... - def pluginSettings(self, plugin_name: str) -> Dict[str, MoVariant]: + def pluginSettings(self: IModInterface, plugin_name: str) -> Dict[str, MoVariant]: """ Retrieve the settings in this mod for a plugin. @@ -1323,13 +1496,13 @@ class IModInterface: A map from setting key to value. The map is empty if there are not settings for this mod. """ ... - def primaryCategory(self) -> int: + def primaryCategory(self: IModInterface) -> int: """ Returns: The ID of the primary category of this mod. """ ... - def removeCategory(self, name: str) -> bool: + def removeCategory(self: IModInterface, name: str) -> bool: """ Unassign a category from this mod. @@ -1341,13 +1514,13 @@ class IModInterface: was assigned). """ ... - def repository(self) -> str: + def repository(self: IModInterface) -> str: """ Returns: The name of the repository from which this mod was installed. """ ... - def setGameName(self, name: str): + def setGameName(self: IModInterface, name: str): """ Set the source game of this mod. @@ -1355,7 +1528,7 @@ class IModInterface: name: The new source game short name of this mod. """ ... - def setIsEndorsed(self, endorsed: bool): + def setIsEndorsed(self: IModInterface, endorsed: bool): """ Set endorsement state of the mod. @@ -1363,7 +1536,7 @@ class IModInterface: endorsed: New endorsement state of this mod. """ ... - def setNewestVersion(self, version: "VersionInfo"): + def setNewestVersion(self: IModInterface, version: VersionInfo): """ Set the latest known version of this mod. @@ -1371,7 +1544,7 @@ class IModInterface: version: The latest known version of this mod. """ ... - def setNexusID(self, nexus_id: int): + def setNexusID(self: IModInterface, nexus_id: int): """ Set the Nexus ID of this mod. @@ -1379,7 +1552,9 @@ class IModInterface: nexus_id: Thew new Nexus ID of this mod. """ ... - def setPluginSetting(self, plugin_name: str, key: str, value: MoVariant) -> bool: + def setPluginSetting( + self: IModInterface, plugin_name: str, key: str, value: MoVariant + ) -> bool: """ Set the specified setting in this mod for a plugin. @@ -1393,7 +1568,7 @@ class IModInterface: True if the setting was set correctly, False otherwise. """ ... - def setUrl(self, url: str): + def setUrl(self: IModInterface, url: str): """ Set the URL of this mod. @@ -1401,7 +1576,7 @@ class IModInterface: url: The URL of this mod. """ ... - def setVersion(self, version: "VersionInfo"): + def setVersion(self: IModInterface, version: VersionInfo): """ Set the version of this mod. @@ -1409,20 +1584,20 @@ class IModInterface: version: The new version of this mod. """ ... - def trackedState(self) -> "TrackedState": + def trackedState(self: IModInterface) -> TrackedState: """ Returns: The tracked state of this mod. """ ... - def url(self) -> str: + def url(self: IModInterface) -> str: """ Returns: The URL of this mod, or an empty QString() if no URL is associated with this mod. """ ... - def validated(self) -> bool: + def validated(self: IModInterface) -> bool: """ Check if the mod was marked as validated by the user. @@ -1434,7 +1609,7 @@ class IModInterface: True if th is mod was marked as containing valid game data. """ ... - def version(self) -> "VersionInfo": + def version(self: IModInterface) -> VersionInfo: """ Returns: The current version of this mod. @@ -1451,19 +1626,21 @@ class IModList: to translate from display name to internal name because the display name might not me un-ambiguous. """ - def allMods(self) -> List[str]: + def allMods(self: IModList) -> Sequence[str]: """ Returns: A list containing the internal names of all installed mods. """ ... - def allModsByProfilePriority(self, profile: "IProfile" = None) -> List[str]: + def allModsByProfilePriority( + self: IModList, profile: IProfile = None + ) -> Sequence[str]: """ Returns: The list of mod (names), sorted according to the current profile priorities. """ ... - def displayName(self, name: str) -> str: + def displayName(self: IModList, name: str) -> str: """ Retrieve the display name of a mod from its internal name. @@ -1478,7 +1655,7 @@ class IModList: The display name of the given mod. """ ... - def getMod(self, name: str) -> "IModInterface": + def getMod(self: IModList, name: str) -> IModInterface: """ Retrieve an interface to a mod using its name. @@ -1489,7 +1666,9 @@ class IModList: An interface to the given mod, or `None` if there is no mod with this name. """ ... - def onModInstalled(self, callback: Callable[["IModInterface"], None]) -> bool: + def onModInstalled( + self: IModList, callback: Callable[[IModInterface], None] + ) -> bool: """ Install a new handler to be called when a new mod is installed. @@ -1501,7 +1680,7 @@ class IModList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onModMoved(self, callback: Callable[[str, int, int], None]) -> bool: + def onModMoved(self: IModList, callback: Callable[[str, int, int], None]) -> bool: """ Install a handler to be called when a mod is moved. @@ -1513,7 +1692,7 @@ class IModList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onModRemoved(self, callback: Callable[[str], None]) -> bool: + def onModRemoved(self: IModList, callback: Callable[[str], None]) -> bool: """ Install a new handler to be called when a mod is removed. @@ -1525,7 +1704,9 @@ class IModList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onModStateChanged(self, callback: Callable[[Dict[str, int]], None]) -> bool: + def onModStateChanged( + self: IModList, callback: Callable[[Dict[str, ModState]], None] + ) -> bool: """ Install a handler to be called when mod states change (enabled/disabled, endorsed, ...). @@ -1537,7 +1718,7 @@ class IModList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def priority(self, name: str) -> int: + def priority(self: IModList, name: str) -> int: """ Retrieve the priority of a mod. @@ -1548,7 +1729,7 @@ class IModList: The priority of the given mod. """ ... - def removeMod(self, mod: "IModInterface") -> bool: + def removeMod(self: IModList, mod: IModInterface) -> bool: """ Remove a mod (from disc and from the UI). @@ -1559,7 +1740,7 @@ class IModList: True if the mod was removed, False otherwise. """ ... - def renameMod(self, mod: "IModInterface", name: str) -> "IModInterface": + def renameMod(self: IModList, mod: IModInterface, name: str) -> IModInterface: """ Rename the given mod. @@ -1575,7 +1756,7 @@ class IModList: """ ... @overload - def setActive(self, names: List[str], active: bool) -> int: + def setActive(self: IModList, names: Sequence[str], active: bool) -> int: """ Enable or disable a list of mods. @@ -1591,7 +1772,7 @@ class IModList: """ ... @overload - def setActive(self, name: str, active: bool) -> bool: + def setActive(self: IModList, name: str, active: bool) -> bool: """ Enable or disable a mod. @@ -1606,7 +1787,7 @@ class IModList: True on success, False otherwise. """ ... - def setPriority(self, name: str, priority: int) -> bool: + def setPriority(self: IModList, name: str, priority: int) -> bool: """ Change the priority of a mod. @@ -1621,7 +1802,7 @@ class IModList: True if the priority was changed, False otherwise (if the name or priority were invalid). """ ... - def state(self, name: str) -> int: + def state(self: IModList, name: str) -> ModState: """ Retrieve the state of a mod. @@ -1633,23 +1814,27 @@ class IModList: """ ... -class IModRepositoryBridge(PyQt5.QtCore.QObject): - descriptionAvailable: PyQt5.QtCore.pyqtSignal = ... - filesAvailable: PyQt5.QtCore.pyqtSignal = ... - fileInfoAvailable: PyQt5.QtCore.pyqtSignal = ... - downloadURLsAvailable: PyQt5.QtCore.pyqtSignal = ... - endorsementsAvailable: PyQt5.QtCore.pyqtSignal = ... - endorsementToggled: PyQt5.QtCore.pyqtSignal = ... - trackedModsAvailable: PyQt5.QtCore.pyqtSignal = ... - trackingToggled: PyQt5.QtCore.pyqtSignal = ... - requestFailed: PyQt5.QtCore.pyqtSignal = ... - def _object(self) -> PyQt5.QtCore.QObject: +class IModRepositoryBridge(PyQt6.QtCore.QObject): + descriptionAvailable: PyQt6.QtCore.pyqtSignal = ... + filesAvailable: PyQt6.QtCore.pyqtSignal = ... + fileInfoAvailable: PyQt6.QtCore.pyqtSignal = ... + downloadURLsAvailable: PyQt6.QtCore.pyqtSignal = ... + endorsementsAvailable: PyQt6.QtCore.pyqtSignal = ... + endorsementToggled: PyQt6.QtCore.pyqtSignal = ... + trackedModsAvailable: PyQt6.QtCore.pyqtSignal = ... + trackingToggled: PyQt6.QtCore.pyqtSignal = ... + requestFailed: PyQt6.QtCore.pyqtSignal = ... + + def __getattr__(self: IModRepositoryBridge, arg0: str) -> object: ... + def _object(self: IModRepositoryBridge) -> PyQt6.QtCore.QObject: """ Returns: The underlying `QObject` for the bridge. """ ... - def requestDescription(self, game_name: str, mod_id: int, user_data: MoVariant): + def requestDescription( + self: IModRepositoryBridge, game_name: str, mod_id: int, user_data: MoVariant + ): """ Request description of a mod. @@ -1660,7 +1845,11 @@ class IModRepositoryBridge(PyQt5.QtCore.QObject): """ ... def requestDownloadURL( - self, game_name: str, mod_id: int, file_id: int, user_data: MoVariant + self: IModRepositoryBridge, + game_name: str, + mod_id: int, + file_id: int, + user_data: MoVariant, ): """ Request download URL for mod file.0 @@ -1673,7 +1862,11 @@ class IModRepositoryBridge(PyQt5.QtCore.QObject): """ ... def requestFileInfo( - self, game_name: str, mod_id: int, file_id: int, user_data: MoVariant + self: IModRepositoryBridge, + game_name: str, + mod_id: int, + file_id: int, + user_data: MoVariant, ): """ Args: @@ -1683,7 +1876,9 @@ class IModRepositoryBridge(PyQt5.QtCore.QObject): user_data: User data to be returned with the result. """ ... - def requestFiles(self, game_name: str, mod_id: int, user_data: MoVariant): + def requestFiles( + self: IModRepositoryBridge, game_name: str, mod_id: int, user_data: MoVariant + ): """ Request the list of files belonging to a mod. @@ -1694,7 +1889,7 @@ class IModRepositoryBridge(PyQt5.QtCore.QObject): """ ... def requestToggleEndorsement( - self, + self: IModRepositoryBridge, game_name: str, mod_id: int, mod_version: str, @@ -1717,19 +1912,19 @@ class IOrganizer: of Mod Organizer to be used by plugins. """ - def appVersion(self) -> "VersionInfo": + def appVersion(self: IOrganizer) -> VersionInfo: """ Returns: The running version of Mod Organizer. """ ... - def basePath(self) -> str: + def basePath(self: IOrganizer) -> str: """ Returns: The absolute path to the base directory of Mod Organizer. """ ... - def createMod(self, name: "GuessedString") -> "IModInterface": + def createMod(self: IOrganizer, name: GuessedString) -> IModInterface: """ Create a new mod with the specified name. @@ -1744,7 +1939,7 @@ class IOrganizer: could not be created. """ ... - def createNexusBridge(self) -> "IModRepositoryBridge": + def createNexusBridge(self: IOrganizer) -> IModRepositoryBridge: """ Create a new Nexus interface. @@ -1752,21 +1947,23 @@ class IOrganizer: The newly created Nexus interface. """ ... - def downloadManager(self) -> "IDownloadManager": + def downloadManager(self: IOrganizer) -> IDownloadManager: """ Returns: The interface to the download manager. """ ... - def downloadsPath(self) -> str: + def downloadsPath(self: IOrganizer) -> str: """ Returns: The absolute path to the download directory. """ ... def findFileInfos( - self, path: str, filter: Callable[["FileInfo"], bool] - ) -> List["FileInfo"]: + self: IOrganizer, + path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir], + filter: Callable[[FileInfo], bool], + ) -> Sequence[FileInfo]: """ Find files in the virtual directory matching the specified filter. @@ -1779,7 +1976,11 @@ class IOrganizer: """ ... @overload - def findFiles(self, path: str, filter: Callable[[str], bool]) -> List[str]: + def findFiles( + self: IOrganizer, + path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir], + filter: Callable[[str], bool], + ) -> Sequence[str]: """ Find files in the given folder that matches the given filter. @@ -1792,7 +1993,11 @@ class IOrganizer: """ ... @overload - def findFiles(self, path: str, patterns: List[str]) -> List[str]: + def findFiles( + self: IOrganizer, + path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir], + patterns: Sequence[str], + ) -> Sequence[str]: """ Find files in the given folder that matches one of the given glob patterns. @@ -1805,7 +2010,11 @@ class IOrganizer: """ ... @overload - def findFiles(self, path: str, pattern: str) -> List[str]: + def findFiles( + self: IOrganizer, + path: Union[str, os.PathLike[str], PyQt6.QtCore.QDir], + pattern: str, + ) -> Sequence[str]: """ Find files in the given folder that matches the given glob pattern. @@ -1817,7 +2026,7 @@ class IOrganizer: The list of matching files. """ ... - def getFileOrigins(self, filename: str) -> List[str]: + def getFileOrigins(self: IOrganizer, filename: str) -> Sequence[str]: """ Retrieve the file origins for the specified file. @@ -1831,7 +2040,7 @@ class IOrganizer: The list of origins that contain the specified file, sorted by their priority. """ ... - def getGame(self, name: str) -> "IPluginGame": + def getGame(self: IOrganizer, name: str) -> IPluginGame: """ Retrieve the game plugin matching the given name. @@ -1849,7 +2058,11 @@ class IOrganizer: The directory for plugin data, typically plugins/data. """ ... - def installMod(self, filename: str, name_suggestion: str = "") -> "IModInterface": + def installMod( + self: IOrganizer, + filename: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo], + name_suggestion: str = "", + ) -> IModInterface: """ Install a mod archive at the specified location. @@ -1862,7 +2075,7 @@ class IOrganizer: """ ... @overload - def isPluginEnabled(self, plugin: "IPlugin") -> bool: + def isPluginEnabled(self: IOrganizer, plugin: IPlugin) -> bool: """ Check if a plugin is enabled. @@ -1874,7 +2087,7 @@ class IOrganizer: """ ... @overload - def isPluginEnabled(self, plugin: str) -> bool: + def isPluginEnabled(self: IOrganizer, plugin: str) -> bool: """ Check if a plugin is enabled. @@ -1885,7 +2098,7 @@ class IOrganizer: True if the plugin is enabled, False otherwise. """ ... - def listDirectories(self, directory: str) -> List[str]: + def listDirectories(self: IOrganizer, directory: str) -> Sequence[str]: """ Retrieve the list of (virtual) subdirectories in the given path. @@ -1896,13 +2109,13 @@ class IOrganizer: The list of directories in the given directory. """ ... - def managedGame(self) -> "IPluginGame": + def managedGame(self: IOrganizer) -> IPluginGame: """ Returns: The plugin corresponding to the current game. """ ... - def modDataChanged(self, mod: "IModInterface"): + def modDataChanged(self: IOrganizer, mod: IModInterface): """ Notify the organizer that the given mod has changed. @@ -1910,19 +2123,19 @@ class IOrganizer: mod: The mod that has changed. """ ... - def modList(self) -> "IModList": + def modList(self: IOrganizer) -> IModList: """ Returns: The interface to the mod list. """ ... - def modsPath(self) -> str: + def modsPath(self: IOrganizer) -> str: """ Returns: The (absolute) path to the mods directory. """ ... - def onAboutToRun(self, callback: Callable[[str], bool]) -> bool: + def onAboutToRun(self: IOrganizer, callback: Callable[[str], bool]) -> bool: """ Install a new handler to be called when an application is about to run. @@ -1937,7 +2150,7 @@ class IOrganizer: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onFinishedRun(self, callback: Callable[[str, int], None]) -> bool: + def onFinishedRun(self: IOrganizer, callback: Callable[[str, int], None]) -> bool: """ Install a new handler to be called when an application has finished running. @@ -1950,7 +2163,7 @@ class IOrganizer: """ ... @overload - def onPluginDisabled(self, callback: Callable[["IPlugin"], None]): + def onPluginDisabled(self: IOrganizer, callback: Callable[[IPlugin], None]): """ Install a new handler to be called when a plugin is disabled. @@ -1959,7 +2172,7 @@ class IOrganizer: """ ... @overload - def onPluginDisabled(self, name: str, callback: Callable[[], None]): + def onPluginDisabled(self: IOrganizer, name: str, callback: Callable[[], None]): """ Install a new handler to be called when the given plugin is disabled. @@ -1969,7 +2182,7 @@ class IOrganizer: """ ... @overload - def onPluginEnabled(self, callback: Callable[["IPlugin"], None]): + def onPluginEnabled(self: IOrganizer, callback: Callable[[IPlugin], None]): """ Install a new handler to be called when a plugin is enabled. @@ -1978,7 +2191,7 @@ class IOrganizer: """ ... @overload - def onPluginEnabled(self, name: str, callback: Callable[[], None]): + def onPluginEnabled(self: IOrganizer, name: str, callback: Callable[[], None]): """ Install a new handler to be called when the given plugin is enabled. @@ -1988,7 +2201,7 @@ class IOrganizer: """ ... def onPluginSettingChanged( - self, callback: Callable[[str, str, MoVariant, MoVariant], None] + self: IOrganizer, callback: Callable[[str, str, MoVariant, MoVariant], None] ) -> bool: """ Install a new handler to be called when a plugin setting is changed. @@ -2003,7 +2216,7 @@ class IOrganizer: """ ... def onProfileChanged( - self, callback: Callable[["IProfile", "IProfile"], None] + self: IOrganizer, callback: Callable[[IProfile, IProfile], None] ) -> bool: """ Install a new handler to be called when the current profile is changed. @@ -2020,7 +2233,9 @@ class IOrganizer: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onProfileCreated(self, callback: Callable[["IProfile"], None]) -> bool: + def onProfileCreated( + self: IOrganizer, callback: Callable[[IProfile], None] + ) -> bool: """ Install a new handler to be called when a new profile is created. @@ -2032,7 +2247,7 @@ class IOrganizer: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onProfileRemoved(self, callback: Callable[[str], None]) -> bool: + def onProfileRemoved(self: IOrganizer, callback: Callable[[str], None]) -> bool: """ Install a new handler to be called when a profile is remove. @@ -2048,7 +2263,7 @@ class IOrganizer: """ ... def onProfileRenamed( - self, callback: Callable[["IProfile", str, str], None] + self: IOrganizer, callback: Callable[[IProfile, str, str], None] ) -> bool: """ Install a new handler to be called when a profile is renamed. @@ -2062,7 +2277,7 @@ class IOrganizer: """ ... def onUserInterfaceInitialized( - self, callback: Callable[[PyQt5.QtWidgets.QMainWindow], None] + self: IOrganizer, callback: Callable[[PyQt6.QtWidgets.QMainWindow], None] ) -> bool: """ Install a new handler to be called when the UI has been fully initialized. @@ -2075,14 +2290,14 @@ class IOrganizer: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def overwritePath(self) -> str: + def overwritePath(self: IOrganizer) -> str: """ Returns: The (absolute) path to the overwrite directory. """ ... def persistent( - self, plugin_name: str, key: str, default: MoVariant = None + self: IOrganizer, plugin_name: str, key: str, default: MoVariant = None ) -> MoVariant: """ Retrieve the specified persistent value for a plugin. @@ -2101,7 +2316,7 @@ class IOrganizer: The value corresponding to the given persistent setting, or `def` is the key is not found. """ ... - def pluginDataPath(self) -> str: + def pluginDataPath(self: IOrganizer) -> str: """ Retrieve the path to a directory where plugin data should be stored. @@ -2112,13 +2327,13 @@ class IOrganizer: Path to a directory where plugin data should be stored. """ ... - def pluginList(self) -> "IPluginList": + def pluginList(self: IOrganizer) -> IPluginList: """ Returns: The plugin list interface. """ ... - def pluginSetting(self, plugin_name: str, key: str) -> MoVariant: + def pluginSetting(self: IOrganizer, plugin_name: str, key: str) -> MoVariant: """ Retrieve settings of plugins. @@ -2130,25 +2345,25 @@ class IOrganizer: The value of the setting. """ ... - def profile(self) -> "IProfile": + def profile(self: IOrganizer) -> IProfile: """ Returns: The interface to the current profile. """ ... - def profileName(self) -> str: + def profileName(self: IOrganizer) -> str: """ Returns: The name of the current profile, or an empty string if no profile has been loaded (yet). """ ... - def profilePath(self) -> str: + def profilePath(self: IOrganizer) -> str: """ Returns: The absolute path to the active profile or an empty string if no profile has been loaded (yet). """ ... - def refresh(self, save_changes: bool = True): + def refresh(self: IOrganizer, save_changes: bool = True): """ Refresh the internal mods file structure from disk. This includes the mod list, the plugin list, data tab and other smaller things like problems button (same as pressing F5). @@ -2160,7 +2375,9 @@ class IOrganizer: save_changes: If True, the relevant profile information is saved first (enabled mods and order of mods). """ ... - def resolvePath(self, filename: str) -> str: + def resolvePath( + self: IOrganizer, filename: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo] + ) -> str: """ Resolves a path relative to the virtual data directory to its absolute real path. @@ -2172,7 +2389,11 @@ class IOrganizer: """ ... def setPersistent( - self, plugin_name: str, key: str, value: MoVariant, sync: bool = True + self: IOrganizer, + plugin_name: str, + key: str, + value: MoVariant, + sync: bool = True, ): """ Set the specified persistent value for a plugin. @@ -2187,7 +2408,9 @@ class IOrganizer: sync: If True, the storage is immediately written to disc. This costs performance but is safer against data loss. """ ... - def setPluginSetting(self, plugin_name: str, key: str, value: MoVariant): + def setPluginSetting( + self: IOrganizer, plugin_name: str, key: str, value: MoVariant + ): """ Set the specified setting for a plugin. @@ -2201,10 +2424,10 @@ class IOrganizer: """ ... def startApplication( - self, - executable: str, - args: List[str] = [], - cwd: str = "", + self: IOrganizer, + executable: Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo], + args: Sequence[str] = [], + cwd: Union[str, os.PathLike[str], PyQt6.QtCore.QDir] = "", profile: str = "", forcedCustomOverwrite: str = "", ignoreCustomOverwrite: bool = False, @@ -2227,7 +2450,17 @@ class IOrganizer: The handle to the started application, or 0 if the application failed to start. """ ... - def waitForApplication(self, handle: int, refresh: bool = True) -> Tuple[bool, int]: + def virtualFileTree(self: IOrganizer) -> IFileTree: + """ + Retrieve a IFileTree object representing the virtual file tree. + + Returns: + An IFileTree representing the virtual file tree. + """ + ... + def waitForApplication( + self: IOrganizer, handle: int, refresh: bool = True + ) -> Tuple[bool, int]: """ Wait for the application corresponding to the given handle to finish. @@ -2251,23 +2484,31 @@ class IPlugin(abc.ABC): Base class for all plugins. """ - def __init__(self): ... + def __init__(self: IPlugin): ... @abc.abstractmethod - def author(self) -> str: + def author(self: IPlugin) -> str: """ Returns: The name of the plugin author. """ ... @abc.abstractmethod - def description(self) -> str: + def description(self: IPlugin) -> str: """ Returns: The description for this plugin. """ ... + def enabledByDefault(self: IPlugin) -> bool: + """ + Check whether this plugin should be enabled by default. + + Returns: + True if this plugin should be enabled by default, False otherwise. + """ + ... @abc.abstractmethod - def init(self, organizer: "IOrganizer") -> bool: + def init(self: IPlugin, organizer: IOrganizer) -> bool: """ Initialize this plugin. @@ -2289,7 +2530,7 @@ class IPlugin(abc.ABC): True if the plugin was initialized correctly, False otherwise. """ ... - def localizedName(self) -> str: + def localizedName(self: IPlugin) -> str: """ Retrieve the localized name of the plugin. @@ -2300,7 +2541,7 @@ class IPlugin(abc.ABC): The localized name of the plugin. """ ... - def master(self) -> str: + def master(self: IPlugin) -> str: """ Retrieve the master plugin of this plugin. @@ -2315,7 +2556,7 @@ class IPlugin(abc.ABC): """ ... @abc.abstractmethod - def name(self) -> str: + def name(self: IPlugin) -> str: """ Retrieve the name of the plugin. @@ -2330,7 +2571,7 @@ class IPlugin(abc.ABC): The name of the plugin. """ ... - def requirements(self) -> List["IPluginRequirement"]: + def requirements(self: IPlugin) -> List[IPluginRequirement]: """ Retrieve the requirements for this plugin. @@ -2341,14 +2582,14 @@ class IPlugin(abc.ABC): """ ... @abc.abstractmethod - def settings(self) -> List["PluginSetting"]: + def settings(self: IPlugin) -> Sequence[PluginSetting]: """ Returns: A list of settings for this plugin. """ ... @abc.abstractmethod - def version(self) -> "VersionInfo": + def version(self: IPlugin) -> VersionInfo: """ Returns: The version of this plugin. @@ -2363,14 +2604,14 @@ class IPluginDiagnose(IPlugin): interfaces) or as a stand-alone diagnosis tool. """ - def __init__(self): ... - def _invalidate(self): + def __init__(self: IPluginDiagnose): ... + def _invalidate(self: IPluginDiagnose): """ Invalidate the problems corresponding to this plugin. """ ... @abc.abstractmethod - def activeProblems(self) -> List[int]: + def activeProblems(self: IPluginDiagnose) -> List[int]: """ Retrieve the list of active problems found by this plugin. @@ -2382,7 +2623,7 @@ class IPluginDiagnose(IPlugin): """ ... @abc.abstractmethod - def fullDescription(self, key: int) -> str: + def fullDescription(self: IPluginDiagnose, key: int) -> str: """ Retrieve the full description of the problem corresponding to the given key. @@ -2397,7 +2638,7 @@ class IPluginDiagnose(IPlugin): """ ... @abc.abstractmethod - def hasGuidedFix(self, key: int) -> bool: + def hasGuidedFix(self: IPluginDiagnose, key: int) -> bool: """ Check if the problem corresponding to the given key has a guided fix. @@ -2412,7 +2653,7 @@ class IPluginDiagnose(IPlugin): """ ... @abc.abstractmethod - def shortDescription(self, key: int) -> str: + def shortDescription(self: IPluginDiagnose, key: int) -> str: """ Retrieve the short description of the problem corresponding to the given key. @@ -2427,7 +2668,7 @@ class IPluginDiagnose(IPlugin): """ ... @abc.abstractmethod - def startGuidedFix(self, key: int): + def startGuidedFix(self: IPluginDiagnose, key: int): """ Starts a guided fix for the problem corresponding to the given key. @@ -2448,9 +2689,9 @@ class IPluginFileMapper(IPlugin): Plugins that adds virtual file links. """ - def __init__(self): ... + def __init__(self: IPluginFileMapper): ... @abc.abstractmethod - def mappings(self) -> List["Mapping"]: + def mappings(self: IPluginFileMapper) -> List[Mapping]: """ Returns: Mapping for the virtual file system (VFS). @@ -2467,37 +2708,37 @@ class IPluginGame(IPlugin): plugin: https://github.com/ModOrganizer2/modorganizer-basic_games """ - def __init__(self): ... + def __init__(self: IPluginGame): ... @abc.abstractmethod - def CCPlugins(self) -> List[str]: + def CCPlugins(self: IPluginGame) -> Sequence[str]: """ Returns: The current list of active Creation Club plugins. """ ... @abc.abstractmethod - def DLCPlugins(self) -> List[str]: + def DLCPlugins(self: IPluginGame) -> Sequence[str]: """ Returns: The list of esp/esm files that are part of known DLCs. """ ... @abc.abstractmethod - def binaryName(self) -> str: + def binaryName(self: IPluginGame) -> str: """ Returns: The name of the default executable to run (relative to the game folder). """ ... @abc.abstractmethod - def dataDirectory(self) -> PyQt5.QtCore.QDir: + def dataDirectory(self: IPluginGame) -> PyQt6.QtCore.QDir: """ Returns: The path to the directory containing data (absolute path). """ ... @abc.abstractmethod - def detectGame(self): + def detectGame(self: IPluginGame): """ Detect the game. @@ -2516,27 +2757,31 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def documentsDirectory(self) -> PyQt5.QtCore.QDir: + def documentsDirectory(self: IPluginGame) -> PyQt6.QtCore.QDir: """ Returns: The directory of the documents folder where configuration files and such for this game reside. """ ... @abc.abstractmethod - def executableForcedLoads(self) -> List["ExecutableForcedLoadSetting"]: + def executableForcedLoads( + self: IPluginGame, + ) -> Sequence[ExecutableForcedLoadSetting]: """ Returns: A list of automatically discovered libraries that can be force loaded with executables. """ ... @abc.abstractmethod - def executables(self) -> List["ExecutableInfo"]: + def executables(self: IPluginGame) -> Sequence[ExecutableInfo]: """ Returns: A list of automatically discovered executables of the game itself and tools surrounding it. """ ... - def feature(self, feature_type: Type[GameFeatureType]) -> GameFeatureType: + def feature( + self: IPluginGame, feature_type: Type[GameFeatureType] + ) -> GameFeatureType: """ Retrieve a specified game feature from this plugin. @@ -2548,7 +2793,7 @@ class IPluginGame(IPlugin): not implemented. """ ... - def featureList(self) -> Dict[Type[GameFeatureType], GameFeatureType]: + def featureList(self: IPluginGame) -> Dict[Type[GameFeatureType], GameFeatureType]: """ Retrieve the list of game features implemented for this plugin. @@ -2559,42 +2804,42 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def gameDirectory(self) -> PyQt5.QtCore.QDir: + def gameDirectory(self: IPluginGame) -> PyQt6.QtCore.QDir: """ Returns: The directory containing the game installation. """ ... @abc.abstractmethod - def gameIcon(self) -> PyQt5.QtGui.QIcon: + def gameIcon(self: IPluginGame) -> PyQt6.QtGui.QIcon: """ Returns: The icon representing the game. """ ... @abc.abstractmethod - def gameName(self) -> str: + def gameName(self: IPluginGame) -> str: """ Returns: The name of the game (as displayed to the user). """ ... @abc.abstractmethod - def gameNexusName(self) -> str: + def gameNexusName(self: IPluginGame) -> str: """ Returns: The name of the game identifier for Nexus. """ ... @abc.abstractmethod - def gameShortName(self) -> str: + def gameShortName(self: IPluginGame) -> str: """ Returns: The short name of the game. """ ... @abc.abstractmethod - def gameVariants(self) -> List[str]: + def gameVariants(self: IPluginGame) -> Sequence[str]: """ Retrieve the list of variants for this game. @@ -2607,14 +2852,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def gameVersion(self) -> str: + def gameVersion(self: IPluginGame) -> str: """ Returns: The version of the game. """ ... @abc.abstractmethod - def getLauncherName(self) -> str: + def getLauncherName(self: IPluginGame) -> str: """ Returns: The name of the launcher executable to run (relative to the game folder), or an @@ -2622,7 +2867,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def iniFiles(self) -> List[str]: + def getSupportURL(self: IPluginGame) -> str: + """ + Returns: + An URL for the support page of this game. + """ + ... + @abc.abstractmethod + def iniFiles(self: IPluginGame) -> Sequence[str]: """ Returns: The list of INI files this game uses. The first file in the list should be the @@ -2630,7 +2882,9 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def initializeProfile(self, directory: PyQt5.QtCore.QDir, settings: int): + def initializeProfile( + self: IPluginGame, directory: PyQt6.QtCore.QDir, settings: ProfileSetting + ): """ Initialize a profile for this game. @@ -2646,14 +2900,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def isInstalled(self) -> bool: + def isInstalled(self: IPluginGame) -> bool: """ Returns: True if this game has been discovered as installed, False otherwise. """ ... @abc.abstractmethod - def listSaves(self, folder: PyQt5.QtCore.QDir) -> List["ISaveGame"]: + def listSaves(self: IPluginGame, folder: PyQt6.QtCore.QDir) -> List[ISaveGame]: """ List saves in the given directory. @@ -2665,14 +2919,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def loadOrderMechanism(self) -> "LoadOrderMechanism": + def loadOrderMechanism(self: IPluginGame) -> LoadOrderMechanism: """ Returns: The load order mechanism used by this game. """ ... @abc.abstractmethod - def looksValid(self, directory: PyQt5.QtCore.QDir) -> bool: + def looksValid(self: IPluginGame, directory: PyQt6.QtCore.QDir) -> bool: """ Check if the given directory looks like a valid game installation. @@ -2684,7 +2938,7 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def nexusGameID(self) -> int: + def nexusGameID(self: IPluginGame) -> int: """ Retrieve the Nexus game ID for this game. @@ -2695,7 +2949,7 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def nexusModOrganizerID(self) -> int: + def nexusModOrganizerID(self: IPluginGame) -> int: """ Retrieve the Nexus mod ID of Mod Organizer for this game. @@ -2707,14 +2961,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def primaryPlugins(self) -> List[str]: + def primaryPlugins(self: IPluginGame) -> Sequence[str]: """ Returns: The list of plugins that are part of the game and not considered optional. """ ... @abc.abstractmethod - def primarySources(self) -> List[str]: + def primarySources(self: IPluginGame) -> Sequence[str]: """ Retrieve primary alternative 'short' names for this game. @@ -2726,14 +2980,25 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def savesDirectory(self) -> PyQt5.QtCore.QDir: + def savesDirectory(self: IPluginGame) -> PyQt6.QtCore.QDir: """ Returns: The directory where save games are stored. """ ... @abc.abstractmethod - def setGamePath(self, path: str): + def secondaryDataDirectories(self: IPluginGame) -> Dict[str, PyQt6.QtCore.QDir]: + """ + Retrieve the list of secondary data directories. Each directories should be + assigned a unique name that differs from "data" which is the name of the main + data directory returned by dataDirectory(). + + Returns: + A mapping from unique name to secondary data directories. + """ + ... + @abc.abstractmethod + def setGamePath(self: IPluginGame, path: str): """ Set the path to the managed game. @@ -2746,7 +3011,7 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def setGameVariant(self, variant: str): + def setGameVariant(self: IPluginGame, variant: str): """ Set the game variant. @@ -2758,14 +3023,14 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def sortMechanism(self) -> "SortMechanism": + def sortMechanism(self: IPluginGame) -> SortMechanism: """ Returns: The sort mechanism for this game. """ ... @abc.abstractmethod - def steamAPPId(self) -> str: + def steamAPPId(self: IPluginGame) -> str: """ Retrieve the Steam app ID for this game. @@ -2779,7 +3044,7 @@ class IPluginGame(IPlugin): """ ... @abc.abstractmethod - def validShortNames(self) -> List[str]: + def validShortNames(self: IPluginGame) -> Sequence[str]: """ Retrieve the valid 'short' names for this game. @@ -2806,8 +3071,20 @@ class IPluginInstaller(IPlugin): used by the external NCC installer and the OMOD installer. """ + def _manager(self: IPluginInstaller) -> IInstallationManager: + """ + Returns: + The installation manager. + """ + ... + def _parentWidget(self: IPluginInstaller) -> PyQt6.QtWidgets.QWidget: + """ + Returns: + The parent widget. + """ + ... @abc.abstractmethod - def isArchiveSupported(self, tree: "IFileTree") -> bool: + def isArchiveSupported(self: IPluginInstaller, tree: IFileTree) -> bool: """ Check if the given file tree corresponds to a supported archive for this installer. @@ -2819,7 +3096,7 @@ class IPluginInstaller(IPlugin): """ ... @abc.abstractmethod - def isManualInstaller(self) -> bool: + def isManualInstaller(self: IPluginInstaller) -> bool: """ Check if this installer is a manual installer. @@ -2827,7 +3104,9 @@ class IPluginInstaller(IPlugin): True if this installer is a manual installer, False otherwise. """ ... - def onInstallationEnd(self, result: "InstallResult", new_mod: "IModInterface"): + def onInstallationEnd( + self: IPluginInstaller, result: InstallResult, new_mod: IModInterface + ): """ Method calls at the end of the installation process. This method is only called once per installation process, even for recursive installations (e.g. with the bundle installer). @@ -2839,7 +3118,10 @@ class IPluginInstaller(IPlugin): """ ... def onInstallationStart( - self, archive: str, reinstallation: bool, current_mod: "IModInterface" + self: IPluginInstaller, + archive: str, + reinstallation: bool, + current_mod: IModInterface, ): """ Method calls at the start of the installation process, before any other methods. @@ -2861,7 +3143,7 @@ class IPluginInstaller(IPlugin): """ ... @abc.abstractmethod - def priority(self) -> int: + def priority(self: IPluginInstaller) -> int: """ Retrieve the priority of this installer. @@ -2871,7 +3153,7 @@ class IPluginInstaller(IPlugin): The priority of this installer. """ ... - def setInstallationManager(self, manager: "IInstallationManager"): + def setInstallationManager(self: IPluginInstaller, manager: IInstallationManager): """ Set the installation manager for this installer. @@ -2882,7 +3164,7 @@ class IPluginInstaller(IPlugin): manager: The installation manager. """ ... - def setParentWidget(self, parent: PyQt5.QtWidgets.QWidget): + def setParentWidget(self: IPluginInstaller, parent: PyQt6.QtWidgets.QWidget): """ Set the parent widget for this installer. @@ -2902,28 +3184,16 @@ class IPluginInstallerCustom(IPluginInstaller): Example of such installers are the external NCC installer or the OMOD installer. """ - def __init__(self): ... - def _manager(self) -> "IInstallationManager": - """ - Returns: - The installation manager. - """ - ... - def _parentWidget(self) -> PyQt5.QtWidgets.QWidget: - """ - Returns: - The parent widget. - """ - ... + def __init__(self: IPluginInstallerCustom): ... @abc.abstractmethod def install( - self, - mod_name: "GuessedString", + self: IPluginInstallerCustom, + mod_name: GuessedString, game_name: str, archive_name: str, version: str, nexus_id: int, - ) -> "InstallResult": + ) -> InstallResult: """ Install the given archive. @@ -2943,8 +3213,22 @@ class IPluginInstallerCustom(IPluginInstaller): The result of the installation process. """ ... + @overload @abc.abstractmethod - def isArchiveSupported(self, archive_name: str) -> bool: + def isArchiveSupported(self: IPluginInstaller, tree: IFileTree) -> bool: + """ + Check if the given file tree corresponds to a supported archive for this installer. + + Args: + tree: The tree representing the content of the archive. + + Returns: + True if this installer can handle the archive, False otherwise. + """ + ... + @overload + @abc.abstractmethod + def isArchiveSupported(self: IPluginInstallerCustom, archive_name: str) -> bool: """ Check if the given file is a supported archive for this installer. @@ -2956,7 +3240,7 @@ class IPluginInstallerCustom(IPluginInstaller): """ ... @abc.abstractmethod - def supportedExtensions(self) -> List[str]: + def supportedExtensions(self: IPluginInstallerCustom) -> Set[str]: """ Returns: A list of file extensions that this installer can handle. @@ -2970,25 +3254,15 @@ class IPluginInstallerSimple(IPluginInstaller): Actually extracting the archive is handled by the manager. """ - def __init__(self): ... - def _manager(self) -> "IInstallationManager": - """ - Returns: - The installation manager. - """ - ... - def _parentWidget(self) -> PyQt5.QtWidgets.QWidget: - """ - Returns: - The parent widget. - """ - ... + def __init__(self: IPluginInstallerSimple): ... @abc.abstractmethod def install( - self, name: "GuessedString", tree: "IFileTree", version: str, nexus_id: int - ) -> Union[ - "InstallResult", "IFileTree", Tuple["InstallResult", "IFileTree", str, int] - ]: + self: IPluginInstallerSimple, + name: GuessedString, + tree: IFileTree, + version: str, + nexus_id: int, + ) -> Union[InstallResult, IFileTree, Tuple[InstallResult, IFileTree, str, int]]: """ Install a mod from an archive filetree. @@ -3019,21 +3293,62 @@ class IPluginList: Primary interface to the list of plugins. """ - def isMaster(self, name: str) -> bool: + def hasLightExtension(self: IPluginList, name: str) -> bool: """ - Check if a plugin is a master file (basically a library, referenced by other plugins). - - In gamebryo games, a master file will usually have a .esm file extension but technically - an esp can be flagged as master and an esm might not be. + Determine if a plugin has a .esl extension. Args: name: Filename of the plugin (without path but with file extension). Returns: - True if the given plugin is a master plugin, False otherwise or if the file does not exist. + True if the given file has a .esl extension, False otherwise or if the + file does not exist. """ ... - def loadOrder(self, name: str) -> int: + def hasMasterExtension(self: IPluginList, name: str) -> bool: + """ + Determine if a plugin has a .esm extension. + + Args: + name: Filename of the plugin (without path but with file extension). + + Returns: + True if the given file has a .esm extension, False otherwise or if the + file does not exist. + """ + ... + def isLightFlagged(self: IPluginList, name: str) -> bool: + """ + Determine if a plugin is flagged as light + + In gamebryo games, a master file will usually have a .esl file extension but + technically an esp can be flagged as light. + + Args: + name: Filename of the plugin (without path but with file extension). + + Returns: + True if the given plugin is a light plugin, False otherwise or if the + file does not exist. + """ + ... + def isMasterFlagged(self: IPluginList, name: str) -> bool: + """ + Determine if a plugin is flagged as mater, i.e., a library, reference by + other plugins. + + In gamebryo games, a master file will usually have a .esm file extension but + technically an esp can be flagged as master and an esm might not be. + + Args: + name: Filename of the plugin (without path but with file extension). + + Returns: + True if the given plugin is a master plugin, False otherwise or if the + file does not exist. + """ + ... + def loadOrder(self: IPluginList, name: str) -> int: """ Retrieve the load order of a plugin. @@ -3046,7 +3361,7 @@ class IPluginList: if the plugin does not exist. """ ... - def masters(self, name: str) -> List[str]: + def masters(self: IPluginList, name: str) -> Sequence[str]: """ Retrieve the list of masters required for a plugin. @@ -3057,7 +3372,9 @@ class IPluginList: The list of masters for the plugin (filenames with extension, no path). """ ... - def onPluginMoved(self, callback: Callable[[str, int, int], None]) -> bool: + def onPluginMoved( + self: IPluginList, callback: Callable[[str, int, int], None] + ) -> bool: """ Install a new handler to be called when a plugin is moved. @@ -3069,7 +3386,9 @@ class IPluginList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onPluginStateChanged(self, callback: Callable[[Dict[str, int]], None]) -> bool: + def onPluginStateChanged( + self: IPluginList, callback: Callable[[Dict[str, PluginState]], None] + ) -> bool: """ Install a new handler to be called when plugin states change. @@ -3081,7 +3400,7 @@ class IPluginList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def onRefreshed(self, callback: Callable[[], None]) -> bool: + def onRefreshed(self: IPluginList, callback: Callable[[], None]) -> bool: """ Install a new handler to be called when the list of plugins is refreshed. @@ -3092,7 +3411,7 @@ class IPluginList: True if the handler was installed properly (there are currently no reasons for this to fail). """ ... - def origin(self, name: str) -> str: + def origin(self: IPluginList, name: str) -> str: """ Retrieve the origin of a plugin. This is either the (internal) name of a mod, `"overwrite"` or `"data"`. @@ -3105,13 +3424,13 @@ class IPluginList: The name of the origin of the plugin, or an empty string if the plugin does not exist. """ ... - def pluginNames(self) -> List[str]: + def pluginNames(self: IPluginList) -> Sequence[str]: """ Returns: The list of all plugin names. """ ... - def priority(self, name: str) -> int: + def priority(self: IPluginList, name: str) -> int: """ Retrieve the priority of a plugin. @@ -3124,7 +3443,7 @@ class IPluginList: The priority of the given plugin, or -1 if the plugin does not exist. """ ... - def setLoadOrder(self, loadorder: List[str]): + def setLoadOrder(self: IPluginList, loadorder: Sequence[str]): """ Set the load order. @@ -3135,7 +3454,7 @@ class IPluginList: loadorder: The new load order, specified by the list of plugin names, sorted. """ ... - def setPriority(self, name: str, priority: int) -> bool: + def setPriority(self: IPluginList, name: str, priority: int) -> bool: """ Change the priority of a plugin. @@ -3149,7 +3468,7 @@ class IPluginList: at the specified priority (e.g. when trying to move a non-master plugin before a master one). """ ... - def setState(self, name: str, state: int): + def setState(self: IPluginList, name: str, state: PluginState): """ Set the state of a plugin. @@ -3158,7 +3477,7 @@ class IPluginList: state: New state of the plugin (`INACTIVE` or `ACTIVE`). """ ... - def state(self, name: str) -> int: + def state(self: IPluginList, name: str) -> PluginState: """ Retrieve the state of a plugin. @@ -3171,15 +3490,15 @@ class IPluginList: ... class IPluginModPage(IPlugin): - def __init__(self): ... - def _parentWidget(self) -> PyQt5.QtWidgets.QWidget: + def __init__(self: IPluginModPage): ... + def _parentWidget(self: IPluginModPage) -> PyQt6.QtWidgets.QWidget: """ Returns: The parent widget. """ ... @abc.abstractmethod - def displayName(self) -> str: + def displayName(self: IPluginModPage) -> str: """ Returns: The name of the page as displayed in the UI. @@ -3187,10 +3506,10 @@ class IPluginModPage(IPlugin): ... @abc.abstractmethod def handlesDownload( - self, - page_url: PyQt5.QtCore.QUrl, - download_url: PyQt5.QtCore.QUrl, - fileinfo: "ModRepositoryFileInfo", + self: IPluginModPage, + page_url: PyQt6.QtCore.QUrl, + download_url: PyQt6.QtCore.QUrl, + fileinfo: ModRepositoryFileInfo, ) -> bool: """ Check if the plugin handles the specified download. @@ -3205,20 +3524,20 @@ class IPluginModPage(IPlugin): """ ... @abc.abstractmethod - def icon(self) -> PyQt5.QtGui.QIcon: + def icon(self: IPluginModPage) -> PyQt6.QtGui.QIcon: """ Returns: The icon to display with the page. """ ... @abc.abstractmethod - def pageURL(self) -> PyQt5.QtCore.QUrl: + def pageURL(self: IPluginModPage) -> PyQt6.QtCore.QUrl: """ Returns: The URL to open when the user wants to visit this mod page. """ ... - def setParentWidget(self, parent: PyQt5.QtWidgets.QWidget): + def setParentWidget(self: IPluginModPage, parent: PyQt6.QtWidgets.QWidget): """ Set the parent widget for this mod page. @@ -3230,7 +3549,7 @@ class IPluginModPage(IPlugin): """ ... @abc.abstractmethod - def useIntegratedBrowser(self) -> bool: + def useIntegratedBrowser(self: IPluginModPage) -> bool: """ Indicates if the page should be displayed in the integrated browser. @@ -3248,11 +3567,11 @@ class IPluginPreview(IPlugin): by qt are implemented (including dds) but no audio files and no 3d mesh formats. """ - def __init__(self): ... + def __init__(self: IPluginPreview): ... @abc.abstractmethod def genFilePreview( - self, filename: str, max_size: PyQt5.QtCore.QSize - ) -> PyQt5.QtWidgets.QWidget: + self: IPluginPreview, filename: str, max_size: PyQt6.QtCore.QSize + ) -> PyQt6.QtWidgets.QWidget: """ Generate a preview for the specified file. @@ -3265,7 +3584,7 @@ class IPluginPreview(IPlugin): """ ... @abc.abstractmethod - def supportedExtensions(self) -> List[str]: + def supportedExtensions(self: IPluginPreview) -> Set[str]: """ Returns: The list of file extensions that are supported by this preview plugin. @@ -3282,27 +3601,32 @@ class IPluginRequirement: Class representing a problem found by a requirement. """ - def __init__(self, short_description: str, long_description: str = ""): + def __init__( + self: IPluginRequirement.Problem, + short_description: str, + long_description: str = "", + ): """ Args: short_description: Short description of the problem. long_description: Long description of the problem. """ ... - def longDescription(self) -> str: + def longDescription(self: IPluginRequirement.Problem) -> str: """ Returns: A long description of the problem. """ ... - def shortDescription(self) -> str: + def shortDescription(self: IPluginRequirement.Problem) -> str: """ Returns: A short description of the problem. """ ... - def __init__(self): ... - def check(self, organizer: "IOrganizer") -> Optional["IPluginRequirement.Problem"]: + def check( + self: IPluginRequirement, organizer: IOrganizer + ) -> Optional[IPluginRequirement.Problem]: """ Check if the requirement is met, and return a problem if not. @@ -3324,34 +3648,34 @@ class IPluginTool(IPlugin): application itself. """ - def __init__(self): ... - def _parentWidget(self) -> PyQt5.QtWidgets.QWidget: + def __init__(self: IPluginTool): ... + def _parentWidget(self: IPluginTool) -> PyQt6.QtWidgets.QWidget: """ Returns: The parent widget. """ ... @abc.abstractmethod - def display(self): + def display(self: IPluginTool): """ Called when the user starts the tool. """ ... @abc.abstractmethod - def displayName(self) -> str: + def displayName(self: IPluginTool) -> str: """ Returns: The display name for this tool, as shown in the tool menu. """ ... @abc.abstractmethod - def icon(self) -> PyQt5.QtGui.QIcon: + def icon(self: IPluginTool) -> PyQt6.QtGui.QIcon: """ Returns: The icon for this tool, or a default-constructed QICon(). """ ... - def setParentWidget(self, parent: PyQt5.QtWidgets.QWidget): + def setParentWidget(self: IPluginTool, parent: PyQt6.QtWidgets.QWidget): """ Set the parent widget for this tool. @@ -3363,7 +3687,7 @@ class IPluginTool(IPlugin): """ ... @abc.abstractmethod - def tooltip(self) -> str: + def tooltip(self: IPluginTool) -> str: """ Returns: The tooltip for this tool. @@ -3375,7 +3699,7 @@ class IProfile: Interface to interact with Mod Organizer 2 profiles. """ - def absoluteIniFilePath(self, inifile: str) -> str: + def absoluteIniFilePath(self: IProfile, inifile: str) -> str: """ Retrieve the absolute file path to the corresponding INI file for this profile. @@ -3392,31 +3716,31 @@ class IProfile: The absolute path for the given INI file for this profile. """ ... - def absolutePath(self) -> str: + def absolutePath(self: IProfile) -> str: """ Returns: The absolute path to the profile folder. """ ... - def invalidationActive(self) -> Tuple[bool, bool]: + def invalidationActive(self: IProfile) -> tuple: """ Returns: True if automatic archive invalidation is enabled for this profile, False otherwise. """ ... - def localSavesEnabled(self) -> bool: + def localSavesEnabled(self: IProfile) -> bool: """ Returns: True if profile-specific saves are enabled for this profile, False otherwise. """ ... - def localSettingsEnabled(self) -> bool: + def localSettingsEnabled(self: IProfile) -> bool: """ Returns: True if profile-specific game settings are enabled for this profile, False otherwise. """ ... - def name(self) -> str: + def name(self: IProfile) -> str: """ Returns: The name of this profile. @@ -3428,14 +3752,16 @@ class ISaveGame: Base class for information about what is in a save game. """ - def __init__(self): ... - def allFiles(self) -> List[str]: + def __init__(self: ISaveGame): ... + def allFiles( + self: ISaveGame, + ) -> Sequence[Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]]: """ Returns: The list of all files related to this save. """ ... - def getCreationTime(self) -> PyQt5.QtCore.QDateTime: + def getCreationTime(self: ISaveGame) -> PyQt6.QtCore.QDateTime: """ Retrieve the creation time of the save. @@ -3446,19 +3772,21 @@ class ISaveGame: The creation time of the save. """ ... - def getFilepath(self) -> str: + def getFilepath( + self: ISaveGame, + ) -> Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]: """ Returns: The path name to the (main) file or folder for the save. """ ... - def getName(self) -> str: + def getName(self: ISaveGame) -> str: """ Returns: The name of this save, for display purpose. """ ... - def getSaveGroupIdentifier(self) -> str: + def getSaveGroupIdentifier(self: ISaveGame) -> str: """ Retrieve the name of the group this files belong to. @@ -3470,25 +3798,26 @@ class ISaveGame: """ ... -class ISaveGameInfoWidget(PyQt5.QtWidgets.QWidget): +class ISaveGameInfoWidget(PyQt6.QtWidgets.QWidget): """ Base class for a save game info widget. """ - def __init__(self, parent: PyQt5.QtWidgets.QWidget = None): + def __init__(self: ISaveGameInfoWidget, parent: PyQt6.QtWidgets.QWidget = None): """ Args: parent: Parent widget. """ ... - def _widget(self) -> PyQt5.QtWidgets.QWidget: + def __getattr__(self: ISaveGameInfoWidget, arg0: str) -> object: ... + def _widget(self: ISaveGameInfoWidget) -> PyQt6.QtWidgets.QWidget: """ Returns: The underlying `QWidget`. """ ... @abc.abstractmethod - def setSave(self, save: "ISaveGame"): + def setSave(self: ISaveGameInfoWidget, save: ISaveGame): """ Set the save file to display in this widget. @@ -3498,11 +3827,13 @@ class ISaveGameInfoWidget(PyQt5.QtWidgets.QWidget): ... class LocalSavegames(abc.ABC): - def __init__(self): ... + def __init__(self: LocalSavegames): ... @abc.abstractmethod - def mappings(self, profile_save_dir: PyQt5.QtCore.QDir) -> List["Mapping"]: ... + def mappings( + self: LocalSavegames, profile_save_dir: PyQt6.QtCore.QDir + ) -> List[Mapping]: ... @abc.abstractmethod - def prepareProfile(self, profile: "IProfile") -> bool: ... + def prepareProfile(self: LocalSavegames, profile: IProfile) -> bool: ... class Mapping: @property @@ -3522,19 +3853,19 @@ class Mapping: @source.setter def source(self, arg0: str): ... @overload - def __init__(self): + def __init__(self: Mapping): """ Creates an empty Mapping. """ ... @overload def __init__( - self, + self: Mapping, source: str, destination: str, is_directory: bool, create_target: bool = False, - ) -> object: + ): """ Creates a Mapping with the given parameters. @@ -3545,7 +3876,7 @@ class Mapping: create_target: True if file creation (including move or copy) should be redirected to source. """ ... - def __str__(self) -> str: ... + def __str__(self: Mapping) -> str: ... class ModDataChecker(abc.ABC): """ @@ -3556,16 +3887,24 @@ class ModDataChecker(abc.ABC): INVALID = ... FIXABLE = ... VALID = ... - def __and__(self, other: int) -> bool: ... - def __or__(self, other: int) -> bool: ... - def __rand__(self, other: int) -> bool: ... - def __ro__(self, other: int) -> bool: ... - FIXABLE: "ModDataChecker.CheckReturn" = ... - INVALID: "ModDataChecker.CheckReturn" = ... - VALID: "ModDataChecker.CheckReturn" = ... - def __init__(self): ... + + @property + def value(self) -> int: ... + @property + def name(self) -> str: ... + def __eq__(self: ModDataChecker.CheckReturn, other: object) -> bool: ... + def __int__(self: ModDataChecker.CheckReturn) -> int: ... + def __ne__(self: ModDataChecker.CheckReturn, other: object) -> bool: ... + + FIXABLE: CheckReturn = ... + INVALID: CheckReturn = ... + VALID: CheckReturn = ... + + def __init__(self: ModDataChecker): ... @abc.abstractmethod - def dataLooksValid(self, filetree: "IFileTree") -> "ModDataChecker.CheckReturn": + def dataLooksValid( + self: ModDataChecker, filetree: IFileTree + ) -> ModDataChecker.CheckReturn: """ Check that the given filetree represent a valid mod layout, or can be easily fixed. @@ -3588,7 +3927,7 @@ class ModDataChecker(abc.ABC): Whether the tree is invalid, fixable or valid. """ ... - def fix(self, filetree: "IFileTree") -> Optional["IFileTree"]: + def fix(self: ModDataChecker, filetree: IFileTree) -> Optional["IFileTree"]: """ Try to fix the given tree. @@ -3657,7 +3996,13 @@ class ModDataContent(abc.ABC): def id(self) -> int: ... @property def name(self) -> str: ... - def __init__(self, id: int, name: str, icon: str, filter_only: bool = False): + def __init__( + self: ModDataContent.Content, + id: int, + name: str, + icon: str, + filter_only: bool = False, + ): """ Args: id: ID of this content. @@ -3669,22 +4014,22 @@ class ModDataContent(abc.ABC): criteria and not in the actual Content column. """ ... - def isOnlyForFilter(self) -> bool: + def isOnlyForFilter(self: ModDataContent.Content) -> bool: """ Returns: True if this content is only meant to be used as a filter criteria. """ ... - def __init__(self): ... + def __init__(self: ModDataContent): ... @abc.abstractmethod - def getAllContents(self) -> List["ModDataContent.Content"]: + def getAllContents(self: ModDataContent) -> List[ModDataContent.Content]: """ Returns: The list of all possible contents for the corresponding game. """ ... @abc.abstractmethod - def getContentsFor(self, filetree: "IFileTree") -> List[int]: + def getContentsFor(self: ModDataContent, filetree: IFileTree) -> List[int]: """ Retrieve the list of contents in the given tree. @@ -3722,9 +4067,9 @@ class ModRepositoryFileInfo: @fileSize.setter def fileSize(self, arg0: int): ... @property - def fileTime(self) -> PyQt5.QtCore.QDateTime: ... + def fileTime(self) -> PyQt6.QtCore.QDateTime: ... @fileTime.setter - def fileTime(self, arg0: PyQt5.QtCore.QDateTime): ... + def fileTime(self, arg0: PyQt6.QtCore.QDateTime): ... @property def gameName(self) -> str: ... @gameName.setter @@ -3742,9 +4087,9 @@ class ModRepositoryFileInfo: @name.setter def name(self, arg0: str): ... @property - def newestVersion(self) -> "VersionInfo": ... + def newestVersion(self) -> VersionInfo: ... @newestVersion.setter - def newestVersion(self, arg0: "VersionInfo"): ... + def newestVersion(self, arg0: VersionInfo): ... @property def repository(self) -> str: ... @repository.setter @@ -3758,25 +4103,27 @@ class ModRepositoryFileInfo: @userData.setter def userData(self, arg0: MoVariant): ... @property - def version(self) -> "VersionInfo": ... + def version(self) -> VersionInfo: ... @version.setter - def version(self, arg0: "VersionInfo"): ... + def version(self, arg0: VersionInfo): ... @overload - def __init__(self, other: "ModRepositoryFileInfo"): ... + def __init__(self: ModRepositoryFileInfo, other: ModRepositoryFileInfo): ... @overload def __init__( - self, game_name: str = None, mod_id: int = None, file_id: int = None + self: ModRepositoryFileInfo, + game_name: str = "", + mod_id: int = 0, + file_id: int = 0, ): ... - def __str__(self) -> str: ... + def __str__(self: ModRepositoryFileInfo) -> str: ... @staticmethod - def createFromJson(data: str) -> "ModRepositoryFileInfo": ... + def createFromJson(data: str) -> ModRepositoryFileInfo: ... class PluginRequirementFactory: - def __init__(self): ... @staticmethod def basic( - checker: Callable[["IOrganizer"], bool], description: str - ) -> "IPluginRequirement": + checker: Callable[[IOrganizer], bool], description: str + ) -> IPluginRequirement: """ Create a basic requirement. @@ -3790,7 +4137,7 @@ class PluginRequirementFactory: """ ... @staticmethod - def diagnose(diagnose: "IPluginDiagnose") -> "IPluginRequirement": + def diagnose(diagnose: IPluginDiagnose) -> IPluginRequirement: """ Construct a requirement from a diagnose plugin. @@ -3807,7 +4154,7 @@ class PluginRequirementFactory: ... @overload @staticmethod - def gameDependency(games: List[str]) -> "IPluginRequirement": + def gameDependency(games: Sequence[str]) -> IPluginRequirement: """ Create a new game dependency requirement. @@ -3822,7 +4169,7 @@ class PluginRequirementFactory: ... @overload @staticmethod - def gameDependency(game: str) -> "IPluginRequirement": + def gameDependency(game: str) -> IPluginRequirement: """ Create a new game dependency requirement. @@ -3837,7 +4184,7 @@ class PluginRequirementFactory: ... @overload @staticmethod - def pluginDependency(plugins: List[str]) -> "IPluginRequirement": + def pluginDependency(plugins: Sequence[str]) -> IPluginRequirement: """ Create a new plugin dependency requirement. @@ -3852,7 +4199,7 @@ class PluginRequirementFactory: ... @overload @staticmethod - def pluginDependency(plugin: str) -> "IPluginRequirement": + def pluginDependency(plugin: str) -> IPluginRequirement: """ Create a new plugin dependency requirement. @@ -3884,7 +4231,9 @@ class PluginSetting: def key(self) -> str: ... @key.setter def key(self, arg0: str): ... - def __init__(self, key: str, description: str, default_value: MoVariant): + def __init__( + self: PluginSetting, key: str, description: str, default_value: MoVariant + ): """ Args: key: Name of the setting. @@ -3898,9 +4247,11 @@ class SaveGameInfo(abc.ABC): Feature to get hold of stuff to do with save games. """ - def __init__(self): ... + def __init__(self: SaveGameInfo): ... @abc.abstractmethod - def getMissingAssets(self, save: "ISaveGame") -> Dict[str, List[str]]: + def getMissingAssets( + self: SaveGameInfo, save: ISaveGame + ) -> Dict[str, Sequence[str]]: """ Retrieve missing assets from the save. @@ -3913,8 +4264,8 @@ class SaveGameInfo(abc.ABC): ... @abc.abstractmethod def getSaveGameWidget( - self, parent: PyQt5.QtWidgets.QWidget - ) -> Optional["ISaveGameInfoWidget"]: + self: SaveGameInfo, parent: PyQt6.QtWidgets.QWidget + ) -> Optional[ISaveGameInfoWidget]: """ Retrieve a widget to display over the save game list. @@ -3929,58 +4280,62 @@ class SaveGameInfo(abc.ABC): ... class ScriptExtender(abc.ABC): - def __init__(self): ... + def __init__(self: ScriptExtender): ... @abc.abstractmethod - def BinaryName(self) -> str: + def binaryName(self: ScriptExtender) -> str: """ Returns: The name of the script extender binary. """ ... @abc.abstractmethod - def PluginPath(self) -> str: - """ - Returns: - The script extender plugin path, relative to the data folder. - """ - ... - @abc.abstractmethod - def getArch(self) -> int: + def getArch(self: ScriptExtender) -> int: """ Returns: The CPU platform of the extender. """ ... @abc.abstractmethod - def getExtenderVersion(self) -> str: + def getExtenderVersion(self: ScriptExtender) -> str: """ Returns: The version of the script extender. """ ... @abc.abstractmethod - def isInstalled(self) -> bool: + def isInstalled(self: ScriptExtender) -> bool: """ Returns: True if the script extender is installed, False otherwise. """ ... @abc.abstractmethod - def loaderName(self) -> str: + def loaderName(self: ScriptExtender) -> str: """ Returns: The loader to use to ensure the game runs with the script extender. """ ... @abc.abstractmethod - def loaderPath(self) -> str: + def loaderPath( + self: ScriptExtender, + ) -> Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]: """ Returns: The full path to the script extender loader. """ ... @abc.abstractmethod - def savegameExtension(self) -> str: + def pluginPath( + self: ScriptExtender, + ) -> Union[str, os.PathLike[str], PyQt6.QtCore.QDir]: + """ + Returns: + The script extender plugin path, relative to the data folder. + """ + ... + @abc.abstractmethod + def savegameExtension(self: ScriptExtender) -> str: """ Retrieve the extension of script extender save files. @@ -3990,9 +4345,9 @@ class ScriptExtender(abc.ABC): ... class UnmanagedMods(abc.ABC): - def __init__(self): ... + def __init__(self: UnmanagedMods): ... @abc.abstractmethod - def displayName(self, mod_name: str) -> str: + def displayName(self: UnmanagedMods, mod_name: str) -> str: """ Retrieve the display name of a given mod. @@ -4004,7 +4359,7 @@ class UnmanagedMods(abc.ABC): """ ... @abc.abstractmethod - def mods(self, official_only: bool) -> List[str]: + def mods(self: UnmanagedMods, official_only: bool) -> Sequence[str]: """ Retrieve the list of unmanaged mods for the corresponding game. @@ -4016,7 +4371,9 @@ class UnmanagedMods(abc.ABC): """ ... @abc.abstractmethod - def referenceFile(self, mod_name: str) -> PyQt5.QtCore.QFileInfo: + def referenceFile( + self: UnmanagedMods, mod_name: str + ) -> Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]: """ Retrieve the reference file for the requested mod. @@ -4031,7 +4388,9 @@ class UnmanagedMods(abc.ABC): """ ... @abc.abstractmethod - def secondaryFiles(self, mod_name: str) -> List[str]: + def secondaryFiles( + self: UnmanagedMods, mod_name: str + ) -> Sequence[Union[str, os.PathLike[str], PyQt6.QtCore.QFileInfo]]: """ Retrieve the secondary files for the requested mod. @@ -4052,13 +4411,15 @@ class VersionInfo: """ @overload - def __init__(self): + def __init__(self: VersionInfo): """ Construct an invalid VersionInfo. """ ... @overload - def __init__(self, value: str, scheme: "VersionScheme" = VersionScheme.DISCOVER): + def __init__( + self: VersionInfo, value: str, scheme: VersionScheme = VersionScheme.DISCOVER + ): """ Construct a VersionInfo by parsing the given string according to the given scheme. @@ -4069,12 +4430,12 @@ class VersionInfo: ... @overload def __init__( - self, + self: VersionInfo, major: int, minor: int, subminor: int, subsubminor: int, - release_type: "ReleaseType" = ReleaseType.FINAL, + release_type: ReleaseType = ReleaseType.FINAL, ): """ Construct a VersionInfo using the given elements. @@ -4089,11 +4450,11 @@ class VersionInfo: ... @overload def __init__( - self, + self: VersionInfo, major: int, minor: int, subminor: int, - release_type: "ReleaseType" = ReleaseType.FINAL, + release_type: ReleaseType = ReleaseType.FINAL, ): """ Construct a VersionInfo using the given elements. @@ -4105,36 +4466,30 @@ class VersionInfo: release_type: Type of release. """ ... - @overload - def __eq__(self, arg2: "VersionInfo") -> bool: ... - @overload - def __eq__(self, other: object) -> bool: ... - def __ge__(self, arg2: "VersionInfo") -> bool: ... - def __gt__(self, arg2: "VersionInfo") -> bool: ... - def __le__(self, arg2: "VersionInfo") -> bool: ... - def __lt__(self, arg2: "VersionInfo") -> bool: ... - @overload - def __ne__(self, arg2: "VersionInfo") -> bool: ... - @overload - def __ne__(self, other: object) -> bool: ... - def __str__(self) -> str: + def __eq__(self: VersionInfo, other: object) -> bool: ... + def __ge__(self: VersionInfo, arg0: VersionInfo) -> bool: ... + def __gt__(self: VersionInfo, arg0: VersionInfo) -> bool: ... + def __le__(self: VersionInfo, arg0: VersionInfo) -> bool: ... + def __lt__(self: VersionInfo, arg0: VersionInfo) -> bool: ... + def __ne__(self: VersionInfo, other: object) -> bool: ... + def __str__(self: VersionInfo) -> str: """ Returns: See `canonicalString()`. """ ... - def canonicalString(self) -> str: + def canonicalString(self: VersionInfo) -> str: """ Returns: A canonical string representing this version, that can be stored and then parsed using the parse() method. """ ... - def clear(self): + def clear(self: VersionInfo): """ Resets this VersionInfo to an invalid version. """ ... - def displayString(self, forced_segments: int = 2) -> str: + def displayString(self: VersionInfo, forced_segments: int = 2) -> str: """ Args: forced_segments: The number of version segments to display even if the version is 0. 1 is major, 2 is major @@ -4145,16 +4500,16 @@ class VersionInfo: A string for display to the user. The returned string may not contain enough information to reconstruct this version info. """ ... - def isValid(self) -> bool: + def isValid(self: VersionInfo) -> bool: """ Returns: True if this VersionInfo is valid, False otherwise. """ ... def parse( - self, + self: VersionInfo, value: str, - scheme: "VersionScheme" = VersionScheme.DISCOVER, + scheme: VersionScheme = VersionScheme.DISCOVER, is_manual: bool = False, ): """ @@ -4166,7 +4521,7 @@ class VersionInfo: is_manual: True if the given string should be treated as user input. """ ... - def scheme(self) -> "VersionScheme": + def scheme(self: VersionInfo) -> VersionScheme: """ Returns: The version scheme in effect for this VersionInfo. diff --git a/stubs/2.5.0/mobase-stubs/widgets/__init__.pyi b/stubs/2.5.0/mobase-stubs/widgets/__init__.pyi new file mode 100644 index 0000000..3a1be96 --- /dev/null +++ b/stubs/2.5.0/mobase-stubs/widgets/__init__.pyi @@ -0,0 +1,171 @@ +from __future__ import annotations + +__version__ = "2.5.0" + +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, + 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]] = "", + ): + """ + 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): + """ + 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): + """ + 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): ... + @property + def description(self) -> str: ... + @description.setter + def description(self, arg0: str): ... + @property + def text(self) -> str: ... + @text.setter + def text(self, arg0: str): ... + @overload + def __init__( + self: TaskDialogButton, + text: str, + description: str, + button: PyQt6.QtWidgets.QMessageBox.StandardButton, + ): + """ + 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, + ): + """ + Create a TaskDialogButton without description. + + Args: + text: Label of the button. + button: Value returned by TaskDialog.exec() if this button is clicked. + """ + ... diff --git a/stubs/setup/setup.py b/stubs/setup/setup.py index c088063..a7ea951 100644 --- a/stubs/setup/setup.py +++ b/stubs/setup/setup.py @@ -11,6 +11,9 @@ import io import os import re +from collections import defaultdict +from pathlib import Path + from setuptools import setup @@ -33,24 +36,33 @@ def find_version(*file_paths): raise RuntimeError("Unable to find version string.") +def find_package_data(path: str): + package_data: dict[str, list[str]] = defaultdict(lambda: []) + for stubfile in Path(path).glob("**/*.pyi"): + package_data[stubfile.parent.as_posix().replace("/", ".")].append(stubfile.name) + return dict(package_data) + + long_description = read("README.md") +package_data = find_package_data("mobase-stubs") setup( name="mobase-stubs", url="https://github.com/ModOrganizer2/mo2-pystubs-generation", author="Holt59", + author_email="capelle.mikael@gmail.com", description="PEP561 stub files for the mobase python API", long_description=long_description, long_description_content_type="text/markdown", version=find_version("mobase-stubs", "__init__.pyi"), - package_data={"mobase-stubs": ["*.pyi"]}, - packages=["mobase-stubs"], - install_requires=["PyQt5-stubs==5.15.2"], - python_requires="==3.8.*", + packages=list(package_data.keys()), + package_data=package_data, + install_requires=[], + python_requires="==3.11.*", classifiers=[ "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Topic :: Software Development", ],