diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 6cc30cb..7bd91d0 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -28,14 +28,10 @@ jobs: - name: Ruff Check run: | uv run ruff --version + uv run ruff format --check uv run ruff check - name: Pyright Check run: | uv run pyright --version uv run pyright - - - name: Isort Check - run: | - uv run isort --version - uv run isort --check . diff --git a/pyproject.toml b/pyproject.toml index 51c9fc3..c80a9c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dev = [ ] build = ["build", "setuptools>=77.0.3"] test = ["coverage>=7.2.0", "pytest-qt", "pytest-timeout", "pytest>=6.0.0"] -lint = ["isort", "pyright", "ruff"] +lint = ["pyright", "ruff"] [project.urls] Homepage = "https://github.com/vkbo/subtle" @@ -51,61 +51,57 @@ include = ["subtle_gui*"] [tool.setuptools.package-data] subtle_gui = ["assets/*"] -[tool.isort] -py_version = "311" -line_length = 99 -wrap_length = 79 -multi_line_output = 5 -force_grid_wrap = 0 -lines_between_types = 1 -forced_separate = ["tests.*", "PyQt6.*"] - [tool.ruff] -line-length = 99 +line-length = 120 [tool.ruff.lint] preview = false # Rules: https://docs.astral.sh/ruff/rules select = [ - "A", # flake8-builtins (A) - "ANN", # flake8-annotations (ANN) - "B", # flake8-bugbear (B) - "E", # pycodestyle (E) - "F", # Pyflakes (F) - "FA", # flake8-future-annotations (FA) - "PERF", # Perflint (PERF) - "PLC", # Pylint Convention (PLC) - "PLE", # Pylint Error (PLE) - "PLW", # Pylint Warning (PLW) - "Q", # flake8-quotes (Q) - "RUF", # Ruff-specific rules (RUF) - "SLF", # flake8-self (SLF) - "SLOT", # flake8-slots (SLOT) - "TC", # flake8-type-checking (TC) - "UP", # pyupgrade (UP) - "W", # pycodestyle (W) + "A", # flake8-builtins (A) + "ANN", # flake8-annotations (ANN) + "B", # flake8-bugbear (B) + "D", # pydocstyle (D) + "E", # pycodestyle (E) + "F", # Pyflakes (F) + "FA", # flake8-future-annotations (FA) + "PERF", # Perflint (PERF) + "PLC", # Pylint Convention (PLC) + "PLE", # Pylint Error (PLE) + "PLR17", # Refactor (PLR) - Only PLR17xx + "PLW", # Pylint Warning (PLW) + "Q", # flake8-quotes (Q) + "RET", # flake8-return (RET) + "RUF", # Ruff-specific rules (RUF) + "SLF", # flake8-self (SLF) + "SLOT", # flake8-slots (SLOT) + "TC", # flake8-type-checking (TC) + "UP", # pyupgrade (UP) + "W", # pycodestyle (W) ] ignore = [ "ANN401", # any-type - "E221", # multiple-spaces-before-operator - "E226", # missing-whitespace-around-arithmetic-operator - "E228", # missing-whitespace-around-modulo-operator - "E241", # multiple-spaces-after-comma - "E272", # multiple-spaces-before-keyword + "D107", # undocumented-public-init + "D203", # incorrect-blank-line-before-class + "D205", # missing-blank-line-after-summary + "D213", # multi-line-summary-second-line "PLC0415", # import-outside-top-level "PLC1901", # compare-to-empty-string "PLW0108", # unnecessary-lambda + "PLW0717", # too-many-statements-in-try-clause "PLW2901", # redefined-loop-name + "RET505", # superfluous-else-return "RUF001", # ambiguous-unicode-character-string - "RUF002", # ambiguous-unicode-character-docstring "RUF015", # unnecessary-iterable-allocation-for-first-element + "RUF067", # non-empty-init-module + "RUF076", # pytest-fixture-autouse "UP015", # redundant-open-modes "UP030", # format-literals ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ANN", "SLF", "TC", "PLC2701"] +"tests/*" = ["ANN", "D101", "D102", "D105", "PLC2701", "RUF069", "SLF", "TC"] [tool.ruff.format] quote-style = "double" diff --git a/subtle.py b/subtle.py index 5f15143..dd5b69c 100755 --- a/subtle.py +++ b/subtle.py @@ -2,7 +2,8 @@ """ Subtle – Start Script ===================== -""" +""" # noqa + import os import sys @@ -10,4 +11,5 @@ if __name__ == "__main__": import subtle_gui + subtle_gui.main(sys.argv[1:]) diff --git a/subtle_gui/__init__.py b/subtle_gui/__init__.py index 6208718..16ab784 100644 --- a/subtle_gui/__init__.py +++ b/subtle_gui/__init__.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import getopt @@ -38,6 +39,7 @@ # Package Meta # ============ +# fmt: off __package__ = "subtle_gui" __copyright__ = "Copyright (C) Veronica Berglyd Olsen" __license__ = "GPLv3" @@ -46,6 +48,7 @@ __email__ = "code@vkbo.net" __version__ = "26.1.1" __date__ = "2026-06-24" +# fmt: on logger = logging.getLogger(__name__) @@ -58,12 +61,12 @@ SHARED = SharedData() # ANSI Colours -RED = "\033[91m" -GREEN = "\033[92m" +RED = "\033[91m" +GREEN = "\033[92m" YELLOW = "\033[93m" -BLUE = "\033[94m" -WHITE = "\033[97m" -END = "\033[0m" +BLUE = "\033[94m" +WHITE = "\033[97m" +END = "\033[0m" # Log Format Components TIME = "[{asctime:}]" @@ -75,7 +78,7 @@ # Read Environment FORCE_COLOR = bool(os.environ.get("FORCE_COLOR")) -NO_COLOR = bool(os.environ.get("NO_COLOR")) +NO_COLOR = bool(os.environ.get("NO_COLOR")) def main(sysArgs: list | None = None) -> GuiMain | None: @@ -113,7 +116,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Defaults logLevel = logging.WARN fmtColor = FORCE_COLOR - fmtLong = False + fmtLong = False # Parse Options try: @@ -141,10 +144,10 @@ def main(sysArgs: list | None = None) -> GuiMain | None: if fmtColor: # This will overwrite the default level names, and also ensure that # they can be converted back to integer levels - logging.addLevelName(logging.DEBUG, f"{BLUE}DEBUG{END}") - logging.addLevelName(logging.INFO, f"{GREEN}INFO{END}") - logging.addLevelName(logging.WARNING, f"{YELLOW}WARNING{END}") - logging.addLevelName(logging.ERROR, f"{RED}ERROR{END}") + logging.addLevelName(logging.DEBUG, f"{BLUE}DEBUG{END}") + logging.addLevelName(logging.INFO, f"{GREEN}INFO{END}") + logging.addLevelName(logging.WARNING, f"{YELLOW}WARNING{END}") + logging.addLevelName(logging.ERROR, f"{RED}ERROR{END}") logging.addLevelName(logging.CRITICAL, f"{RED}CRITICAL{END}") logTxt = f"{LVLC} {TEXT}" if fmtColor else f"{LVLP} {TEXT}" diff --git a/subtle_gui/common.py b/subtle_gui/common.py index ed6a594..af106d9 100644 --- a/subtle_gui/common.py +++ b/subtle_gui/common.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import json @@ -49,7 +50,7 @@ def checkInt(value: Any, default: int) -> int: def formatInt(value: int) -> str: - """Formats an integer with k, M, G etc.""" + """Format an integer with k, M, G etc.""" if not isinstance(value, int): return "ERR" @@ -74,11 +75,10 @@ def textCleanup(text: str) -> str: def regexCleanup(text: str, patterns: list[tuple[re.Pattern, str]]) -> str: - """Replaces all occurrences of match group 1 in patterns.""" + """Replace all occurrences of match group 1 in patterns.""" for regEx, value in patterns: matches = [ - (s, e, value) for match in regEx.finditer(text) - if (s := match.start(1)) >= 0 and (e := match.end(1)) >= 0 + (s, e, value) for match in regEx.finditer(text) if (s := match.start(1)) >= 0 and (e := match.end(1)) >= 0 ] for s, e, value in reversed(matches): text = text[:s] + value + text[e:] @@ -120,22 +120,22 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: elif first in ("{", "["): n += 1 - indent = "\n"+" "*n - if n > nmax and nmax > 0: + indent = "\n" + " " * n + if n > nmax > 0: buffer.append(chunk) else: buffer.append(chunk[0] + indent + chunk[1:]) elif first in ("}", "]"): n -= 1 - indent = "\n"+" "*n - if n >= nmax and nmax > 0: + indent = "\n" + " " * n + if n >= nmax > 0: buffer.append(chunk) else: buffer.append(indent + chunk) elif first == ",": - if n > nmax and nmax > 0: + if n > nmax > 0: buffer.append(chunk) else: buffer.append(chunk[0] + indent + chunk[1:].lstrip()) @@ -148,8 +148,8 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: def formatTS(value: int) -> str: """Format millisecond integer as HH:MM:SS,uuu timestamp.""" - i, f = value//1000, value%1000 - return f"{i//3600:02d}:{i%3600//60:02d}:{i%60:02d},{f:03d}" + i, f = value // 1000, value % 1000 + return f"{i // 3600:02d}:{i % 3600 // 60:02d}:{i % 60:02d},{f:03d}" def decodeTS(value: str | None, default: int = 0, fmt: T_Subs = "SRT") -> int: @@ -158,21 +158,13 @@ def decodeTS(value: str | None, default: int = 0, fmt: T_Subs = "SRT") -> int: if fmt == "SRT" and len(value) >= 12: if value[2] == ":" and value[5] == ":" and value[8] in ".,": try: - return ( - 3600000*int(value[0:2]) - + 60000*int(value[3:5]) - + int(value[6:8] + value[9:12]) - ) + return 3600000 * int(value[0:2]) + 60000 * int(value[3:5]) + int(value[6:8] + value[9:12]) except Exception: pass elif fmt == "SSA" and len(value) == 10: if value[1] == ":" and value[4] == ":" and value[7] in ":.,": try: - return ( - 3600000*int(value[0]) - + 60000*int(value[2:4]) - + 10*int(value[5:7] + value[8:10]) - ) + return 3600000 * int(value[0]) + 60000 * int(value[2:4]) + 10 * int(value[5:7] + value[8:10]) except Exception: pass return default diff --git a/subtle_gui/config.py b/subtle_gui/config.py index bbfe439..c4c2144 100644 --- a/subtle_gui/config.py +++ b/subtle_gui/config.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import json @@ -31,10 +32,7 @@ from subtle_gui.common import jsonEncode -from PyQt6.QtCore import ( - PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QSize, - QStandardPaths, QSysInfo -) +from PyQt6.QtCore import PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QSize, QStandardPaths, QSysInfo from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QApplication @@ -58,28 +56,25 @@ "guiFont": "", "fixedFont": "", "subsFont": "", - } + }, } T_Fonts = Literal["gui"] | Literal["fixed"] | Literal["subs"] class Config: + """Main Config Class.""" def __init__(self) -> None: self._data: dict[str, dict] = deepcopy(DEFAULTS) - self.appName = "Subtle" + self.appName = "Subtle" self.appHandle = "subtle" # Set Paths - confRoot = Path(QStandardPaths.writableLocation( - QStandardPaths.StandardLocation.ConfigLocation) - ) - cacheRoot = Path(QStandardPaths.writableLocation( - QStandardPaths.StandardLocation.CacheLocation) - ) + confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.ConfigLocation)) + cacheRoot = Path(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.CacheLocation)) self._confPath = confRoot.absolute() / self.appHandle # The user config location self._cachePath = cacheRoot.absolute() / self.appHandle # The user cache location self._homePath = Path.home().absolute() # The user's home directory @@ -95,19 +90,19 @@ def __init__(self) -> None: self.subsFont = QFont() # Check Qt6 Versions - self.verQtString = QT_VERSION_STR - self.verQtValue = QT_VERSION + self.verQtString = QT_VERSION_STR + self.verQtValue = QT_VERSION self.verPyQtString = PYQT_VERSION_STR - self.verPyQtValue = PYQT_VERSION + self.verPyQtValue = PYQT_VERSION # Check Python Version self.verPyString = sys.version.split()[0] # Check OS Type - self.osType = sys.platform - self.osLinux = False + self.osType = sys.platform + self.osLinux = False self.osWindows = False - self.osDarwin = False + self.osDarwin = False self.osUnknown = False if self.osType.startswith("linux"): self.osLinux = True @@ -119,11 +114,9 @@ def __init__(self) -> None: self.osUnknown = True # Other System Info - self.hostName = QSysInfo.machineHostName() + self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() - return - ## # Properties ## @@ -172,7 +165,6 @@ def assetPath(self, resource: str, kind: str | None = None) -> Path: path /= kind return path / resource - ## # Setters ## @@ -181,7 +173,6 @@ def setSize(self, key: str, value: QSize) -> None: """Set a size in config.""" if isinstance(value, QSize): self._data["Sizes"][key] = [value.width(), value.height()] - return def setSizes(self, key: str, value: list[int]) -> None: """Set a size in config.""" @@ -190,10 +181,9 @@ def setSizes(self, key: str, value: list[int]) -> None: self._data["Sizes"][key] = [int(x) for x in value] except Exception as e: logger.error("Problem when saving sizes list", exc_info=e) - return def setFontSpec(self, target: T_Fonts, font: QFont | str) -> None: - """Set the font """ + """Set a font in config.""" if isinstance(font, str): temp = QFont() temp.fromString(font) @@ -210,8 +200,6 @@ def setFontSpec(self, target: T_Fonts, font: QFont | str) -> None: self.subsFont = font self._data["Fonts"]["subsFont"] = font.toString() - return - ## # Methods ## @@ -220,17 +208,16 @@ def initialise(self) -> None: """Initialise the config.""" self._confPath.mkdir(exist_ok=True) self._cachePath.mkdir(exist_ok=True) - return def cleanup(self) -> None: - """Called before exit to clean up cache.""" + """Clean up cache. Called before exit.""" path = self._cachePath / "dump" if path.exists(): logger.debug("Clearing session cache") shutil.rmtree(path) - return def localisation(self, app: QApplication) -> None: + """Set up localisation.""" return def fonts(self, app: QApplication) -> None: @@ -249,9 +236,8 @@ def fonts(self, app: QApplication) -> None: self.setFontSpec("subs", font) else: temp = app.font() - temp.setPointSizeF(3.0*temp.pointSizeF()) + temp.setPointSizeF(3.0 * temp.pointSizeF()) self.setFontSpec("subs", temp) - return def load(self) -> None: """Load the app config.""" @@ -265,7 +251,6 @@ def load(self) -> None: self._storeConfigGroup(data, "Fonts") except Exception as e: logger.error("Could not load config", exc_info=e) - return def save(self) -> None: """Save the app config.""" @@ -275,7 +260,6 @@ def save(self) -> None: fo.write(jsonEncode(self._data, nmax=2)) except Exception: logger.error("Could not save config") - return ## # Internal Functions @@ -288,4 +272,3 @@ def _storeConfigGroup(self, data: dict, group: str) -> None: default = DEFAULTS.get(group, {}) values = {k: v for k, v in loaded.items() if k in default} self._data[group].update(values) - return diff --git a/subtle_gui/constants.py b/subtle_gui/constants.py index 5baa2e3..271c00a 100644 --- a/subtle_gui/constants.py +++ b/subtle_gui/constants.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations from enum import Enum @@ -27,23 +28,25 @@ def trConst(text: str) -> str: - """Wrapper function for locally translating constants.""" + """Wrap locally translating constants.""" return QCoreApplication.translate("Constant", text) class MediaType(Enum): + """Media types.""" VIDEO = 0 AUDIO = 1 - SUBS = 2 + SUBS = 2 OTHER = 4 class GuiLabels: + """GUI labels.""" MEDIA_TYPES: Final[dict[MediaType, str]] = { MediaType.VIDEO: QT_TRANSLATE_NOOP("Constant", "Video"), MediaType.AUDIO: QT_TRANSLATE_NOOP("Constant", "Audio"), - MediaType.SUBS: QT_TRANSLATE_NOOP("Constant", "Subtitles"), + MediaType.SUBS: QT_TRANSLATE_NOOP("Constant", "Subtitles"), MediaType.OTHER: QT_TRANSLATE_NOOP("Constant", "Other"), } diff --git a/subtle_gui/core/icons.py b/subtle_gui/core/icons.py index 1eb9131..5069acc 100644 --- a/subtle_gui/core/icons.py +++ b/subtle_gui/core/icons.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -32,29 +33,26 @@ RAW_SVG = { "up": ( b'' ), "down": ( b'' ), - "italic": ( - b'' - ), + "italic": (b''), "note": ( b'' ), @@ -67,32 +65,26 @@ class GuiIcons: + """GUI Icons.""" def __init__(self) -> None: palette = QApplication.palette() self._cache: dict[str, QIcon] = {} self._color = palette.buttonText().color().name(QColor.NameFormat.HexRgb).encode() - return def icon(self, key: str) -> QIcon: """Return an icon, either from the cache or generate it.""" if key not in self._cache: self._cache[key] = QIcon( - _IconEngine( - BASE - .replace(b"{content}", RAW_SVG.get(key, b"")) - .replace(b"{foreground}", self._color) - ) + _IconEngine(BASE.replace(b"{content}", RAW_SVG.get(key, b"")).replace(b"{foreground}", self._color)) ) return self._cache[key] class _IconEngine(QIconEngine): - def __init__(self, data: bytes) -> None: super().__init__() self._data = data - return def clone(self) -> QIconEngine | None: """Clone the icon engine.""" @@ -107,10 +99,7 @@ def pixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State) -> QPixmap: self.paint(painter, QRect(QPoint(0, 0), size), mode, state) return pix - def paint( - self, painter: QPainter | None, rect: QRect, mode: QIcon.Mode, state: QIcon.State - ) -> None: + def paint(self, painter: QPainter | None, rect: QRect, mode: QIcon.Mode, state: QIcon.State) -> None: """SVG icon painter.""" renderer = QSvgRenderer(self._data) renderer.render(painter, QRectF(rect)) - return diff --git a/subtle_gui/core/media.py b/subtle_gui/core/media.py index 89a3ac6..fa5bb05 100644 --- a/subtle_gui/core/media.py +++ b/subtle_gui/core/media.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -44,6 +45,7 @@ class MediaData(QObject): + """Shared data object for media files and tracks.""" mediaDataCleared = pyqtSignal() newMediaLoaded = pyqtSignal() @@ -54,7 +56,6 @@ def __init__(self) -> None: self._tracks: dict[str, MediaTrack] = {} self._track: MediaTrack | None = None self._file: MediaFile | None = None - return @property def hasMedia(self) -> bool: @@ -77,7 +78,6 @@ def clear(self) -> None: self._track = None self._file = None self.mediaDataCleared.emit() - return def loadMediaFile(self, path: Path) -> None: """Load a media file into the data store.""" @@ -90,7 +90,6 @@ def loadMediaFile(self, path: Path) -> None: if idx := track.trackID: self._tracks[idx] = track self.newMediaLoaded.emit() - return def setCurrentTrack(self, trackID: str) -> None: """Set the current active track.""" @@ -98,7 +97,6 @@ def setCurrentTrack(self, trackID: str) -> None: self._track = track SHARED.setSpellLanguage(track) self.newTrackSelected.emit() - return def iterTracks(self) -> Iterable[MediaTrack]: """Iterate through all tracks.""" @@ -110,6 +108,7 @@ def getTrack(self, trackID: str) -> MediaTrack | None: class MediaTrack: + """Represents a media track within a media file.""" def __init__(self, media: MediaData, info: dict) -> None: self._media = media @@ -145,8 +144,6 @@ def __init__(self, media: MediaData, info: dict) -> None: else: logger.info("Unsupported subtitle format: %s", codec_id or codec_nm) - return - ## # Properties ## @@ -220,14 +217,12 @@ def duration(self) -> int: def setTrackFile(self, path: Path) -> None: """Set the extraction location of the track file.""" self._path = path - return def readTrackFile(self) -> None: """Read the content of the track file.""" if self._path and self._wrapper: self._wrapper.read(self._path) self._wrapper.checkFrames() - return def getFrame(self, index: int) -> FrameBase | None: """Return a subtitles frame.""" @@ -245,10 +240,8 @@ def copyFrames(self, target: SubtitlesBase) -> None: """Copy all frames from current track to target.""" if self._wrapper: target.copyFrames(self._wrapper) - return def copyText(self, source: SubtitlesBase) -> None: """Copy all text from source to current track.""" if self._wrapper: self._wrapper.copyText(source) - return diff --git a/subtle_gui/core/mediafile.py b/subtle_gui/core/mediafile.py index f11ed7b..b4011cb 100644 --- a/subtle_gui/core/mediafile.py +++ b/subtle_gui/core/mediafile.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import json @@ -38,17 +39,18 @@ class ContainerType(IntEnum): + """Container Type Enum.""" - UNKNOWN = 0 - AVI = 5 - MATROSKA = 17 + UNKNOWN = 0 + AVI = 5 + MATROSKA = 17 MPEG_STREAM = 21 - OGM = 23 - PGSSUP = 24 - QUICKTIME = 25 - SRT = 27 - SSA_ASS = 28 - VOBSUB = 34 + OGM = 23 + PGSSUP = 24 + QUICKTIME = 25 + SRT = 27 + SSA_ASS = 28 + VOBSUB = 34 EXTRACTABLE = ( @@ -77,7 +79,6 @@ def __init__(self, file: Path) -> None: self._id = sha1(bytes(file), usedforsecurity=False).hexdigest() self._info = {} self._process() - return @property def filePath(self) -> Path: @@ -145,4 +146,3 @@ def _process(self) -> None: except Exception as e: logger.error("Failed to load media file info", exc_info=e) self._info = {} - return diff --git a/subtle_gui/core/mkvextract.py b/subtle_gui/core/mkvextract.py index 6b05a3f..c94f6c4 100644 --- a/subtle_gui/core/mkvextract.py +++ b/subtle_gui/core/mkvextract.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -35,6 +36,7 @@ class MkvExtract(QObject): + """Wrapper for mkvextract process.""" processProgress = pyqtSignal(int) processDone = pyqtSignal(int) @@ -43,7 +45,6 @@ def __init__(self, parent: QObject) -> None: super().__init__(parent=parent) self._process = None self._pid = 0 - return def extract(self, file: Path, tracks: list[tuple[str, Path]]) -> None: """Start a subprocess running mkvextract.""" @@ -56,14 +57,12 @@ def extract(self, file: Path, tracks: list[tuple[str, Path]]) -> None: self._process.start("mkvextract", args) self._pid = self._process.processId() logger.debug("Starting process %d", self._pid) - return def cancel(self) -> None: """Cancel the process.""" if isinstance(self._process, QProcess): logger.debug("Killing process %d", self._pid) self._process.kill() - return ## # Private Slots @@ -76,7 +75,6 @@ def _processStdOut(self) -> None: text = self._process.readAllStandardOutput().data().decode("utf-8").strip() if text.startswith("#GUI#progress"): self.processProgress.emit(checkInt(text[13:].strip().removesuffix("%"), 0)) - return @pyqtSlot() def _processFinished(self) -> None: @@ -86,4 +84,3 @@ def _processFinished(self) -> None: logger.debug("Process %d exited with return code %d", self._pid, code) self.processDone.emit(code) self._process = None - return diff --git a/subtle_gui/core/spellcheck.py b/subtle_gui/core/spellcheck.py index 6375162..658a56d 100644 --- a/subtle_gui/core/spellcheck.py +++ b/subtle_gui/core/spellcheck.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import json @@ -37,7 +38,7 @@ class SpellEnchant: - """Core: Enchant Spell Checking Wrapper + """Core: Enchant Spell Checking Wrapper. This is a wrapper class for Enchant to keep the API consistent between spell check tools. @@ -48,7 +49,6 @@ def __init__(self) -> None: self._userDict = UserDictionary() self._language = None self._broker = None - return ## # Properties @@ -56,6 +56,7 @@ def __init__(self) -> None: @property def spellLanguage(self) -> str | None: + """Get the currently loaded spell checking language.""" return self._language ## @@ -93,21 +94,19 @@ def setLanguage(self, language: str | None) -> None: for word in self._userDict: self._enchant.add_to_session(word) - return - ## # Methods ## def checkWord(self, word: str) -> bool: - """Wrapper function for pyenchant.""" + """Wrap pyenchant check.""" try: return bool(self._enchant.check(word)) except Exception: return True def suggestWords(self, word: str) -> list[str]: - """Wrapper function for pyenchant.""" + """Wrap pyenchant suggestions.""" try: return self._enchant.suggest(word) except Exception: @@ -129,6 +128,7 @@ def listDictionaries(self) -> list[tuple[str, str]]: lang = [] try: import enchant + tags = [x for x, _ in enchant.list_dicts()] lang = [(x, f"{QLocale(x).nativeLanguageName().title()} [{x}]") for x in set(tags)] except Exception: @@ -158,28 +158,31 @@ class FakeProvider: self.tag = "" self.provider = FakeProvider() - return - def check(self, word: str) -> bool: + """Wrap pyenchant check.""" return True def suggest(self, word: str) -> list[str]: + """Wrap pyenchant suggestions.""" return [] def add_to_session(self, word: str) -> None: + """Wrap pyenchant add_to_session.""" return class UserDictionary: + """A simple user dictionary that is saved to a JSON file.""" def __init__(self) -> None: self._words = set() - return def __contains__(self, word: str) -> bool: + """Check if a word is in the dictionary.""" return word in self._words def __iter__(self) -> Iterator[str]: + """Iterate over the words in the dictionary.""" return iter(self._words) def add(self, word: str) -> bool: @@ -204,7 +207,6 @@ def load(self) -> None: logger.info("Loaded user dictionary") except Exception as exc: logger.error("Failed to load user dictionary", exc_info=exc) - return def save(self) -> None: """Save the user's dictionary.""" @@ -216,4 +218,3 @@ def save(self) -> None: json.dump(data, fObj, indent=2) except Exception as exc: logger.error("Failed to save user dictionary", exc_info=exc) - return diff --git a/subtle_gui/formats/base.py b/subtle_gui/formats/base.py index 9a1e1b2..4816aec 100644 --- a/subtle_gui/formats/base.py +++ b/subtle_gui/formats/base.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -37,15 +38,16 @@ class SubtitlesBase(ABC): + """Base class for subtitle formats.""" __slots__ = ("_frames", "_path") def __init__(self) -> None: self._path: Path | None = None self._frames: list[FrameBase] = [] - return def __repr__(self) -> str: + """Return a string representation of the subtitle object.""" return f"<{self.__class__.__name__}: id={id(self)}>" def frameCount(self) -> int: @@ -65,7 +67,7 @@ def iterFrames(self) -> Iterable[FrameBase]: def checkFrames(self) -> None: """Check all frames to ensure that time stamps make sense.""" for i in range(len(self._frames) - 1): - cf, nf = self._frames[i:i+2] + cf, nf = self._frames[i : i + 2] if cf.end < cf.start: te = cf.end ts = cf.start @@ -73,20 +75,25 @@ def checkFrames(self) -> None: cf.setRange(end=max(ts + 90, tn)) logger.warning( "Correcting end time for frame %d to %s (%+.3fs from %+.3fs)", - i+1, formatTS(tn), (tn-ts)/1000.0, (te-ts)/1000.0 + i + 1, + formatTS(tn), + (tn - ts) / 1000.0, + (te - ts) / 1000.0, ) - return @abstractmethod def read(self, path: Path) -> None: + """Read subtitles from a file.""" raise NotImplementedError @abstractmethod def write(self, path: Path | None = None) -> None: + """Write subtitles to a file.""" raise NotImplementedError @abstractmethod def copyFrames(self, other: SubtitlesBase) -> None: + """Copy frames from another subtitle object.""" raise NotImplementedError def copyText(self, other: SubtitlesBase) -> None: @@ -107,20 +114,16 @@ def copyText(self, other: SubtitlesBase) -> None: if (pos := frame.start + offset) in frames: frames[pos].setText(frame.text) - return - def _copyFrames(self, frameType: type[FrameBase], other: SubtitlesBase) -> None: """Copy frame content from other class. Must be implemented in subclasses by passing its own FrameBase implementation as frameType. """ - self._frames = [ - frameType.fromFrame(n, frame) for n, frame in enumerate(other.iterFrames()) - ] - return + self._frames = [frameType.fromFrame(n, frame) for n, frame in enumerate(other.iterFrames())] class FrameBase(ABC): + """Base class for subtitle frames.""" __slots__ = ("_end", "_index", "_start", "_text") @@ -129,7 +132,6 @@ def __init__(self, index: int) -> None: self._start: int = -1 self._end: int = -1 self._text: list[str] = [] - return @classmethod @abstractmethod @@ -171,7 +173,6 @@ def imageBased(self) -> bool: def setText(self, text: list[str]) -> None: """Set the frame's text.""" self._text = [t.strip() for t in text if t.strip()] - return def setRange(self, *, start: int | None = None, end: int | None = None) -> None: """Update the start and end times.""" @@ -179,7 +180,6 @@ def setRange(self, *, start: int | None = None, end: int | None = None) -> None: self._start = start if end: self._end = end - return @abstractmethod def getImage(self) -> QImage: diff --git a/subtle_gui/formats/pgssubs.py b/subtle_gui/formats/pgssubs.py index 8d20036..f7c6832 100644 --- a/subtle_gui/formats/pgssubs.py +++ b/subtle_gui/formats/pgssubs.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -38,10 +39,10 @@ logger = logging.getLogger(__name__) COMP_NORMAL = 0x00 -COMP_ACQ = 0x40 -COMP_EPOCH = 0x80 +COMP_ACQ = 0x40 +COMP_EPOCH = 0x80 -IMAGE_FILL = 0xff242424 +IMAGE_FILL = 0xFF242424 CROP_MARGINS = QMargins(20, 20, 20, 20) @@ -54,7 +55,6 @@ class PGSSubs(SubtitlesBase): def __init__(self) -> None: super().__init__() - return def read(self, path: Path) -> None: """Read a PGS file.""" @@ -63,13 +63,13 @@ def read(self, path: Path) -> None: self._path = path except Exception as e: logger.error("Failed to read file data", exc_info=e) - return def write(self, path: Path | None = None) -> None: """Write a PGS file.""" raise NotImplementedError("Cannot write PGS files.") def copyFrames(self, other: SubtitlesBase) -> None: + """Copy frames from another subtitle object.""" return super()._copyFrames(PGSFrame, other) ## @@ -142,16 +142,14 @@ def _readData(self, path: Path) -> None: self._frames = frames - return - class PGSFrame(FrameBase): + """PGS Subtitle Frame Class.""" def __init__(self, index: int, ds: DisplaySet) -> None: super().__init__(index=index) self._ds = ds self._start = int(ds.timestamp / 90.0) - return @classmethod def fromFrame(cls, index: int, other: FrameBase) -> FrameBase: @@ -160,13 +158,13 @@ def fromFrame(cls, index: int, other: FrameBase) -> FrameBase: @property def imageBased(self) -> bool: - """This class is image based.""" + """Check if the frame is image based.""" + # A PGS frame is always image-based, as it is a bitmap subtitle format return True def closeFrame(self, ds: DisplaySet) -> None: """Extract timestamp from display set used to close frame.""" self._end = int(ds.timestamp / 90.0) - return def getImage(self) -> QImage: """Return the rendered image.""" @@ -174,6 +172,7 @@ def getImage(self) -> QImage: class DisplaySet: + """PGS Display Set Class.""" __slots__ = ("_image", "_ods", "_pcs", "_pds", "_wds") @@ -183,9 +182,9 @@ def __init__(self, pcs: PresentationSegment) -> None: self._pds: dict[int, PaletteSegment] = {} self._ods: dict[int, list[ObjectSegment]] = {} self._image: QImage | None = None - return def __repr__(self) -> str: + """Return a string representation of the display set.""" return ( f"<{self.__class__.__name__}: " f"composition={self._pcs.compNumber} " @@ -198,10 +197,12 @@ def __repr__(self) -> str: @property def pcs(self) -> PresentationSegment: + """Return the presentation segment.""" return self._pcs @property def timestamp(self) -> float: + """Return the timestamp of the display set.""" return self._pcs.timestamp def addWDS(self, wds: WindowSegment, pos: int) -> None: @@ -211,7 +212,6 @@ def addWDS(self, wds: WindowSegment, pos: int) -> None: self._wds[idx] = rect else: logger.warning("Skipping invalid WindowSegment at pos %d", pos) - return def addPDS(self, pds: PaletteSegment, pos: int) -> None: """Save the segment mapped to its id.""" @@ -219,7 +219,6 @@ def addPDS(self, pds: PaletteSegment, pos: int) -> None: self._pds[pds.id] = pds else: logger.warning("Skipping invalid PaletteSegment at pos %d", pos) - return def addODS(self, ods: ObjectSegment, pos: int) -> None: """Save the segment mapped to its id.""" @@ -230,12 +229,13 @@ def addODS(self, ods: ObjectSegment, pos: int) -> None: self._ods[oid].append(ods) else: logger.warning("Skipping invalid ObjectSegment at pos %d", pos) - return def isValid(self) -> bool: + """Check if the display set is valid.""" return self._pcs is not None and self._pcs.valid def isClearFrame(self) -> bool: + """Check if the display set is a clear frame.""" return self._pcs.compState == COMP_NORMAL and self._pcs.compObjectCount == 0 def render(self, crop: bool = True) -> QImage: @@ -281,23 +281,21 @@ def render(self, crop: bool = True) -> QImage: if (b1 := data[p]) > 0x00: raw += palette[b1] p += 1 - elif (b2 := data[p+1]) <= 0x3f: + elif (b2 := data[p + 1]) <= 0x3F: raw += palette[0] * b2 p += 2 - elif b2 <= 0x7f: - raw += palette[0] * ((b2 & 0x3f)*256 + data[p+2]) + elif b2 <= 0x7F: + raw += palette[0] * ((b2 & 0x3F) * 256 + data[p + 2]) p += 3 - elif b2 <= 0xbf: - raw += palette[data[p+2]] * (b2 & 0x3f) + elif b2 <= 0xBF: + raw += palette[data[p + 2]] * (b2 & 0x3F) p += 3 else: - raw += palette[data[p+3]] * ((b2 & 0x3f)*256 + data[p+2]) + raw += palette[data[p + 3]] * ((b2 & 0x3F) * 256 + data[p + 2]) p += 4 frame = frame.united(QRect(offset, box)) - painter.drawImage(offset, QImage( - bytes(raw), box.width(), box.height(), QImage.Format.Format_ARGB32 - )) + painter.drawImage(offset, QImage(bytes(raw), box.width(), box.height(), QImage.Format.Format_ARGB32)) painter.end() if crop: @@ -308,7 +306,8 @@ def render(self, crop: bool = True) -> QImage: return self._image -class BaseSegment(ABC): +class SegmentBase(ABC): + """Base class for PGS segments.""" __slots__ = ("_data", "_ts", "_valid") @@ -317,39 +316,39 @@ def __init__(self, ts: int, data: bytes) -> None: self._data = data self._valid = False self.validate() - return def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}: t={self._ts/90000:.3f} " - f"size={len(self._data)} valid={self._valid}>" - ) + """Return a string representation of the segment.""" + return f"<{self.__class__.__name__}: t={self._ts / 90000:.3f} size={len(self._data)} valid={self._valid}>" @property def valid(self) -> bool: + """Check if the segment is valid.""" return self._valid @property def timestamp(self) -> float: + """Return the timestamp of the segment.""" return self._ts @abstractmethod def validate(self) -> None: + """Validate the segment data.""" raise NotImplementedError -class PresentationSegment(BaseSegment): - """Presentation Composition Segment +class PresentationSegment(SegmentBase): + """Presentation Composition Segment. The Presentation Composition Segment is used for composing a sub picture. """ def validate(self) -> None: - """Length is 11 + n*8""" + """Validate the segment data.""" + # Length is 11 + n*8 size = len(self._data) - self._valid = (size >= 11 and size % 8 == 3) - return + self._valid = size >= 11 and size % 8 == 3 @property def size(self) -> QSize: @@ -368,7 +367,9 @@ def compNumber(self) -> int: @property def compState(self) -> int: - """Type of this composition. Allowed values are: + """Return the type of this composition. + + Allowed values are: 0x00: Normal 0x40: Acquisition Point 0x80: Epoch Start @@ -377,8 +378,9 @@ def compState(self) -> int: @property def paletteUpdate(self) -> bool: - """Indicates if this PCS describes a Palette only Display - Update. Allowed values are: + """Returns True if this composition is a Palette only Display Update. + + Allowed values are: 0x00: False 0x80: True """ @@ -397,7 +399,9 @@ def compObjectCount(self) -> int: return int.from_bytes(self._data[10:11]) def compObjects(self) -> Iterable[tuple[int, int, QPoint]]: - """The composition objects, also known as window information + """Return an iterator over the composition objects defined in this segment. + + The composition objects, also known as window information objects, define the position on the screen of every image that will be shown. @@ -410,18 +414,18 @@ def compObjects(self) -> Iterable[tuple[int, int, QPoint]]: pos = 11 size = len(self._data) while pos < size: - o = int.from_bytes(self._data[pos:pos+2]) # Object ID - w = int.from_bytes(self._data[pos+2:pos+3]) # Window ID - f = int.from_bytes(self._data[pos+3:pos+4]) # Crop flag, only used for offset - x = int.from_bytes(self._data[pos+4:pos+6]) # Horizontal position - y = int.from_bytes(self._data[pos+6:pos+8]) # Vertical position - pos += (16 if f == 0x40 else 8) + o = int.from_bytes(self._data[pos : pos + 2]) # Object ID + w = int.from_bytes(self._data[pos + 2 : pos + 3]) # Window ID + f = int.from_bytes(self._data[pos + 3 : pos + 4]) # Crop flag, only used for offset + x = int.from_bytes(self._data[pos + 4 : pos + 6]) # Horizontal position + y = int.from_bytes(self._data[pos + 6 : pos + 8]) # Vertical position + pos += 16 if f == 0x40 else 8 yield o, w, QPoint(x, y) return -class WindowSegment(BaseSegment): - """Window Definition Segment +class WindowSegment(SegmentBase): + """Window Definition Segment. This segment is used to define the rectangular area on the screen where the sub picture will be shown. This rectangular area is called @@ -431,39 +435,44 @@ class WindowSegment(BaseSegment): """ def validate(self) -> None: - """Length is 1 + n*9""" - self._valid = (len(self._data) % 9 == 1) - return + """Validate the segment data.""" + # Length is 1 + n*9 + self._valid = len(self._data) % 9 == 1 @property def count(self) -> int: + """Return the number of windows defined in the segment.""" return int.from_bytes(self._data[0:1]) def windows(self) -> Iterable[tuple[int, QRect]]: """Iterate over windows defined in the segment.""" for pos in range(1, len(self._data), 9): - yield int.from_bytes(self._data[pos:pos+1]), QRect( - int.from_bytes(self._data[pos+1:pos+3]), - int.from_bytes(self._data[pos+3:pos+5]), - int.from_bytes(self._data[pos+5:pos+7]), - int.from_bytes(self._data[pos+7:pos+9]), + yield ( + int.from_bytes(self._data[pos : pos + 1]), + QRect( + int.from_bytes(self._data[pos + 1 : pos + 3]), + int.from_bytes(self._data[pos + 3 : pos + 5]), + int.from_bytes(self._data[pos + 5 : pos + 7]), + int.from_bytes(self._data[pos + 7 : pos + 9]), + ), ) return -class PaletteSegment(BaseSegment): - """Palette Definition Segment +class PaletteSegment(SegmentBase): + """Palette Definition Segment. This segment is used to define a palette for color conversion. """ + __slots__ = ("_col",) def validate(self) -> None: - """Length is 2 + n*5""" + """Validate the segment data.""" + # Length is 2 + n*5 size = len(self._data) - self._valid = (size >= 7 and size % 5 == 2) + self._valid = size >= 7 and size % 5 == 2 self._col: dict[int, QColor] = {} - return @property def id(self) -> int: @@ -482,28 +491,28 @@ def palette(self) -> list[bytes]: """ palette = [IMAGE_FILL.to_bytes(4, "little")] * 256 for pos in range(2, len(self._data), 5): - if a := self._data[pos+4]: # We ignore transparent colours - y = self._data[pos+1] - 16 - cr = self._data[pos+2] - 128 - cb = self._data[pos+3] - 128 - r = round(1.164*y + 1.793*cr) - g = round(1.164*y - 0.213*cb - 0.533*cr) - b = round(1.164*y + 2.112*cb) + if a := self._data[pos + 4]: # We ignore transparent colours + y = self._data[pos + 1] - 16 + cr = self._data[pos + 2] - 128 + cb = self._data[pos + 3] - 128 + r = round(1.164 * y + 1.793 * cr) + g = round(1.164 * y - 0.213 * cb - 0.533 * cr) + b = round(1.164 * y + 2.112 * cb) palette[self._data[pos]] = qRgba(r, g, b, a).to_bytes(4, "little") return palette -class ObjectSegment(BaseSegment): - """Object Definition Segment +class ObjectSegment(SegmentBase): + """Object Definition Segment. This segment defines the graphics object. These are images with rendered text on a transparent background. """ def validate(self) -> None: - """The header is 11 bytes, followed by the raw image data.""" + """Validate the segment data.""" + # The header is 11 bytes, followed by the raw image data. self._valid = len(self._data) >= 11 - return @property def id(self) -> int: @@ -517,7 +526,9 @@ def version(self) -> int: @property def sequence(self) -> int: - """If the image is split into a series of consecutive fragments, + """Return the sequence flag of this object. + + If the image is split into a series of consecutive fragments, the last fragment has this flag set. Possible values: 0x40: Last in sequence 0x80: First in sequence @@ -544,7 +555,7 @@ def size(self) -> QSize: @property def rle(self) -> bytes: - """This is the image data compressed using Run-length Encoding (RLE). + """Return the image data compressed using Run-length Encoding (RLE). The size of the data is defined in the Object Data Length field. """ pos = 4 if self.sequence == 0x40 else 11 diff --git a/subtle_gui/formats/srtsubs.py b/subtle_gui/formats/srtsubs.py index 7da84c3..bedcf84 100644 --- a/subtle_gui/formats/srtsubs.py +++ b/subtle_gui/formats/srtsubs.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -36,10 +37,10 @@ class SRTSubs(SubtitlesBase): + """SRT Subtitles.""" def __init__(self) -> None: super().__init__() - return def read(self, path: Path) -> None: """Read SRT info from file.""" @@ -48,7 +49,6 @@ def read(self, path: Path) -> None: self._path = path except Exception as exc: logger.error("Could not read SRT file: %s", self._path, exc_info=exc) - return def write(self, path: Path | None = None, offset: float = 0.0) -> None: """Writer SRT data to file.""" @@ -56,12 +56,12 @@ def write(self, path: Path | None = None, offset: float = 0.0) -> None: if path is None: path = self._path if path: - self._writeData(path, round(offset*1000.0)) + self._writeData(path, round(offset * 1000.0)) except Exception as exc: logger.error("Could not write SRT file: %s", self._path, exc_info=exc) - return def copyFrames(self, other: SubtitlesBase) -> None: + """Copy frames from another subtitle object.""" return super()._copyFrames(SRTFrame, other) ## @@ -81,7 +81,6 @@ def _readData(self, path: Path) -> None: block = [] if block: self._parseFrame(block) - return def _writeData(self, path: Path, offset: int = 0) -> None: """Write SRT text to file.""" @@ -105,7 +104,6 @@ def _writeData(self, path: Path, offset: int = 0) -> None: if skipped: logger.warning("Skipping %d entries with no text", skipped) logger.info("Saved %d subtitle entries to SRT file with %d ms offset.", index, offset) - return def _parseFrame(self, block: list[str]) -> None: """Add a new frame to internal data.""" @@ -119,17 +117,16 @@ def _parseFrame(self, block: list[str]) -> None: closeItalics([textCleanup(t) for t in block[2:]]), ) ) - return class SRTFrame(FrameBase): + """SRT Subtitle Frame.""" def __init__(self, index: int, start: int, end: int, text: list[str]) -> None: super().__init__(index) self._start = start self._end = end self._text = text - return @classmethod def fromFrame(cls, index: int, other: FrameBase) -> FrameBase: diff --git a/subtle_gui/formats/ssasubs.py b/subtle_gui/formats/ssasubs.py index 6aa0d16..d072c59 100644 --- a/subtle_gui/formats/ssasubs.py +++ b/subtle_gui/formats/ssasubs.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -43,6 +44,7 @@ class EventFormat(NamedTuple): + """Format of an SSA event.""" length: int start: int @@ -51,12 +53,12 @@ class EventFormat(NamedTuple): class SSASubs(SubtitlesBase): + """SubStation Alpha Subtitles.""" def __init__(self) -> None: super().__init__() self._format: EventFormat | None = None self._line = -1 - return def read(self, path: Path) -> None: """Read a PGS file.""" @@ -65,13 +67,13 @@ def read(self, path: Path) -> None: self._path = path except Exception as e: logger.error("Failed to read file data", exc_info=e) - return def write(self, path: Path | None = None) -> None: """Write a PGS file.""" raise NotImplementedError("Cannot write PGS files.") def copyFrames(self, other: SubtitlesBase) -> None: + """Copy frames from another subtitle object.""" return super()._copyFrames(SSAFrame, other) ## @@ -92,21 +94,14 @@ def _readData(self, path: Path) -> None: self._parseDialogue(line[9:]) elif line.startswith("Format:"): self._parseFormat(line[7:]) - return def _parseFormat(self, line: str) -> None: """Parse dialogue format.""" parts = [f.strip() for f in line.split(",")] try: - self._format = EventFormat( - len(parts), - parts.index("Start"), - parts.index("End"), - parts.index("Text") - ) + self._format = EventFormat(len(parts), parts.index("Start"), parts.index("End"), parts.index("Text")) except ValueError: logger.error("Invalid events format string") - return def _parseDialogue(self, line: str) -> None: """Parse a dialogue entry.""" @@ -116,12 +111,14 @@ def _parseDialogue(self, line: str) -> None: fmt = self._format bits = line.split(",", fmt.length - 1) if len(bits) == fmt.length: - self._frames.append(SSAFrame( - len(self._frames), - decodeTS(bits[fmt.start], fmt="SSA"), - decodeTS(bits[fmt.end], fmt="SSA"), - self._processText(bits[fmt.text]), - )) + self._frames.append( + SSAFrame( + len(self._frames), + decodeTS(bits[fmt.start], fmt="SSA"), + decodeTS(bits[fmt.end], fmt="SSA"), + self._processText(bits[fmt.text]), + ) + ) else: logger.error("Dialogue entry is malformed on line %d", self._line) return @@ -143,13 +140,13 @@ def _processText(self, text: str) -> list[str]: class SSAFrame(FrameBase): + """SubStation Alpha Subtitle Frame.""" def __init__(self, index: int, start: int, end: int, text: list[str]) -> None: super().__init__(index=index) self._start = start self._end = end self._text = text - return @classmethod def fromFrame(cls, index: int, other: FrameBase) -> FrameBase: diff --git a/subtle_gui/gui/filetree.py b/subtle_gui/gui/filetree.py index 508ca75..bbaaab2 100644 --- a/subtle_gui/gui/filetree.py +++ b/subtle_gui/gui/filetree.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -34,6 +35,7 @@ class GuiFileTree(QTreeView): + """GUI File Tree.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent) @@ -54,18 +56,13 @@ def __init__(self, parent: QWidget) -> None: self.doubleClicked.connect(self._itemDoubleClicked) - return - ## # Methods ## def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("fileTreeColumns", [ - self.columnWidth(i) for i in range(self._model.columnCount()) - ]) - return + CONFIG.setSizes("fileTreeColumns", [self.columnWidth(i) for i in range(self._model.columnCount())]) ## # Private Slots @@ -78,11 +75,9 @@ def _itemDoubleClicked(self, index: QModelIndex) -> None: if path != self._current: SHARED.media.loadMediaFile(path) self._current = path - return @pyqtSlot(str) def _directoryLoaded(self, path: str) -> None: """Process model finished loading directory.""" if path == "/": self.expand(self._model.index(path)) - return diff --git a/subtle_gui/gui/highlighter.py b/subtle_gui/gui/highlighter.py index 41bc04b..abb214a 100644 --- a/subtle_gui/gui/highlighter.py +++ b/subtle_gui/gui/highlighter.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -25,10 +26,7 @@ from subtle_gui import SHARED -from PyQt6.QtGui import ( - QColor, QSyntaxHighlighter, QTextBlockUserData, QTextCharFormat, - QTextDocument -) +from PyQt6.QtGui import QColor, QSyntaxHighlighter, QTextBlockUserData, QTextCharFormat, QTextDocument from PyQt6.QtWidgets import QApplication logger = logging.getLogger(__name__) @@ -40,6 +38,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): + """GUI Document Syntax Highlighter.""" def __init__(self, document: QTextDocument) -> None: super().__init__(document) @@ -51,8 +50,6 @@ def __init__(self, document: QTextDocument) -> None: self._syntaxCol = QTextCharFormat() self._syntaxCol.setForeground(QApplication.palette().highlight().color()) - return - def highlightBlock(self, text: str) -> None: """Highlight a single block.""" data = self.currentBlockUserData() @@ -66,17 +63,15 @@ def highlightBlock(self, text: str) -> None: cFmt.merge(self._spellErr) self.setFormat(x, 1, cFmt) - return - class TextBlockData(QTextBlockUserData): + """Text Block User Data.""" __slots__ = ("_spellErrors",) def __init__(self) -> None: super().__init__() self._spellErrors: list[tuple[int, int]] = [] - return @property def spellErrors(self) -> list[tuple[int, int]]: @@ -93,12 +88,9 @@ def spellCheck(self, text: str) -> list[tuple[int, int]]: for rX in IGNORE_PATTERNS: for match in rX.finditer(text): if (s := match.start(0)) >= 0 and (e := match.end(0)) >= 0: - text = text[:s] + " "*(e - s) + text[e:] + text = text[:s] + " " * (e - s) + text[e:] for match in re.finditer(SPELL_RX, text.replace("_", " ")): - if ( - (word := match.group(0)) - and not (word.isnumeric() or word.isupper() or checker.checkWord(word)) - ): + if (word := match.group(0)) and not (word.isnumeric() or word.isupper() or checker.checkWord(word)): self._spellErrors.append((match.start(0), match.end(0))) return self._spellErrors diff --git a/subtle_gui/gui/imageviewer.py b/subtle_gui/gui/imageviewer.py index 2a55218..cad9b84 100644 --- a/subtle_gui/gui/imageviewer.py +++ b/subtle_gui/gui/imageviewer.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -32,6 +33,7 @@ class GuiImageViewer(QWidget): + """GUI Image Viewer.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent) @@ -49,8 +51,6 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Public Slots ## @@ -66,7 +66,6 @@ def processFrameUpdate(self, frame: FrameBase) -> None: scene.setSceneRect(QRectF(self._imageSize)) self.imageView.setScene(scene) self._updateSizes() - return ## # Event @@ -76,7 +75,6 @@ def resizeEvent(self, event: QResizeEvent) -> None: """Capture resize to update image scaling.""" super().resizeEvent(event) self._updateSizes() - return ## # Internal Functions @@ -86,4 +84,3 @@ def _updateSizes(self) -> None: """Scale down the image if it does not fit.""" if not self.imageView.rect().contains(self._imageSize): self.imageView.fitInView(QRectF(self._imageSize), Qt.AspectRatioMode.KeepAspectRatio) - return diff --git a/subtle_gui/gui/mediaview.py b/subtle_gui/gui/mediaview.py index 9d0eed7..3d8b322 100644 --- a/subtle_gui/gui/mediaview.py +++ b/subtle_gui/gui/mediaview.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -33,8 +34,14 @@ from PyQt6.QtCore import QModelIndex, pyqtSlot from PyQt6.QtWidgets import ( - QHBoxLayout, QLabel, QProgressBar, QPushButton, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget + QHBoxLayout, + QLabel, + QProgressBar, + QPushButton, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, ) if TYPE_CHECKING: @@ -46,17 +53,18 @@ class GuiMediaView(QWidget): - - C_TRACK = 0 - C_TYPE = 1 - C_CODEC = 2 - C_LANG = 3 - C_LENGTH = 4 - C_FRAMES = 5 - C_LABEL = 6 + """GUI Media View.""" + + C_TRACK = 0 + C_TYPE = 1 + C_CODEC = 2 + C_LANG = 3 + C_LENGTH = 4 + C_FRAMES = 5 + C_LABEL = 6 C_ENABLED = 7 C_DEFAULT = 8 - C_FORCED = 9 + C_FORCED = 9 def __init__(self, parent: QWidget) -> None: super().__init__(parent) @@ -71,11 +79,20 @@ def __init__(self, parent: QWidget) -> None: self.tracksView = QTreeWidget(self) self.tracksView.setIndentation(0) - self.tracksView.setHeaderLabels([ - "#", self.tr("Type"), self.tr("Codec"), self.tr("Lang"), self.tr("Length"), - self.tr("Frames"), self.tr("Label"), self.tr("Enabled"), self.tr("Default"), - self.tr("Forced"), - ]) + self.tracksView.setHeaderLabels( + [ + "#", + self.tr("Type"), + self.tr("Codec"), + self.tr("Lang"), + self.tr("Length"), + self.tr("Frames"), + self.tr("Label"), + self.tr("Enabled"), + self.tr("Default"), + self.tr("Forced"), + ] + ) self.tracksView.doubleClicked.connect(self._itemDoubleClicked) columns = self.tracksView.columnCount() @@ -106,18 +123,15 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Methods ## def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("mediaViewColumns", [ - self.tracksView.columnWidth(i) for i in range(self.tracksView.columnCount()) - ]) - return + CONFIG.setSizes( + "mediaViewColumns", [self.tracksView.columnWidth(i) for i in range(self.tracksView.columnCount())] + ) ## # Public Slots @@ -137,7 +151,6 @@ def processNewMediaLoaded(self) -> None: self._setTrackInfo(track, item) self.tracksView.addTopLevelItem(item) self._map[track.trackID] = item - return ## # Private Slots @@ -155,7 +168,6 @@ def _extractTracks(self) -> None: tracks.append((idx, path)) track.setTrackFile(path) self._runTrackExtraction(file.filePath, tracks) - return @pyqtSlot() def _cancelExtract(self) -> None: @@ -165,7 +177,6 @@ def _cancelExtract(self) -> None: self.progressBar.setValue(0) self._extracted.clear() self._extractWorker = None - return @pyqtSlot(QModelIndex) def _itemDoubleClicked(self, index: QModelIndex) -> None: @@ -180,13 +191,11 @@ def _itemDoubleClicked(self, index: QModelIndex) -> None: self._emitTrack = idx if idx in self._extracted and not self._emitTrack: SHARED.media.setCurrentTrack(idx) - return @pyqtSlot(int) def _extractProgress(self, value: int) -> None: """Process track extraction progress count.""" self.progressBar.setValue(value) - return @pyqtSlot() def _extractFinished(self) -> None: @@ -208,8 +217,6 @@ def _extractFinished(self) -> None: SHARED.media.setCurrentTrack(self._emitTrack) self._emitTrack = None - return - ## # Internal Functions ## @@ -229,7 +236,6 @@ def _setTrackInfo(self, track: MediaTrack, item: QTreeWidgetItem | None = None) item.setText(self.C_ENABLED, self._trYes if track.enabled else "") item.setText(self.C_DEFAULT, self._trYes if track.default else "") item.setText(self.C_FORCED, self._trYes if track.forced else "") - return def _runTrackExtraction(self, path: Path, tracks: list[tuple[str, Path]]) -> None: """Call for extraction for a set of tracks.""" diff --git a/subtle_gui/gui/subsview.py b/subtle_gui/gui/subsview.py index be0f365..c749f09 100644 --- a/subtle_gui/gui/subsview.py +++ b/subtle_gui/gui/subsview.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -37,12 +38,13 @@ class GuiSubtitleView(QWidget): + """GUI Subtitle View.""" - C_DATA = 0 - C_ID = 0 - C_TIME = 1 + C_DATA = 0 + C_ID = 0 + C_TIME = 1 C_LENGTH = 2 - C_TEXT = 3 + C_TEXT = 3 D_INDEX = Qt.ItemDataRole.UserRole @@ -56,9 +58,7 @@ def __init__(self, parent: QWidget) -> None: # Entries View self.subEntries = QTreeWidget(self) self.subEntries.setIndentation(0) - self.subEntries.setHeaderLabels([ - "#", self.tr("Time Stamp"), self.tr("Length"), self.tr("Text") - ]) + self.subEntries.setHeaderLabels(["#", self.tr("Time Stamp"), self.tr("Length"), self.tr("Text")]) self.subEntries.clicked.connect(self._itemClicked) columns = self.subEntries.columnCount() @@ -72,18 +72,15 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Methods ## def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("subsViewColumns", [ - self.subEntries.columnWidth(i) for i in range(self.subEntries.columnCount() - 1) - ]) - return + CONFIG.setSizes( + "subsViewColumns", [self.subEntries.columnWidth(i) for i in range(self.subEntries.columnCount() - 1)] + ) ## # Public Slots @@ -94,7 +91,6 @@ def processNewMediaLoaded(self) -> None: """Clear previous content.""" self._map = {} self.subEntries.clear() - return @pyqtSlot() def processNewTrackLoaded(self) -> None: @@ -107,7 +103,7 @@ def processNewTrackLoaded(self) -> None: item = QTreeWidgetItem() item.setText(self.C_ID, str(self.subEntries.topLevelItemCount())) item.setText(self.C_TIME, formatTS(frame.start)) - item.setText(self.C_LENGTH, f"{frame.length/1000.0:.3f} s") + item.setText(self.C_LENGTH, f"{frame.length / 1000.0:.3f} s") item.setData(self.C_DATA, self.D_INDEX, frame.index) item.setFont(self.C_ID, font) item.setFont(self.C_TIME, font) @@ -115,14 +111,12 @@ def processNewTrackLoaded(self) -> None: self._map[frame.index] = item self._updateItemText(item, frame.text) self.subEntries.addTopLevelItem(item) - return @pyqtSlot(FrameBase) def updateText(self, frame: FrameBase) -> None: """Update text for a specific frame.""" if item := self._map.get(frame.index): self._updateItemText(item, frame.text) - return @pyqtSlot(Path, float) def writeSrtFile(self, path: Path, offset: float = 0.0) -> None: @@ -131,7 +125,6 @@ def writeSrtFile(self, path: Path, offset: float = 0.0) -> None: writer = SRTSubs() SHARED.media.currentTrack.copyFrames(writer) writer.write(path, offset) - return @pyqtSlot(Path) def readSubsFile(self, path: Path) -> None: @@ -143,7 +136,6 @@ def readSubsFile(self, path: Path) -> None: for frame in track.iterFrames(): if item := self._map.get(frame.index): self._updateItemText(item, frame.text) - return @pyqtSlot(int) def selectNearby(self, step: int) -> None: @@ -153,12 +145,10 @@ def selectNearby(self, step: int) -> None: if item := self.subEntries.topLevelItem(index): self.subEntries.clearSelection() self.subEntries.scrollTo( - self.subEntries.indexFromItem(item, 0), - QAbstractItemView.ScrollHint.PositionAtCenter + self.subEntries.indexFromItem(item, 0), QAbstractItemView.ScrollHint.PositionAtCenter ) item.setSelected(True) self._itemClicked(self.subEntries.indexFromItem(item)) - return ## # Private Slots @@ -176,7 +166,6 @@ def _itemClicked(self, index: QModelIndex) -> None: frame.setText(text) self.updateText(frame) self.subsFrameUpdated.emit(frame) - return ## # Internal Functions @@ -185,4 +174,3 @@ def _itemClicked(self, index: QModelIndex) -> None: def _updateItemText(self, item: QTreeWidgetItem, text: list[str]) -> None: """Update the subtitle text for a given item.""" item.setText(self.C_TEXT, "\u21b2".join(text)) - return diff --git a/subtle_gui/gui/texteditor.py b/subtle_gui/gui/texteditor.py index a91330f..78249c6 100644 --- a/subtle_gui/gui/texteditor.py +++ b/subtle_gui/gui/texteditor.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -28,14 +29,13 @@ from PyQt6.QtCore import QPoint, Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import QShortcut, QTextBlock, QTextBlockFormat, QTextCharFormat, QTextCursor -from PyQt6.QtWidgets import ( - QComboBox, QMenu, QTextEdit, QToolBar, QToolButton, QVBoxLayout, QWidget -) +from PyQt6.QtWidgets import QComboBox, QMenu, QTextEdit, QToolBar, QToolButton, QVBoxLayout, QWidget logger = logging.getLogger(__name__) class GuiTextEditor(QWidget): + """GUI Text Editor.""" newTextForFrame = pyqtSignal(FrameBase) requestNewFrame = pyqtSignal(int) @@ -112,8 +112,6 @@ def __init__(self, parent: QWidget) -> None: self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyContext.activated.connect(self._openContextFromCursor) - return - ## # Public Slots ## @@ -141,8 +139,6 @@ def setEditorText(self, frame: FrameBase) -> None: self._block = False - return - @pyqtSlot(str) def updateSpellLanguage(self, language: str) -> None: """Update the spell check language box.""" @@ -152,7 +148,6 @@ def updateSpellLanguage(self, language: str) -> None: else: self.spellLang.setCurrentIndex(0) self.spellLang.blockSignals(False) - return ## # Private Slots @@ -164,26 +159,22 @@ def _textChanged(self) -> None: if self._frame and not self._block: self._frame.setText(self._getText()) self.newTextForFrame.emit(self._frame) - return @pyqtSlot(int) def _spellLangChanged(self, index: int) -> None: """Process change in spell check language.""" SHARED.spelling.setLanguage(self.spellLang.currentData()) self.highlight.rehighlight() - return @pyqtSlot() def _requestPrevious(self) -> None: """Process Page Up key press.""" self.requestNewFrame.emit(-1) - return @pyqtSlot() def _requestNext(self) -> None: """Process Page Down key press.""" self.requestNewFrame.emit(1) - return @pyqtSlot() def _formatItalic(self) -> None: @@ -214,20 +205,16 @@ def _formatItalic(self) -> None: self.textEdit.setTextCursor(cursor) - return - @pyqtSlot() def _insertNoteSymbol(self) -> None: """Process Ctrl+J key press.""" cursor = self.textEdit.textCursor() cursor.insertText("\u266a") - return @pyqtSlot() def _openContextFromCursor(self) -> None: """Open the spell check context menu at the cursor.""" self._openContextMenu(self.textEdit.cursorRect().center()) - return @pyqtSlot("QPoint") def _openContextMenu(self, pos: QPoint) -> None: @@ -244,17 +231,13 @@ def _openContextMenu(self, pos: QPoint) -> None: block = pCursor.block() sCursor = self.textEdit.textCursor() sCursor.setPosition(block.position() + cPos) - sCursor.movePosition( - QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, cLen - ) + sCursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, cLen) if suggest: ctxMenu.addSeparator() ctxMenu.addAction(self.tr("Spelling Suggestion(s)")) for option in suggest[:15]: if action := ctxMenu.addAction(option): - action.triggered.connect( - lambda _, option=option: self._correctWord(sCursor, option) - ) + action.triggered.connect(lambda _, option=option: self._correctWord(sCursor, option)) else: ctxMenu.addAction(self.tr("No Suggestions")) @@ -269,8 +252,6 @@ def _openContextMenu(self, pos: QPoint) -> None: ctxMenu.exec(viewport.mapToGlobal(pos)) ctxMenu.deleteLater() - return - ## # Internal Functions ## @@ -307,7 +288,6 @@ def _correctWord(self, cursor: QTextCursor, word: str) -> None: cursor.endEditBlock() cursor.setPosition(pos) self.textEdit.setTextCursor(cursor) - return def _addWord(self, word: str, block: QTextBlock, save: bool) -> None: """Slot for the spell check context menu triggered when the user @@ -317,7 +297,6 @@ def _addWord(self, word: str, block: QTextBlock, save: bool) -> None: logger.debug("Added '%s' to session dictionary, saved = %s", word, str(save)) SHARED.spelling.addWord(word, save=save) self.highlight.rehighlightBlock(block) - return def _spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]: """Check if there is a misspelled word at a given position in diff --git a/subtle_gui/gui/toolspanel.py b/subtle_gui/gui/toolspanel.py index af0d63d..5462b1d 100644 --- a/subtle_gui/gui/toolspanel.py +++ b/subtle_gui/gui/toolspanel.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -29,14 +30,24 @@ from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import ( - QCheckBox, QDoubleSpinBox, QFormLayout, QGroupBox, QHBoxLayout, QLineEdit, - QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, QWidget + QCheckBox, + QDoubleSpinBox, + QFormLayout, + QGroupBox, + QHBoxLayout, + QLineEdit, + QListWidget, + QListWidgetItem, + QPushButton, + QVBoxLayout, + QWidget, ) logger = logging.getLogger(__name__) class GuiToolsPanel(QWidget): + """GUI Tools Panel.""" D_SUBS_PATH = Qt.ItemDataRole.UserRole @@ -147,8 +158,6 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Public Slots ## @@ -162,7 +171,6 @@ def processNewMediaLoaded(self) -> None: self.mediaFile.setText(path.name) self._scanForSubs(path) self._updateTrackInfo() - return @pyqtSlot() def processNewTrackLoaded(self) -> None: @@ -171,7 +179,6 @@ def processNewTrackLoaded(self) -> None: self.srtForced.setChecked(track.forced) self.srtSDH.setChecked(track.sdh) self._updateTrackInfo() - return ## # Private Slots @@ -187,7 +194,6 @@ def _clickedSaveSrt(self) -> None: self.requestSrtSave.emit(folder / name, self.srtOffset.value()) except Exception as exc: logger.error("Failed to prepare subtitles folder", exc_info=exc) - return @pyqtSlot() def _clickedLoadSubs(self) -> None: @@ -195,7 +201,6 @@ def _clickedLoadSubs(self) -> None: if items := self.subsList.selectedItems(): if (path := Path(items[0].data(self.D_SUBS_PATH))).is_file(): self.requestSubsLoad.emit(path) - return @pyqtSlot() def _updateTrackInfo(self) -> None: @@ -219,8 +224,6 @@ def _updateTrackInfo(self) -> None: self.srtSaveDir.setText(str(folder)) self.srtFileName.setText(".".join(bits)) - return - ## # Internal Functions ## @@ -242,11 +245,7 @@ def _scanForSubs(self, path: Path) -> None: for folder in folders: try: for entry in folder.iterdir(): - if ( - entry.is_file() - and entry.suffix == ".srt" - and entry.stem.lower().startswith(prefix) - ): + if entry.is_file() and entry.suffix == ".srt" and entry.stem.lower().startswith(prefix): item = QListWidgetItem() item.setText(str(entry.relative_to(root))) item.setData(self.D_SUBS_PATH, entry) @@ -256,5 +255,3 @@ def _scanForSubs(self, path: Path) -> None: except Exception as exc: logger.error("Could not scan path: %s", root, exc_info=exc) - - return diff --git a/subtle_gui/guimain.py b/subtle_gui/guimain.py index cef62b1..3493e1c 100644 --- a/subtle_gui/guimain.py +++ b/subtle_gui/guimain.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -48,6 +49,7 @@ class GuiMain(QMainWindow): + """Main GUI Window.""" def __init__(self) -> None: super().__init__() @@ -129,8 +131,6 @@ def __init__(self) -> None: self.setCentralWidget(self.splitMain) self.resize(CONFIG.getSize("mainWindow")) - return - ## # Events ## @@ -150,4 +150,3 @@ def closeEvent(self, event: QCloseEvent) -> None: CONFIG.save() CONFIG.cleanup() event.accept() - return diff --git a/subtle_gui/ocr/base.py b/subtle_gui/ocr/base.py index 79375f7..f9e6562 100644 --- a/subtle_gui/ocr/base.py +++ b/subtle_gui/ocr/base.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -32,10 +33,12 @@ class OCRBase(ABC): + """Base class for OCR implementations.""" def __init__(self) -> None: return @abstractmethod def processImage(self, index: int, image: QImage, lang: list[str]) -> list[str]: + """Process an image and return the recognized text.""" raise NotImplementedError diff --git a/subtle_gui/ocr/tesseract.py b/subtle_gui/ocr/tesseract.py index 6ab2e5e..17bb20f 100644 --- a/subtle_gui/ocr/tesseract.py +++ b/subtle_gui/ocr/tesseract.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -53,14 +54,11 @@ (re.compile(r"^(-\s)[\w]", re.UNICODE), "-"), (re.compile(r"^(\.{2})[\s\w]", re.UNICODE), "..."), (re.compile(r"^(\.{3}\s)\w", re.UNICODE), "..."), - # Wrong capitalisation in the middle of words (re.compile(r"[a-z]+(S)", re.UNICODE), "s"), (re.compile(r"[a-z]+(O)", re.UNICODE), "o"), - # Slash for I in italics (re.compile(r"(?:^|\s)(\/)\s", re.UNICODE), "I"), - # Notes (re.compile(r"(?:^|\s|\[|\()(J|f)(?:$|\s|\]|\))", re.UNICODE), "\u266a"), ], @@ -68,26 +66,24 @@ # Misinterpreted words (re.compile(r"\b(tt)\b", re.UNICODE), "it"), (re.compile(r"\b(fo)\b", re.UNICODE), "to"), - # Wrong capitalisation at the start of words (re.compile(r"(? None: super().__init__() - return def processImage(self, index: int, image: QImage, lang: list[str]) -> list[str]: """Perform OCR on a QImage.""" diff --git a/subtle_gui/shared.py b/subtle_gui/shared.py index ea42f7b..49ef281 100644 --- a/subtle_gui/shared.py +++ b/subtle_gui/shared.py @@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations import logging @@ -36,6 +37,7 @@ class SharedData(QObject): + """Shared Data Object.""" spellLanguageChanged = pyqtSignal(str) @@ -45,7 +47,6 @@ def __init__(self) -> None: self._ocr: OCRBase | None = None self._spell: SpellEnchant | None = None self._icons: GuiIcons | None = None - return @property def media(self) -> MediaData: @@ -75,13 +76,7 @@ def icons(self) -> GuiIcons: return self._icons raise RuntimeError("Shared data object not yet initialised.") - def initSharedData( - self, - media: MediaData, - ocr: OCRBase, - spell: SpellEnchant, - icons: GuiIcons - ) -> None: + def initSharedData(self, media: MediaData, ocr: OCRBase, spell: SpellEnchant, icons: GuiIcons) -> None: """Init the shared data object. This must be called right after the GUI is created. """ @@ -89,7 +84,6 @@ def initSharedData( self._ocr = ocr self._spell = spell self._icons = icons - return def setSpellLanguage(self, track: MediaTrack | None) -> None: """Set the current spell checking language.""" @@ -100,4 +94,3 @@ def setSpellLanguage(self, track: MediaTrack | None) -> None: else: self.spelling.setLanguage(None) self.spellLanguageChanged.emit("None") - return diff --git a/tests/test_common.py b/tests/test_common.py index c3122e6..e21e6e8 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -17,21 +17,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa + from __future__ import annotations -from subtle.common import closeItalics +from subtle_gui.common import closeItalics def testCommon_closeItalics(): """Test the closeItalics function.""" - assert closeItalics(["Foo", "Bar"]) == [ - "Foo", "Bar" - ] + assert closeItalics(["Foo", "Bar"]) == ["Foo", "Bar"] assert closeItalics(["Foo", "Bar", "Baz"]) == [ - "Foo", "Bar", "Baz", + "Foo", + "Bar", + "Baz", ] assert closeItalics(["Foo foo foo", "Bar"]) == [ - "Foo foo foo", "Bar", + "Foo foo foo", + "Bar", ] - return diff --git a/uv.lock b/uv.lock index 2e1a9b7..1194cd3 100644 --- a/uv.lock +++ b/uv.lock @@ -118,15 +118,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "isort" -version = "8.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, -] - [[package]] name = "nodeenv" version = "1.10.0" @@ -346,7 +337,6 @@ build = [ dev = [ { name = "build" }, { name = "coverage" }, - { name = "isort" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-qt" }, @@ -355,7 +345,6 @@ dev = [ { name = "setuptools" }, ] lint = [ - { name = "isort" }, { name = "pyright" }, { name = "ruff" }, ] @@ -380,7 +369,6 @@ build = [ dev = [ { name = "build" }, { name = "coverage", specifier = ">=7.2.0" }, - { name = "isort" }, { name = "pyright" }, { name = "pytest", specifier = ">=6.0.0" }, { name = "pytest-qt" }, @@ -389,7 +377,6 @@ dev = [ { name = "setuptools", specifier = ">=77.0.3" }, ] lint = [ - { name = "isort" }, { name = "pyright" }, { name = "ruff" }, ]