2022-03-30 21:04:23 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import fnmatch
|
|
|
|
|
import re
|
2023-09-15 19:35:32 -05:00
|
|
|
from dataclasses import dataclass, field
|
2023-09-24 20:39:44 +02:00
|
|
|
from typing import Iterable, Literal
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
import mobase
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
from .utils import is_directory
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
class OptionalRegexPattern:
|
|
|
|
|
_pattern: re.Pattern[str] | None
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
def __init__(self, globs: Iterable[str] | None) -> None:
|
|
|
|
|
if globs is None:
|
|
|
|
|
self._pattern = None
|
|
|
|
|
else:
|
|
|
|
|
self._pattern = OptionalRegexPattern.regex_from_glob_list(globs)
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
@staticmethod
|
2023-09-15 19:35:32 -05:00
|
|
|
def regex_from_glob_list(glob_list: Iterable[str]) -> re.Pattern[str]:
|
2022-03-30 21:04:23 +02:00
|
|
|
"""
|
2023-09-15 19:35:32 -05:00
|
|
|
Returns a regex pattern form a list of glob patterns.
|
2022-04-02 00:07:28 +02:00
|
|
|
|
|
|
|
|
Every pattern has a capturing group, so that `match.lastindex - 1` will
|
|
|
|
|
give the `glob_list` index.
|
|
|
|
|
"""
|
|
|
|
|
return re.compile(
|
2023-09-15 19:35:32 -05:00
|
|
|
"|".join(f"({fnmatch.translate(f)})" for f in glob_list), re.I
|
2022-04-02 00:07:28 +02:00
|
|
|
)
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
def match(self, value: str) -> bool:
|
|
|
|
|
if self._pattern is None:
|
|
|
|
|
return False
|
|
|
|
|
return bool(self._pattern.match(value))
|
2022-04-02 00:07:28 +02:00
|
|
|
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
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)
|
|
|
|
|
|
2024-03-14 08:21:39 +01:00
|
|
|
self.move = {
|
|
|
|
|
key: re.compile(fnmatch.translate(key), re.I) for key in globs.move
|
|
|
|
|
}
|
2026-01-11 17:44:06 +01:00
|
|
|
self.ignore = OptionalRegexPattern(globs.ignore)
|
2023-09-15 19:35:32 -05:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2023-09-24 20:39:44 +02:00
|
|
|
def _merge_list(l1: list[str] | None, l2: list[str] | None) -> list[str] | None:
|
|
|
|
|
if l1 is None and l2 is None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return (l1 or []) + (l2 or [])
|
|
|
|
|
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
@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
|
2025-04-25 08:23:10 +02:00
|
|
|
move: dict[str, str] = field(default_factory=dict[str, str])
|
2026-01-11 17:44:06 +01:00
|
|
|
ignore: list[str] | None = None
|
2022-04-02 00:07:28 +02:00
|
|
|
|
2023-09-24 20:39:44 +02:00
|
|
|
def merge(
|
|
|
|
|
self, other: GlobPatterns, mode: Literal["merge", "replace"] = "replace"
|
|
|
|
|
) -> GlobPatterns:
|
|
|
|
|
"""
|
|
|
|
|
Construct a new GlobPatterns by merging the current one with the given one.
|
|
|
|
|
|
|
|
|
|
There are two different modes:
|
|
|
|
|
- 'merge': In this mode, unfold/valid/delete are concatenated and move
|
|
|
|
|
will contain the union of key from self and other, with values from other
|
|
|
|
|
overriding common keys.
|
|
|
|
|
- 'replace': The merged object will contains attributes from other, except
|
|
|
|
|
for None attributes taken from self.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
other: Other patterns to "merge" with this one.
|
|
|
|
|
mode: Merge mode.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
A new glob pattern representing the merge of this one with other.
|
|
|
|
|
"""
|
|
|
|
|
if mode == "merge":
|
|
|
|
|
return GlobPatterns(
|
|
|
|
|
unfold=_merge_list(self.unfold, other.unfold),
|
|
|
|
|
valid=_merge_list(self.valid, other.valid),
|
|
|
|
|
delete=_merge_list(self.delete, other.delete),
|
|
|
|
|
move=self.move | other.move,
|
2026-01-11 17:44:06 +01:00
|
|
|
ignore=_merge_list(self.ignore, other.ignore),
|
2023-09-24 20:39:44 +02:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return GlobPatterns(
|
|
|
|
|
unfold=other.unfold or self.unfold,
|
|
|
|
|
valid=other.valid or self.valid,
|
|
|
|
|
delete=other.delete or self.delete,
|
|
|
|
|
move=other.move or self.move,
|
2026-01-11 17:44:06 +01:00
|
|
|
ignore=other.ignore or self.ignore,
|
2023-09-24 20:39:44 +02:00
|
|
|
)
|
|
|
|
|
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
class BasicModDataChecker(mobase.ModDataChecker):
|
|
|
|
|
"""Game feature that is used to check and fix the content of a data tree
|
|
|
|
|
via simple file definitions.
|
|
|
|
|
|
2022-04-07 16:01:53 +02:00
|
|
|
The file definitions support glob pattern (without subfolders) and are
|
|
|
|
|
checked and fixed in definition order of the `file_patterns` dict.
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2022-04-02 00:07:28 +02:00
|
|
|
Args:
|
2023-09-15 19:35:32 -05:00
|
|
|
file_patterns (optional): A GlobPatterns object, with the following attributes:
|
2026-01-11 17:44:06 +01:00
|
|
|
ignore: [ "list of files and folders to ignore." ]
|
|
|
|
|
# Check result: unchanged
|
2023-09-15 19:35:32 -05:00
|
|
|
unfold: [ "list of folders to unfold" ],
|
|
|
|
|
# (remove and move contents to parent), after being checked and
|
|
|
|
|
# fixed recursively.
|
|
|
|
|
# Check result: `mobase.ModDataChecker.VALID`.
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
valid: [ "list of files and folders in the right path." ],
|
|
|
|
|
# Check result: `mobase.ModDataChecker.VALID`.
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
delete: [ "list of files/folders to delete." ],
|
|
|
|
|
# Check result: `mobase.ModDataChecker.FIXABLE`.
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
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`.
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
Example:
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
BasicModDataChecker(
|
|
|
|
|
GlobPatterns(
|
|
|
|
|
valid=["valid_folder", "*.ext1"]
|
|
|
|
|
move={"*.ext2": "path/to/target_folder/"}
|
2022-04-02 00:07:28 +02:00
|
|
|
)
|
2023-09-15 19:35:32 -05:00
|
|
|
)
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
See Also:
|
2022-04-07 16:01:53 +02:00
|
|
|
`mobase.IFileTree.move` for the `"move"` target path specs.
|
2022-03-30 21:04:23 +02:00
|
|
|
"""
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
_file_patterns: GlobPatterns
|
2022-04-07 16:01:53 +02:00
|
|
|
"""Private `file_patterns`, updated together with `._regex` and `._move_targets`."""
|
2022-04-02 00:07:28 +02:00
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
_regex_patterns: RegexPatterns
|
2022-03-30 21:04:23 +02:00
|
|
|
"""The regex patterns derived from the file (glob) patterns."""
|
|
|
|
|
|
2024-06-09 12:25:46 +02:00
|
|
|
def __init__(self, file_patterns: GlobPatterns | None = None):
|
2022-04-02 00:07:28 +02:00
|
|
|
super().__init__()
|
|
|
|
|
|
2024-06-09 12:25:46 +02:00
|
|
|
self._file_patterns = file_patterns or GlobPatterns()
|
|
|
|
|
self._regex_patterns = RegexPatterns(self._file_patterns)
|
2022-03-30 21:04:23 +02:00
|
|
|
|
|
|
|
|
def dataLooksValid(
|
|
|
|
|
self, filetree: mobase.IFileTree
|
|
|
|
|
) -> mobase.ModDataChecker.CheckReturn:
|
|
|
|
|
status = mobase.ModDataChecker.INVALID
|
2023-09-15 19:35:32 -05:00
|
|
|
|
|
|
|
|
rp = self._regex_patterns
|
2022-03-30 21:04:23 +02:00
|
|
|
for entry in filetree:
|
|
|
|
|
name = entry.name().casefold()
|
2023-09-15 19:35:32 -05:00
|
|
|
|
2026-01-11 17:44:06 +01:00
|
|
|
if rp.ignore.match(name):
|
|
|
|
|
continue
|
2023-09-15 19:35:32 -05:00
|
|
|
if rp.unfold.match(name):
|
|
|
|
|
if is_directory(entry):
|
|
|
|
|
status = self.dataLooksValid(entry)
|
|
|
|
|
else:
|
|
|
|
|
status = mobase.ModDataChecker.INVALID
|
|
|
|
|
break
|
2023-10-02 21:36:17 +02:00
|
|
|
elif rp.valid.match(name):
|
|
|
|
|
if status is mobase.ModDataChecker.INVALID:
|
|
|
|
|
status = mobase.ModDataChecker.VALID
|
2023-09-15 19:35:32 -05:00
|
|
|
elif rp.delete.match(name) or rp.move_match(name) is not None:
|
|
|
|
|
status = mobase.ModDataChecker.FIXABLE
|
2022-03-30 21:04:23 +02:00
|
|
|
else:
|
2023-09-15 19:35:32 -05:00
|
|
|
status = mobase.ModDataChecker.INVALID
|
|
|
|
|
break
|
2022-03-30 21:04:23 +02:00
|
|
|
return status
|
|
|
|
|
|
2023-09-15 19:35:32 -05:00
|
|
|
def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
|
|
|
|
|
rp = self._regex_patterns
|
2022-03-30 21:04:23 +02:00
|
|
|
for entry in list(filetree):
|
2022-04-05 23:31:33 +02:00
|
|
|
name = entry.name()
|
2022-03-30 21:04:23 +02:00
|
|
|
|
2026-01-11 17:44:06 +01:00
|
|
|
if rp.ignore.match(name):
|
|
|
|
|
continue
|
2023-09-15 19:35:32 -05:00
|
|
|
# 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
|