Additional fixes and cleanup for python updates

This commit is contained in:
Jeremy Rimpo
2023-09-23 14:24:22 +02:00
committed by Mikaël Capelle
parent 0f106363cd
commit 55a987bff6
62 changed files with 1205 additions and 737 deletions
+16 -18
View File
@@ -5,22 +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@v1
with:
path: basic_games
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
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 . --check --diff
poetry run isort -c .
poetry run mypy .
poetry run ruff .
poetry run pyright .
+1 -2
View File
@@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*-
# pyright: reportUnboundVariable=false
import glob
import importlib
@@ -17,7 +17,6 @@ BasicGame.setup()
def createPlugins():
# List of game class from python:
game_plugins: typing.List[BasicGame] = []
+3 -3
View File
@@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*-
from .basic_mod_data_checker import BasicModDataChecker
from .basic_save_game_info import BasicGameSaveGameInfo
from .basic_mod_data_checker import BasicModDataChecker # noqa
from .basic_save_game_info import BasicGameSaveGameInfo # noqa
__all__ = ["BasicModDataChecker", "BasicGameSaveGameInfo"]
+108 -151
View File
@@ -1,89 +1,75 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import fnmatch
import re
import sys
from collections.abc import Iterable, Sequence
from typing import ClassVar, MutableMapping, Optional, TypedDict
from dataclasses import dataclass, field
from typing import Iterable
import mobase
def convert_entry_to_tree(entry: mobase.FileTreeEntry) -> Optional[mobase.IFileTree]:
if not entry.isDir():
return None
if isinstance(entry, mobase.IFileTree):
return entry
if (parent := entry.parent()) is None:
return None
converted_entry = parent.find(
entry.name(), mobase.FileTreeEntry.FileTypes.DIRECTORY
)
if isinstance(converted_entry, mobase.IFileTree):
return converted_entry
return None
from .utils import is_directory
class RegexPatternDict(dict, MutableMapping[str, re.Pattern]):
"""Regex patterns for validation in `BasicModDataChecker`."""
class OptionalRegexPattern:
_pattern: re.Pattern[str] | None
@classmethod
def from_glob_patterns(cls, glob_patterns: GlobPatternDict) -> RegexPatternDict:
"""Returns an instance of `RegexPatternDict`, with the `glob_patterns`
translated to regex.
"""
return cls(
(
key,
cls.regex_from_glob_list(value)
if isinstance(value, Iterable)
else None,
)
for key, value in glob_patterns.items()
)
def get_match_index(self, key: str, search_str: str) -> Optional[int]:
"""Get the index of the matched group if the `self[key]` matches `search_str`.
Returns:
The 0-based index of the matched group or None for no match.
"""
if pattern := self.get(key):
return self.match_index_of_pattern(search_str, pattern)
return None
def __init__(self, globs: Iterable[str] | None) -> None:
if globs is None:
self._pattern = None
else:
self._pattern = OptionalRegexPattern.regex_from_glob_list(globs)
@staticmethod
def match_index_of_pattern(search_str: str, pattern: re.Pattern) -> Optional[int]:
"""Get the index of the matched group if `search_str` matches `pattern`
(from this dict).
Returns:
The 0-based index of the matched group or None for no match.
def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern[str]:
"""
if (match := pattern.match(search_str)) and match.lastindex:
return match.lastindex - 1
return None
@staticmethod
def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern:
"""Returns a regex pattern form a list of glob patterns.
Returns a regex pattern form a list of glob patterns.
Every pattern has a capturing group, so that `match.lastindex - 1` will
give the `glob_list` index.
"""
return re.compile(
f'(?:{"|".join(f"({fnmatch.translate(f)})" for f in glob_list)})', re.I
"|".join(f"({fnmatch.translate(f)})" for f in glob_list), re.I
)
def match(self, value: str) -> bool:
if self._pattern is None:
return False
return bool(self._pattern.match(value))
class GlobPatternDict(TypedDict, total=False):
"""See: `BasicModDataChecker`"""
unfold: Iterable[str] | None
valid: Iterable[str] | None
delete: Iterable[str] | None
move: MutableMapping[str, str] | None
class RegexPatterns:
"""
Regex patterns for validation in `BasicModDataChecker`.
"""
def __init__(self, globs: GlobPatterns) -> None:
self.unfold = OptionalRegexPattern(globs.unfold)
self.delete = OptionalRegexPattern(globs.delete)
self.valid = OptionalRegexPattern(globs.valid)
self.move = {key: re.compile(fnmatch.translate(key)) for key in globs.move}
def move_match(self, value: str) -> str | None:
"""
Retrieve the first move patterns that matches the given value, or None if no
move matches.
"""
for key, pattern in self.move.items():
if pattern.match(value):
return key
return None
@dataclass(frozen=True, unsafe_hash=True)
class GlobPatterns:
"""
See: `BasicModDataChecker`
"""
unfold: list[str] | None = None
valid: list[str] | None = None
delete: list[str] | None = None
move: dict[str, str] = field(default_factory=dict)
class BasicModDataChecker(mobase.ModDataChecker):
@@ -94,121 +80,92 @@ class BasicModDataChecker(mobase.ModDataChecker):
checked and fixed in definition order of the `file_patterns` dict.
Args:
file_patterns (optional): A dict (GlobPatternDict) with the following keys::
file_patterns (optional): A GlobPatterns object, with the following attributes:
unfold: [ "list of folders to unfold" ],
# (remove and move contents to parent), after being checked and
# fixed recursively.
# Check result: `mobase.ModDataChecker.VALID`.
{
"unfold": [ "list of folders to unfold" ],
# (remove and move contents to parent), after being checked and
# fixed recursively.
# Check result: `mobase.ModDataChecker.VALID`.
valid: [ "list of files and folders in the right path." ],
# Check result: `mobase.ModDataChecker.VALID`.
"valid": [ "list of files and folders in the right path." ],
# Check result: `mobase.ModDataChecker.VALID`.
delete: [ "list of files/folders to delete." ],
# Check result: `mobase.ModDataChecker.FIXABLE`.
"delete": [ "list of files/folders to delete." ],
# Check result: `mobase.ModDataChecker.FIXABLE`.
move: {"Files/folders to move": "target path"}
# If the path ends with `/` or `\\`, the entry will be inserted
# in the corresponding directory instead of replacing it.
# Check result: `mobase.ModDataChecker.FIXABLE`.
"move": {"Files/folders to move": "target path"}
# If the path ends with `/` or `\\`, the entry will be inserted
# in the corresponding directory instead of replacing it.
# Check result: `mobase.ModDataChecker.FIXABLE`.
}
Example:
Example::
BasicModDataChecker(
{
"valid": ["valid_folder", "*.ext1"]
"move": {"*.ext2": "path/to/target_folder/"}
}
BasicModDataChecker(
GlobPatterns(
valid=["valid_folder", "*.ext1"]
move={"*.ext2": "path/to/target_folder/"}
)
)
See Also:
`mobase.IFileTree.move` for the `"move"` target path specs.
"""
default_file_patterns: ClassVar[GlobPatternDict] = {}
"""Default for `file_patterns` - for subclasses."""
_file_patterns: GlobPatternDict
_file_patterns: GlobPatterns
"""Private `file_patterns`, updated together with `._regex` and `._move_targets`."""
_regex: RegexPatternDict
_regex_patterns: RegexPatterns
"""The regex patterns derived from the file (glob) patterns."""
_move_targets: Sequence[str]
"""Target paths from `file_patterns["move"]`."""
def __init__(self, file_patterns: Optional[GlobPatternDict] = None):
def __init__(self, file_patterns: GlobPatterns = GlobPatterns()):
super().__init__()
# Init with copy from class var by default (for unique instance var).
self.set_patterns(file_patterns or (self.default_file_patterns.copy()))
def set_patterns(self, file_patterns: GlobPatternDict):
"""Sets the file patterns, replacing previous/default values and order."""
self._file_patterns = file_patterns
self.update_patterns()
def update_patterns(self, file_patterns: Optional[GlobPatternDict] = None):
"""Update file patterns. Preserves previous/default definition
(check/fix) order.
"""
if file_patterns:
self._file_patterns.update(file_patterns)
self._regex = RegexPatternDict.from_glob_patterns(self._file_patterns)
if move_map := self._file_patterns.get("move"):
self._move_targets = list(move_map.values())
self._regex_patterns = RegexPatterns(file_patterns)
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
status = mobase.ModDataChecker.INVALID
rp = self._regex_patterns
for entry in filetree:
name = entry.name().casefold()
for key, regex in self._regex.items():
if not regex or not regex.match(name):
continue
if key == "unfold":
return mobase.ModDataChecker.FIXABLE
elif key == "valid":
if status is not mobase.ModDataChecker.FIXABLE:
status = mobase.ModDataChecker.VALID
elif key in ("move", "delete"):
status = mobase.ModDataChecker.FIXABLE
break
if rp.unfold.match(name):
if is_directory(entry):
status = self.dataLooksValid(entry)
else:
status = mobase.ModDataChecker.INVALID
break
elif rp.valid.match(name) and status == mobase.ModDataChecker.INVALID:
status = mobase.ModDataChecker.VALID
elif rp.delete.match(name) or rp.move_match(name) is not None:
status = mobase.ModDataChecker.FIXABLE
else:
return mobase.ModDataChecker.INVALID
status = mobase.ModDataChecker.INVALID
break
return status
def fix(self, filetree: mobase.IFileTree) -> Optional[mobase.IFileTree]:
def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
rp = self._regex_patterns
for entry in list(filetree):
name = entry.name()
# Fix entries in pattern definition order.
for key, regex in self._regex.items():
if not regex or not regex.match(name):
continue
if key == "valid":
break
elif key == "unfold":
if (folder_tree := convert_entry_to_tree(entry)) is not None:
if folder_tree: # Not empty
# Recursively fix subtree and unfold.
if not (fixed_folder_tree := self.fix(folder_tree)):
return None
filetree.merge(fixed_folder_tree)
folder_tree.detach()
else:
print(f"Cannot unfold {name}!", file=sys.stderr)
return None
elif key == "delete":
entry.detach()
elif key == "move":
if (move_target := self._get_move_target(name)) is not None:
filetree.move(entry, move_target)
break
return filetree
def _get_move_target(self, filename: str) -> Optional[str]:
if (i := self._regex.get_match_index("move", filename)) is None:
return None
return self._move_targets[i]
# unfold first - if this match, entry is a directory (checked in
# dataLooksValid)
if rp.unfold.match(name):
assert is_directory(entry)
filetree.merge(entry)
entry.detach()
elif rp.valid.match(name):
continue
elif rp.delete.match(name):
entry.detach()
elif (move_key := rp.move_match(name)) is not None:
target = self._file_patterns.move[move_key]
filetree.move(entry, target)
return filetree
+18 -9
View File
@@ -2,14 +2,13 @@
import sys
from pathlib import Path
from typing import Callable
from typing import Callable, Sequence
import mobase
from PyQt6.QtCore import QDateTime, Qt
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
import mobase
class BasicGameSaveGame(mobase.ISaveGame):
def __init__(self, filepath: Path):
@@ -33,7 +32,13 @@ class BasicGameSaveGame(mobase.ISaveGame):
class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
def __init__(self, parent: QWidget, get_preview: Callable[[Path], Path | None]):
def __init__(
self,
parent: QWidget | None,
get_preview: Callable[
[Path], QPixmap | QImage | Path | str | None
] = lambda p: None,
):
super().__init__(parent)
self._get_preview = get_preview
@@ -69,8 +74,6 @@ class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
pixmap = QPixmap(str(value))
elif isinstance(value, str):
pixmap = QPixmap(value)
elif isinstance(value, QPixmap):
pixmap = value
elif isinstance(value, QImage):
pixmap = QPixmap.fromImage(value)
else:
@@ -89,14 +92,20 @@ class BasicGameSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
class BasicGameSaveGameInfo(mobase.SaveGameInfo):
def __init__(self, get_preview: Callable[[Path], Path | None] | None = None):
def __init__(
self,
get_preview: Callable[[Path], QPixmap | QImage | Path | str | None]
| None = None,
):
super().__init__()
self._get_preview = get_preview
def getMissingAssets(self, save: mobase.ISaveGame):
def getMissingAssets(self, save: mobase.ISaveGame) -> dict[str, Sequence[str]]:
return {}
def getSaveGameWidget(self, parent=None):
def getSaveGameWidget(
self, parent: QWidget | None = None
) -> mobase.ISaveGameInfoWidget | None:
if self._get_preview is not None:
return BasicGameSaveGameInfoWidget(parent, self._get_preview)
return None
+7
View File
@@ -0,0 +1,7 @@
from typing import TypeGuard
import mobase
def is_directory(entry: mobase.FileTreeEntry) -> TypeGuard[mobase.IFileTree]:
return entry.isDir()
+82 -63
View File
@@ -1,19 +1,18 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import shutil
import sys
from pathlib import Path
from typing import Callable, Dict, Generic, List, Optional, TypeVar, Union
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from PyQt6.QtGui import QIcon
from typing import Callable, Generic, TypeVar
import mobase
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from PyQt6.QtGui import QIcon
from .basic_features.basic_save_game_info import BasicGameSaveGame
def replace_variables(value: str, game: "BasicGame") -> str:
def replace_variables(value: str, game: BasicGame) -> str:
"""Replace special paths in the given value."""
if value.find("%DOCUMENTS%") != -1:
@@ -40,11 +39,10 @@ def replace_variables(value: str, game: "BasicGame") -> str:
return value
T = TypeVar("T")
_T = TypeVar("_T")
class BasicGameMapping(Generic[T]):
class BasicGameMapping(Generic[_T]):
# The game:
_game: "BasicGame"
@@ -58,20 +56,19 @@ class BasicGameMapping(Generic[T]):
_required: bool
# Callable returning a default value (if not required):
_default: Callable[["BasicGame"], T]
_default: Callable[["BasicGame"], _T]
# Function to apply to the value:
_apply_fn: Optional[Callable[[Union[T, str]], T]]
_apply_fn: Callable[[_T | str], _T] | None
def __init__(
self,
game,
exposed_name,
internal_method,
default: Optional[Callable[["BasicGame"], T]] = None,
apply_fn: Optional[Callable[[Union[T, str]], T]] = None,
game: BasicGame,
exposed_name: str,
internal_method: str,
default: Callable[[BasicGame], _T] | None = None,
apply_fn: Callable[[_T | str], _T] | None = None,
):
self._game = game
self._exposed_name = exposed_name
self._internal_method_name = internal_method
@@ -86,7 +83,8 @@ class BasicGameMapping(Generic[T]):
except: # noqa
raise ValueError(
"Basic game plugin from {} has an invalid {} property.".format(
game._fromName, self._exposed_name
game._fromName, # pyright: ignore[reportPrivateUsage]
self._exposed_name,
)
)
self._default = lambda game: value # type: ignore
@@ -97,11 +95,12 @@ class BasicGameMapping(Generic[T]):
):
raise ValueError(
"Basic game plugin from {} is missing {} property.".format(
game._fromName, self._exposed_name
game._fromName, # pyright: ignore[reportPrivateUsage]
self._exposed_name,
)
)
def get(self) -> T:
def get(self) -> _T:
"""Return the value of this mapping."""
value = self._default(self._game) # type: ignore
@@ -117,7 +116,7 @@ class BasicGameMapping(Generic[T]):
return value
class BasicGameOptionsMapping(BasicGameMapping[List[T]]):
class BasicGameOptionsMapping(BasicGameMapping[list[_T]]):
"""
Represents a game mappings for which multiple options are possible. The game
@@ -128,11 +127,11 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]):
def __init__(
self,
game,
exposed_name,
internal_method,
default: Optional[Callable[["BasicGame"], T]] = None,
apply_fn: Optional[Callable[[Union[List[T], str]], List[T]]] = None,
game: BasicGame,
exposed_name: str,
internal_method: str,
default: Callable[[BasicGame], _T] | None = None,
apply_fn: Callable[[list[_T] | str], list[_T]] | None = None,
):
super().__init__(game, exposed_name, internal_method, lambda g: [], apply_fn)
self._index = -1
@@ -147,7 +146,7 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]):
"""
self._index = index
def set_value(self, value: T):
def set_value(self, value: _T):
"""
Set the index corresponding of the given value. If the value is not present,
the index is set to -1.
@@ -169,7 +168,7 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]):
"""
return self._index != -1
def current(self) -> T:
def current(self) -> _T:
values = self._default(self._game) # type: ignore
if not values:
@@ -189,7 +188,6 @@ class BasicGameOptionsMapping(BasicGameMapping[List[T]]):
class BasicGameMappings:
name: BasicGameMapping[str]
author: BasicGameMapping[str]
version: BasicGameMapping[mobase.VersionInfo]
@@ -197,7 +195,7 @@ class BasicGameMappings:
gameName: BasicGameMapping[str]
gameShortName: BasicGameMapping[str]
gameNexusName: BasicGameMapping[str]
validShortNames: BasicGameMapping[List[str]]
validShortNames: BasicGameMapping[list[str]]
nexusGameId: BasicGameMapping[int]
binaryName: BasicGameMapping[str]
launcherName: BasicGameMapping[str]
@@ -208,14 +206,13 @@ class BasicGameMappings:
steamAPPId: BasicGameOptionsMapping[str]
gogAPPId: BasicGameOptionsMapping[str]
originManifestIds: BasicGameOptionsMapping[str]
originWatcherExecutables: BasicGameMapping[List[str]]
originWatcherExecutables: BasicGameMapping[list[str]]
epicAPPId: BasicGameOptionsMapping[str]
eaDesktopContentId: BasicGameOptionsMapping[str]
supportURL: BasicGameMapping[str]
@staticmethod
def _default_documents_directory(game):
def _default_documents_directory(game: mobase.IPluginGame):
folders = [
"{}/My Games/{}".format(
QStandardPaths.writableLocation(
@@ -238,7 +235,7 @@ class BasicGameMappings:
return QDir()
# Game mappings:
def __init__(self, game: "BasicGame"):
def __init__(self, game: BasicGame):
self._game = game
self.name = BasicGameMapping(game, "Name", "name")
@@ -302,9 +299,14 @@ class BasicGameMappings:
)
# Convert Union[int, str, List[Union[int, str]]] to List[str].
def ids_apply(v) -> List[str]:
def ids_apply(v: list[int] | list[str] | int | str) -> list[str]:
"""
Convert various types to a list of string. If the given value is already a
list, returns a new list with all values converted to string, otherwise
returns a list with the value convert to a string as its only element.
"""
if isinstance(v, (int, str)):
v = [v]
v = [str(v)]
return [str(x) for x in v]
self.steamAPPId = BasicGameOptionsMapping(
@@ -342,6 +344,19 @@ class BasicGameMappings:
)
_GameFeature = (
mobase.BSAInvalidation
| mobase.DataArchives
| mobase.GamePlugins
| mobase.LocalSavegames
| mobase.ModDataChecker
| mobase.ModDataContent
| mobase.SaveGameInfo
| mobase.ScriptExtender
| mobase.UnmanagedMods
)
class BasicGame(mobase.IPluginGame):
"""This class implements some methods from mobase.IPluginGame
@@ -349,11 +364,11 @@ class BasicGame(mobase.IPluginGame):
all the methods of mobase.IPluginGame."""
# List of steam, GOG, origin and Epic games:
steam_games: Dict[str, Path]
gog_games: Dict[str, Path]
origin_games: Dict[str, Path]
epic_games: Dict[str, Path]
eadesktop_games: Dict[str, Path]
steam_games: dict[str, Path]
gog_games: dict[str, Path]
origin_games: dict[str, Path]
epic_games: dict[str, Path]
eadesktop_games: dict[str, Path]
@staticmethod
def setup():
@@ -379,7 +394,7 @@ class BasicGame(mobase.IPluginGame):
_gamePath: str
# The feature map:
_featureMap: Dict
_featureMap: dict[type[_GameFeature], _GameFeature]
def __init__(self):
super(BasicGame, self).__init__()
@@ -449,7 +464,7 @@ class BasicGame(mobase.IPluginGame):
# Note: self is self._organizer.managedGame() does not work:
return self.name() == self._organizer.managedGame().name()
def settings(self) -> List[mobase.PluginSetting]:
def settings(self) -> list[mobase.PluginSetting]:
return []
# IPluginGame interface:
@@ -491,7 +506,7 @@ class BasicGame(mobase.IPluginGame):
self.gameDirectory().absoluteFilePath(self.binaryName())
)
def validShortNames(self) -> List[str]:
def validShortNames(self) -> list[str]:
return self._mappings.validShortNames.get()
def gameNexusName(self) -> str:
@@ -524,8 +539,8 @@ class BasicGame(mobase.IPluginGame):
def getSupportURL(self) -> str:
return self._mappings.supportURL.get()
def executables(self) -> List[mobase.ExecutableInfo]:
execs = []
def executables(self) -> list[mobase.ExecutableInfo]:
execs: list[mobase.ExecutableInfo] = []
if self.getLauncherName():
execs.append(
mobase.ExecutableInfo(
@@ -543,37 +558,41 @@ class BasicGame(mobase.IPluginGame):
)
return execs
def executableForcedLoads(self) -> List[mobase.ExecutableForcedLoadSetting]:
def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
return []
def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
ext = self._mappings.savegameExtension.get()
return [
BasicGameSaveGame(path)
for path in Path(folder.absolutePath()).glob(f"**/*.{ext}")
]
def initializeProfile(self, path: QDir, settings: mobase.ProfileSetting):
def initializeProfile(
self, directory: QDir, settings: mobase.ProfileSetting
) -> None:
if settings & mobase.ProfileSetting.CONFIGURATION:
for iniFile in self.iniFiles():
try:
shutil.copyfile(
self.documentsDirectory().absoluteFilePath(iniFile),
path.absoluteFilePath(QFileInfo(iniFile).fileName()),
directory.absoluteFilePath(QFileInfo(iniFile).fileName()),
)
except FileNotFoundError:
Path(path.absoluteFilePath(QFileInfo(iniFile).fileName())).touch()
Path(
directory.absoluteFilePath(QFileInfo(iniFile).fileName())
).touch()
def primarySources(self):
def primarySources(self) -> list[str]:
return []
def primaryPlugins(self):
def primaryPlugins(self) -> list[str]:
return []
def gameVariants(self):
def gameVariants(self) -> list[str]:
return []
def setGameVariant(self, variantStr):
def setGameVariant(self, variant: str) -> None:
pass
def gameVersion(self) -> str:
@@ -581,23 +600,23 @@ class BasicGame(mobase.IPluginGame):
self.gameDirectory().absoluteFilePath(self.binaryName())
)
def iniFiles(self):
def iniFiles(self) -> list[str]:
return []
def DLCPlugins(self):
def DLCPlugins(self) -> list[str]:
return []
def CCPlugins(self):
def CCPlugins(self) -> list[str]:
return []
def loadOrderMechanism(self):
return mobase.LoadOrderMechanism.PluginsTxt
return mobase.LoadOrderMechanism.PLUGINS_TXT
def sortMechanism(self):
return mobase.SortMechanism.NONE
def looksValid(self, aQDir: QDir):
return aQDir.exists(self.binaryName())
def looksValid(self, directory: QDir):
return directory.exists(self.binaryName())
def isInstalled(self) -> bool:
return bool(self._gamePath)
@@ -613,10 +632,10 @@ class BasicGame(mobase.IPluginGame):
self.gameDirectory().absoluteFilePath(self._mappings.dataDirectory.get())
)
def secondaryDataDirectories(self) -> Dict[str, QDir]:
def secondaryDataDirectories(self) -> dict[str, QDir]:
return {}
def setGamePath(self, path: Union[Path, str]):
def setGamePath(self, path: Path | str) -> None:
self._gamePath = str(path)
path = Path(path)
-1
View File
@@ -2,7 +2,6 @@ from ..basic_game import BasicGame
class AssettoCorsaGame(BasicGame):
Name = "Assetto Corsa Support Plugin"
Author = "Deorder"
Version = "0.0.1"
+42 -39
View File
@@ -1,16 +1,15 @@
# -*- encoding: utf-8 -*-
import datetime
import os
import struct
import sys
import time
from pathlib import Path
from typing import List
from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo, Qt
from PyQt6.QtGui import QPainter, QPixmap
from typing import BinaryIO
import mobase
from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo, Qt
from PyQt6.QtGui import QPainter, QPixmap
from PyQt6.QtWidgets import QWidget
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
@@ -99,9 +98,9 @@ class BlackAndWhite2ModDataChecker(mobase.ModDataChecker):
_mapFile = ["chl", "bmp", "bwe", "ter", "pat", "xml", "wal", "txt"]
_fileIgnore = ["readme", "read me", "meta.ini", "thumbs.db", "backup", ".png"]
def fix(self, tree: mobase.IFileTree):
toMove = []
for entry in tree:
def fix(self, filetree: mobase.IFileTree):
toMove: list[tuple[mobase.FileTreeEntry, str]] = []
for entry in filetree:
if any([sub in entry.name().casefold() for sub in self._fileIgnore]):
continue
elif entry.suffix() == "chl":
@@ -113,25 +112,24 @@ class BlackAndWhite2ModDataChecker(mobase.ModDataChecker):
else:
toMove.append((entry, "/Data/landscape/BW2/"))
for (entry, path) in toMove:
tree.move(entry, path, policy=mobase.IFileTree.MERGE)
for entry, path in toMove:
filetree.move(entry, path, policy=mobase.IFileTree.MERGE)
return tree
return filetree
def dataLooksValid(
self, tree: mobase.IFileTree
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
# qInfo("Data validation start")
root = tree
root = filetree
unpackagedMap = False
for entry in tree:
for entry in filetree:
entryName = entry.name().casefold()
canIgnore = any([sub in entryName for sub in self._fileIgnore])
if not canIgnore:
parent = entry.parent()
if parent is not None:
if parent != root:
parentName = parent.name().casefold()
else:
@@ -176,7 +174,7 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame):
"empty2": [0x00000108, 0x0000011C],
}
def __init__(self, filepath):
def __init__(self, filepath: Path):
super().__init__(filepath)
self._filepath = Path(filepath)
self.name: str = ""
@@ -200,11 +198,11 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame):
) - (time.localtime().tm_gmtoff * 1000)
info.close()
def readInf(self, inf, key):
def readInf(self, inf: BinaryIO, key: str):
inf.seek(self._saveInfLayout[key][0])
return inf.read(self._saveInfLayout[key][1] - self._saveInfLayout[key][0])
def allFiles(self) -> List[str]:
def allFiles(self) -> list[str]:
files = [str(file) for file in self._filepath.glob("./*")]
files.append(str(self._filepath))
return files
@@ -227,11 +225,11 @@ class BlackAndWhite2SaveGame(BasicGameSaveGame):
class BlackAndWhite2LocalSavegames(mobase.LocalSavegames):
def __init__(self, myGameSaveDir):
def __init__(self, my_game_save_dir: QDir):
super().__init__()
self._savesDir = myGameSaveDir.absolutePath()
self._savesDir = my_game_save_dir.absolutePath()
def mappings(self, profile_save_dir):
def mappings(self, profile_save_dir: QDir) -> list[mobase.Mapping]:
m = mobase.Mapping()
m.createTarget = True
@@ -241,20 +239,28 @@ class BlackAndWhite2LocalSavegames(mobase.LocalSavegames):
return [m]
def prepareProfile(self, profile):
def prepareProfile(self, profile: mobase.IProfile):
return profile.localSavesEnabled()
def getPreview(save):
save = BlackAndWhite2SaveGame(save)
def _getPreview(savepath: Path):
save = BlackAndWhite2SaveGame(savepath)
lines = [
[
("Name : " + save.getName(), Qt.AlignLeft),
("| Profile : " + save.getSaveGroupIdentifier()[1:], Qt.AlignLeft),
("Name : " + save.getName(), Qt.AlignmentFlag.AlignLeft),
(
"| Profile : " + save.getSaveGroupIdentifier()[1:],
Qt.AlignmentFlag.AlignLeft,
),
],
[("Land number : " + save.getLand(), Qt.AlignLeft)],
[("Saved at : " + save.getCreationTime().toString(), Qt.AlignLeft)],
[("Elapsed time : " + save.getElapsed(), Qt.AlignLeft)],
[("Land number : " + save.getLand(), Qt.AlignmentFlag.AlignLeft)],
[
(
"Saved at : " + save.getCreationTime().toString(),
Qt.AlignmentFlag.AlignLeft,
)
],
[("Elapsed time : " + save.getElapsed(), Qt.AlignmentFlag.AlignLeft)],
]
pixmap = QPixmap(320, 320)
@@ -269,15 +275,14 @@ def getPreview(save):
width = 0
ln = 0
for line in lines:
cHeight = 0
cWidth = 0
for (toPrint, align) in line:
for toPrint, align in line:
bRect = fm.boundingRect(toPrint)
cHeight = bRect.height() * (ln + 1)
bRect.moveTop(cHeight - bRect.height())
if align != Qt.AlignLeft:
if align != Qt.AlignmentFlag.AlignLeft:
continue
else:
bRect.moveLeft(cWidth + margin)
@@ -323,7 +328,7 @@ class BlackAndWhite2SaveGameInfoWidget(BasicGameSaveGameInfoWidget):
class BlackAndWhite2SaveGameInfo(BasicGameSaveGameInfo):
def getSaveGameWidget(self, parent=None):
def getSaveGameWidget(self, parent: QWidget | None = None):
if self._get_preview is not None:
return BasicGameSaveGameInfoWidget(parent, self._get_preview)
return None
@@ -335,7 +340,6 @@ PSTART_MENU = (
class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper):
Name = "Black & White 2 Support Plugin"
Author = "Ilyu"
Version = "1.0.1"
@@ -364,7 +368,7 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper):
self._featureMap[mobase.LocalSavegames] = BlackAndWhite2LocalSavegames(
self.savesDirectory()
)
self._featureMap[mobase.SaveGameInfo] = BlackAndWhite2SaveGameInfo(getPreview)
self._featureMap[mobase.SaveGameInfo] = BlackAndWhite2SaveGameInfo(_getPreview)
return True
def detectGame(self):
@@ -378,7 +382,7 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper):
return
def executables(self) -> List[mobase.ExecutableInfo]:
def executables(self) -> list[mobase.ExecutableInfo]:
execs = super().executables()
"""
@@ -401,8 +405,8 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper):
return execs
def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
profiles = list()
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
profiles: list[Path] = []
for path in Path(folder.absolutePath()).glob("*/Saved Games/*"):
if (
path.name == "Autosave"
@@ -425,7 +429,6 @@ class BlackAndWhite2Game(BasicGame, mobase.IPluginFileMapper):
class BOTGGame(BlackAndWhite2Game):
Name = "Black & White 2 Battle of the Gods Support Plugin"
GameName = "Black & White 2 Battle of the Gods"
+62 -33
View File
@@ -1,30 +1,34 @@
from pathlib import Path
import json
import os
from typing import List
from PyQt6.QtCore import QDateTime, QDir, QLocale, Qt
from PyQt6.QtGui import QFont
from pathlib import Path
import mobase
from PyQt6.QtWidgets import QSizePolicy, QVBoxLayout, QFormLayout, QLabel, QStyle
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo
from PyQt6.QtCore import QDateTime, QDir, QLocale, Qt
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import (
QFormLayout,
QLabel,
QSizePolicy,
QStyle,
QVBoxLayout,
QWidget,
)
from ..basic_features.basic_save_game_info import (
BasicGameSaveGame,
BasicGameSaveGameInfo,
)
from ..basic_game import BasicGame
class BaSSaveGame(BasicGameSaveGame):
def __init__(self, filepath):
def __init__(self, filepath: Path):
super().__init__(filepath)
self._filepath = Path(filepath)
with open(self._filepath, "rb") as save:
save_data = json.load(save)
self._gameMode: str = save_data["gameModeId"]
self._gender = "Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female"
self._gender = (
"Male" if save_data["creatureId"] == "PlayerDefaultMale" else "Female"
)
self._ethnicity: str = save_data["ethnicGroupId"]
h, m, s = save_data["playTime"].split(":")
self._elapsed = (int(h), int(m), float(s))
@@ -45,17 +49,23 @@ class BaSSaveGame(BasicGameSaveGame):
return f"{self._gender} {self._ethnicity}"
def getElapsed(self) -> str:
return f"{self._elapsed[0]} hours, {self._elapsed[1]} minutes, {int(self._elapsed[2])} seconds"
return (
f"{self._elapsed[0]} hours, "
f"{self._elapsed[1]} minutes, "
f"{int(self._elapsed[2])} seconds"
)
def getGameMode(self) -> str:
return self._gameMode
class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self.resize(400, 125)
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum)
sizePolicy = QSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
@@ -64,7 +74,9 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
self._verticalLayout.setObjectName("verticalLayout")
self._formLayout = QFormLayout()
self._formLayout.setObjectName("formLayout")
self._formLayout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self._formLayout.setFieldGrowthPolicy(
QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow
)
self._label = QLabel()
self._label.setObjectName("label")
@@ -111,14 +123,18 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
self._characterLabel.setFont(font1)
self._characterLabel.setText("")
self._formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self._characterLabel)
self._formLayout.setWidget(
0, QFormLayout.ItemRole.FieldRole, self._characterLabel
)
self._gameModeLabel = QLabel()
self._gameModeLabel.setObjectName("gameModeLabel")
self._gameModeLabel.setFont(font1)
self._gameModeLabel.setText("")
self._formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self._gameModeLabel)
self._formLayout.setWidget(
1, QFormLayout.ItemRole.FieldRole, self._gameModeLabel
)
self._dateLabel = QLabel()
self._dateLabel.setObjectName("dateLabel")
@@ -132,39 +148,53 @@ class BaSSaveGameInfoWidget(mobase.ISaveGameInfoWidget):
self._sessionLabel.setFont(font1)
self._sessionLabel.setText("")
self._formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self._sessionLabel)
self._formLayout.setWidget(
3, QFormLayout.ItemRole.FieldRole, self._sessionLabel
)
self._elapsedTimeLabel = QLabel()
self._elapsedTimeLabel.setObjectName("elapsedTimeLabel")
self._elapsedTimeLabel.setFont(font1)
self._elapsedTimeLabel.setText("")
self._formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self._elapsedTimeLabel)
self._formLayout.setWidget(
4, QFormLayout.ItemRole.FieldRole, self._elapsedTimeLabel
)
self._verticalLayout.addLayout(self._formLayout)
self.setLayout(self._verticalLayout)
self.setWindowFlags(Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget)
self.setWindowOpacity(
self.style().styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0
self.setWindowFlags(
Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget
)
style = self.style()
if style is not None:
self.setWindowOpacity(
style.styleHint(QStyle.StyleHint.SH_ToolTipLabel_Opacity) / 255.0
)
def setSave(self, save: mobase.ISaveGame):
assert isinstance(save, BaSSaveGame)
self._characterLabel.setText(save.getPlayerSlug())
self._gameModeLabel.setText(save.getGameMode())
t = save.getCreationTime().toLocalTime()
self._dateLabel.setText(QLocale.system().toString(t.date(), QLocale.FormatType.ShortFormat)
+ " " + QLocale.system().toString(t.time()))
self._dateLabel.setText(
QLocale.system().toString(t.date(), QLocale.FormatType.ShortFormat)
+ " "
+ QLocale.system().toString(t.time())
)
s = save.getModifiedTime().toLocalTime()
self._sessionLabel.setText(QLocale.system().toString(s.date(), QLocale.FormatType.ShortFormat)
+ " " + QLocale.system().toString(s.time()))
self._sessionLabel.setText(
QLocale.system().toString(s.date(), QLocale.FormatType.ShortFormat)
+ " "
+ QLocale.system().toString(s.time())
)
self._elapsedTimeLabel.setText(save.getElapsed())
self.resize(0, 125)
class BaSSaveGameInfo(BasicGameSaveGameInfo):
def getSaveGameWidget(self, parent=None):
def getSaveGameWidget(self, parent: QWidget | None = None):
return BaSSaveGameInfoWidget(parent)
@@ -191,9 +221,8 @@ class BaSGame(BasicGame):
self._featureMap[mobase.SaveGameInfo] = BaSSaveGameInfo()
return True
def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
ext = self._mappings.savegameExtension.get()
return [
BaSSaveGame(path)
for path in Path(folder.absolutePath()).glob(f"*.{ext}")
BaSSaveGame(path) for path in Path(folder.absolutePath()).glob(f"*.{ext}")
]
+1 -5
View File
@@ -1,16 +1,12 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
from PyQt6.QtCore import QFileInfo
import mobase
from PyQt6.QtCore import QFileInfo
from ..basic_game import BasicGame
class ControlGame(BasicGame):
Name = "Control Support Plugin"
Author = "Zash"
Version = "1.0.0"
-1
View File
@@ -2,7 +2,6 @@ from ..basic_game import BasicGame
class Cyberpunk2077Game(BasicGame):
Name = "Cyberpunk 2077 Support Plugin"
Author = "6788"
Version = "1.0.0"
+1 -2
View File
@@ -5,7 +5,6 @@ from ..basic_game import BasicGame
class DA2Game(BasicGame):
Name = "Dragon Age 2 Support Plugin"
Author = "Patchier"
@@ -26,7 +25,7 @@ class DA2Game(BasicGame):
def version(self):
# Don't forget to import mobase!
return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.final)
return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.FINAL)
def init(self, organizer: mobase.IOrganizer):
super().init(organizer)
+2 -2
View File
@@ -25,9 +25,9 @@ class DaggerfallUnityModDataChecker(mobase.ModDataChecker):
]
def dataLooksValid(
self, tree: mobase.IFileTree
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in tree:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
-1
View File
@@ -5,7 +5,6 @@ from ..basic_game import BasicGame
class DAOriginsGame(BasicGame):
Name = "Dragon Age Origins Support Plugin"
Author = "Patchier"
Version = "1.1.1"
+13 -13
View File
@@ -1,11 +1,8 @@
# -*- encoding: utf-8 -*-
import json
from pathlib import Path
from typing import List
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
import mobase
from PyQt6.QtCore import QDir, QFileInfo, QStandardPaths
from ..basic_game import BasicGame, BasicGameSaveGame
from ..steam_utils import find_steam_path
@@ -51,9 +48,9 @@ class DarkestDungeonModDataChecker(mobase.ModDataChecker):
]
def dataLooksValid(
self, tree: mobase.IFileTree
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
for entry in tree:
for entry in filetree:
if not entry.isDir():
continue
if entry.name().casefold() in self.validDirNames:
@@ -62,7 +59,7 @@ class DarkestDungeonModDataChecker(mobase.ModDataChecker):
class DarkestDungeonSaveGame(BasicGameSaveGame):
def __init__(self, filepath):
def __init__(self, filepath: Path):
super().__init__(filepath)
dataPath = filepath.joinpath("persist.game.json")
self.name: str = ""
@@ -127,12 +124,12 @@ class DarkestDungeonSaveGame(BasicGameSaveGame):
raise ValueError(
"Meta2 has wrong number of bytes: " + str(meta2DataLength)
)
meta2List = list()
meta2List: list[tuple[int, int, int]] = []
for x in range(numMeta2Entries):
entryHash = int.from_bytes(fp.read(4), "little")
offset = int.from_bytes(fp.read(4), "little")
fieldInfo = int.from_bytes(fp.read(4), "little")
meta2List.append([entryHash, offset, fieldInfo])
meta2List.append((entryHash, offset, fieldInfo))
# read Data
fp.seek(dataOffset, 0)
@@ -193,8 +190,11 @@ class DarkestDungeonGame(BasicGame):
]
@staticmethod
def getCloudSaveDirectory():
steamPath = Path(find_steam_path())
def getCloudSaveDirectory() -> str | None:
steamPath = find_steam_path()
if steamPath is None:
return None
userData = steamPath.joinpath("userdata")
for child in userData.iterdir():
name = child.name
@@ -224,8 +224,8 @@ class DarkestDungeonGame(BasicGame):
return QDir(cloudSaves)
return documentsSaves
def listSaves(self, folder: QDir) -> List[mobase.ISaveGame]:
profiles = list()
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
profiles: list[Path] = []
for path in Path(folder.absolutePath()).glob("profile_*"):
# profile_9 is only for the Multiplayer DLC "The Butcher's Circus"
# and contains different files than other profiles
+7 -7
View File
@@ -1,10 +1,8 @@
# -*- encoding: utf-8 -*-
import struct
from PyQt6.QtGui import QImage
from pathlib import Path
import mobase
from PyQt6.QtGui import QImage
from ..basic_features import BasicGameSaveGameInfo
from ..basic_game import BasicGame
@@ -31,15 +29,17 @@ class DarkMessiahOfMightAndMagicGame(BasicGame):
GameSavesDirectory = "%GAME_PATH%/mm/SAVE"
GameSaveExtension = "sav"
def _read_save_tga(self, filename):
def _read_save_tga(self, filepath: Path) -> QImage | None:
# Qt TGA reader does not work for TGA, I hope that all files
# have the same format:
with open(filename.replace(".sav", ".tga"), "rb") as fp:
with open(
filepath.parent.joinpath(filepath.name.replace(".sav", ".tga")), "rb"
) as fp:
data = fp.read()
_, _, w, h, bpp, _ = struct.unpack("<HHHHBB", data[8:18])
if bpp != 24:
return None
return QImage(data[18:], w, h, QImage.Format_RGB888)
return QImage(data[18:], w, h, QImage.Format.Format_RGB888)
def init(self, organizer: mobase.IOrganizer):
super().init(organizer)
-2
View File
@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
from ..basic_game import BasicGame
-2
View File
@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import mobase
from ..basic_features import BasicGameSaveGameInfo
+13 -18
View File
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
import os
from typing import List, Optional
from pathlib import Path
import mobase
@@ -14,13 +14,12 @@ class DivinityOriginalSinEnhancedEditionModDataChecker(mobase.ModDataChecker):
super().__init__()
def dataLooksValid(
self, tree: mobase.IFileTree
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
folders: list[mobase.IFileTree] = []
files: list[mobase.FileTreeEntry] = []
folders: List[mobase.IFileTree] = []
files: List[mobase.FileTreeEntry] = []
for entry in tree:
for entry in filetree:
if isinstance(entry, mobase.IFileTree):
folders.append(entry)
else:
@@ -53,9 +52,6 @@ class DivinityOriginalSinEnhancedEditionModDataChecker(mobase.ModDataChecker):
return mobase.ModDataChecker.INVALID
def fix(self, tree: mobase.IFileTree) -> Optional[mobase.IFileTree]:
return None
class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper):
Name = "Divinity: Original Sin (Enhanced Edition) Support Plugin"
@@ -101,10 +97,9 @@ class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper
] = DivinityOriginalSinEnhancedEditionModDataChecker()
return True
def mappings(self) -> List[mobase.Mapping]:
map = []
modDirs = [self.DOCS_MOD_SPECIAL_NAME]
self._listDirsRecursive(modDirs, prefix=self.DOCS_MOD_SPECIAL_NAME)
def mappings(self) -> list[mobase.Mapping]:
map: list[mobase.Mapping] = []
modDirs = self._listDirsRecursive(Path(self.DOCS_MOD_SPECIAL_NAME))
for dir_ in modDirs:
for file_ in self._organizer.findFiles(path=dir_, filter=lambda x: True):
m = mobase.Mapping()
@@ -121,9 +116,9 @@ class DivinityOriginalSinEnhancedEditionGame(BasicGame, mobase.IPluginFileMapper
def primarySources(self):
return self.GameValidShortNames
def _listDirsRecursive(self, dirs_list, prefix=""):
dirs = self._organizer.listDirectories(prefix)
def _listDirsRecursive(self, prefix: Path) -> list[str]:
res = [str(prefix)]
dirs = self._organizer.listDirectories(str(prefix))
for dir_ in dirs:
dir_ = os.path.join(prefix, dir_)
dirs_list.append(dir_)
self._listDirsRecursive(dirs_list, dir_)
res.extend(self._listDirsRecursive(prefix.joinpath(dir_)))
return res

Some files were not shown because too many files have changed in this diff Show More