From 248f856af3fab26912e5784131c5a866fe719499 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:26:16 +0200 Subject: [PATCH 1/4] Update pyproject and lock file --- pyproject.toml | 18 ++---------------- uv.lock | 13 ------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51c9fc3..34b9343 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,17 +51,8 @@ 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 @@ -88,11 +79,6 @@ select = [ ] 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 "PLC0415", # import-outside-top-level "PLC1901", # compare-to-empty-string "PLW0108", # unnecessary-lambda 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" }, ] From e901ae0b90ca0a990463495176cdfda14defa6c8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:29:55 +0200 Subject: [PATCH 2/4] Apply formatting --- subtle.py | 2 + subtle_gui/__init__.py | 37 +++++++++-------- subtle_gui/common.py | 24 ++++------- subtle_gui/config.py | 38 +++++++---------- subtle_gui/constants.py | 7 ++-- subtle_gui/core/icons.py | 40 +++++++----------- subtle_gui/core/media.py | 3 +- subtle_gui/core/mediafile.py | 20 ++++----- subtle_gui/core/mkvextract.py | 2 +- subtle_gui/core/spellcheck.py | 3 +- subtle_gui/formats/base.py | 14 +++---- subtle_gui/formats/pgssubs.py | 77 +++++++++++++++++------------------ subtle_gui/formats/srtsubs.py | 5 +-- subtle_gui/formats/ssasubs.py | 25 +++++------- subtle_gui/gui/filetree.py | 6 +-- subtle_gui/gui/highlighter.py | 15 ++----- subtle_gui/gui/imageviewer.py | 2 +- subtle_gui/gui/mediaview.py | 53 +++++++++++++++--------- subtle_gui/gui/subsview.py | 25 +++++------- subtle_gui/gui/texteditor.py | 14 ++----- subtle_gui/gui/toolspanel.py | 21 ++++++---- subtle_gui/guimain.py | 2 +- subtle_gui/ocr/base.py | 2 +- subtle_gui/ocr/tesseract.py | 9 +--- subtle_gui/shared.py | 10 +---- tests/test_common.py | 14 ++++--- 26 files changed, 215 insertions(+), 255 deletions(-) diff --git a/subtle.py b/subtle.py index 5f15143..e551d50 100755 --- a/subtle.py +++ b/subtle.py @@ -3,6 +3,7 @@ Subtle – Start Script ===================== """ + 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..0f5842f 100644 --- a/subtle_gui/__init__.py +++ b/subtle_gui/__init__.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import getopt @@ -38,14 +39,14 @@ # Package Meta # ============ -__package__ = "subtle_gui" -__copyright__ = "Copyright (C) Veronica Berglyd Olsen" -__license__ = "GPLv3" -__author__ = "Veronica Berglyd Olsen" +__package__ = "subtle_gui" +__copyright__ = "Copyright (C) Veronica Berglyd Olsen" +__license__ = "GPLv3" +__author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" -__email__ = "code@vkbo.net" -__version__ = "26.1.1" -__date__ = "2026-06-24" +__email__ = "code@vkbo.net" +__version__ = "26.1.1" +__date__ = "2026-06-24" logger = logging.getLogger(__name__) @@ -58,12 +59,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 +76,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 +114,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 +142,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..62b4ee8 100644 --- a/subtle_gui/common.py +++ b/subtle_gui/common.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import json @@ -77,8 +78,7 @@ def regexCleanup(text: str, patterns: list[tuple[re.Pattern, str]]) -> str: """Replaces 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,7 +120,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: elif first in ("{", "["): n += 1 - indent = "\n"+" "*n + indent = "\n" + " " * n if n > nmax and nmax > 0: buffer.append(chunk) else: @@ -128,7 +128,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: elif first in ("}", "]"): n -= 1 - indent = "\n"+" "*n + indent = "\n" + " " * n if n >= nmax and nmax > 0: buffer.append(chunk) else: @@ -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..bf3c06f 100644 --- a/subtle_gui/config.py +++ b/subtle_gui/config.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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,23 @@ "guiFont": "", "fixedFont": "", "subsFont": "", - } + }, } T_Fonts = Literal["gui"] | Literal["fixed"] | Literal["subs"] class Config: - 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 +88,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,7 +112,7 @@ def __init__(self) -> None: self.osUnknown = True # Other System Info - self.hostName = QSysInfo.machineHostName() + self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() return @@ -172,7 +165,6 @@ def assetPath(self, resource: str, kind: str | None = None) -> Path: path /= kind return path / resource - ## # Setters ## @@ -193,7 +185,7 @@ def setSizes(self, key: str, value: list[int]) -> None: return def setFontSpec(self, target: T_Fonts, font: QFont | str) -> None: - """Set the font """ + """Set the font""" if isinstance(font, str): temp = QFont() temp.fromString(font) @@ -249,7 +241,7 @@ 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 diff --git a/subtle_gui/constants.py b/subtle_gui/constants.py index 5baa2e3..b4f85ae 100644 --- a/subtle_gui/constants.py +++ b/subtle_gui/constants.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations from enum import Enum @@ -32,18 +33,16 @@ def trConst(text: str) -> str: class MediaType(Enum): - VIDEO = 0 AUDIO = 1 - SUBS = 2 + SUBS = 2 OTHER = 4 class GuiLabels: - 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..e548a65 100644 --- a/subtle_gui/core/icons.py +++ b/subtle_gui/core/icons.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -32,29 +33,26 @@ RAW_SVG = { "up": ( b'' ), "down": ( b'' ), - "italic": ( - b'' - ), + "italic": (b''), "note": ( b'' ), @@ -67,7 +65,6 @@ class GuiIcons: - def __init__(self) -> None: palette = QApplication.palette() self._cache: dict[str, QIcon] = {} @@ -78,17 +75,12 @@ 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 @@ -107,9 +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)) diff --git a/subtle_gui/core/media.py b/subtle_gui/core/media.py index 89a3ac6..e0e9625 100644 --- a/subtle_gui/core/media.py +++ b/subtle_gui/core/media.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -44,7 +45,6 @@ class MediaData(QObject): - mediaDataCleared = pyqtSignal() newMediaLoaded = pyqtSignal() newTrackSelected = pyqtSignal() @@ -110,7 +110,6 @@ def getTrack(self, trackID: str) -> MediaTrack | None: class MediaTrack: - def __init__(self, media: MediaData, info: dict) -> None: self._media = media self._info = {} diff --git a/subtle_gui/core/mediafile.py b/subtle_gui/core/mediafile.py index f11ed7b..27775ad 100644 --- a/subtle_gui/core/mediafile.py +++ b/subtle_gui/core/mediafile.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import json @@ -38,17 +39,16 @@ class ContainerType(IntEnum): - - 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 = ( diff --git a/subtle_gui/core/mkvextract.py b/subtle_gui/core/mkvextract.py index 6b05a3f..48c0b61 100644 --- a/subtle_gui/core/mkvextract.py +++ b/subtle_gui/core/mkvextract.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -35,7 +36,6 @@ class MkvExtract(QObject): - processProgress = pyqtSignal(int) processDone = pyqtSignal(int) diff --git a/subtle_gui/core/spellcheck.py b/subtle_gui/core/spellcheck.py index 6375162..05de462 100644 --- a/subtle_gui/core/spellcheck.py +++ b/subtle_gui/core/spellcheck.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import json @@ -129,6 +130,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: @@ -171,7 +173,6 @@ def add_to_session(self, word: str) -> None: class UserDictionary: - def __init__(self) -> None: self._words = set() return diff --git a/subtle_gui/formats/base.py b/subtle_gui/formats/base.py index 9a1e1b2..d57b1d1 100644 --- a/subtle_gui/formats/base.py +++ b/subtle_gui/formats/base.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -37,7 +38,6 @@ class SubtitlesBase(ABC): - __slots__ = ("_frames", "_path") def __init__(self) -> None: @@ -65,7 +65,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,7 +73,10 @@ 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 @@ -114,14 +117,11 @@ def _copyFrames(self, frameType: type[FrameBase], other: SubtitlesBase) -> None: subclasses by passing its own FrameBase implementation as frameType. """ - self._frames = [ - frameType.fromFrame(n, frame) for n, frame in enumerate(other.iterFrames()) - ] + self._frames = [frameType.fromFrame(n, frame) for n, frame in enumerate(other.iterFrames())] return class FrameBase(ABC): - __slots__ = ("_end", "_index", "_start", "_text") def __init__(self, index: int) -> None: diff --git a/subtle_gui/formats/pgssubs.py b/subtle_gui/formats/pgssubs.py index 8d20036..85efcbe 100644 --- a/subtle_gui/formats/pgssubs.py +++ b/subtle_gui/formats/pgssubs.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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) @@ -146,7 +147,6 @@ def _readData(self, path: Path) -> None: class PGSFrame(FrameBase): - def __init__(self, index: int, ds: DisplaySet) -> None: super().__init__(index=index) self._ds = ds @@ -174,7 +174,6 @@ def getImage(self) -> QImage: class DisplaySet: - __slots__ = ("_image", "_ods", "_pcs", "_pds", "_wds") def __init__(self, pcs: PresentationSegment) -> None: @@ -281,23 +280,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: @@ -309,7 +306,6 @@ def render(self, crop: bool = True) -> QImage: class BaseSegment(ABC): - __slots__ = ("_data", "_ts", "_valid") def __init__(self, ts: int, data: bytes) -> None: @@ -320,10 +316,7 @@ def __init__(self, ts: int, data: bytes) -> None: return def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}: t={self._ts/90000:.3f} " - f"size={len(self._data)} valid={self._valid}>" - ) + return f"<{self.__class__.__name__}: t={self._ts / 90000:.3f} size={len(self._data)} valid={self._valid}>" @property def valid(self) -> bool: @@ -348,7 +341,7 @@ class PresentationSegment(BaseSegment): def validate(self) -> None: """Length is 11 + n*8""" size = len(self._data) - self._valid = (size >= 11 and size % 8 == 3) + self._valid = size >= 11 and size % 8 == 3 return @property @@ -410,12 +403,12 @@ 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 @@ -432,7 +425,7 @@ class WindowSegment(BaseSegment): def validate(self) -> None: """Length is 1 + n*9""" - self._valid = (len(self._data) % 9 == 1) + self._valid = len(self._data) % 9 == 1 return @property @@ -442,11 +435,14 @@ def count(self) -> int: 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 @@ -456,12 +452,13 @@ class PaletteSegment(BaseSegment): This segment is used to define a palette for color conversion. """ + __slots__ = ("_col",) def validate(self) -> None: """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 @@ -482,13 +479,13 @@ 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 diff --git a/subtle_gui/formats/srtsubs.py b/subtle_gui/formats/srtsubs.py index 7da84c3..4672615 100644 --- a/subtle_gui/formats/srtsubs.py +++ b/subtle_gui/formats/srtsubs.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -36,7 +37,6 @@ class SRTSubs(SubtitlesBase): - def __init__(self) -> None: super().__init__() return @@ -56,7 +56,7 @@ 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 @@ -123,7 +123,6 @@ def _parseFrame(self, block: list[str]) -> None: class SRTFrame(FrameBase): - def __init__(self, index: int, start: int, end: int, text: list[str]) -> None: super().__init__(index) self._start = start diff --git a/subtle_gui/formats/ssasubs.py b/subtle_gui/formats/ssasubs.py index 6aa0d16..73f2cf3 100644 --- a/subtle_gui/formats/ssasubs.py +++ b/subtle_gui/formats/ssasubs.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -43,7 +44,6 @@ class EventFormat(NamedTuple): - length: int start: int end: int @@ -51,7 +51,6 @@ class EventFormat(NamedTuple): class SSASubs(SubtitlesBase): - def __init__(self) -> None: super().__init__() self._format: EventFormat | None = None @@ -98,12 +97,7 @@ 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 @@ -116,12 +110,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,7 +139,6 @@ def _processText(self, text: str) -> list[str]: class SSAFrame(FrameBase): - def __init__(self, index: int, start: int, end: int, text: list[str]) -> None: super().__init__(index=index) self._start = start diff --git a/subtle_gui/gui/filetree.py b/subtle_gui/gui/filetree.py index 508ca75..19f6114 100644 --- a/subtle_gui/gui/filetree.py +++ b/subtle_gui/gui/filetree.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -34,7 +35,6 @@ class GuiFileTree(QTreeView): - def __init__(self, parent: QWidget) -> None: super().__init__(parent) @@ -62,9 +62,7 @@ def __init__(self, parent: QWidget) -> None: def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("fileTreeColumns", [ - self.columnWidth(i) for i in range(self._model.columnCount()) - ]) + CONFIG.setSizes("fileTreeColumns", [self.columnWidth(i) for i in range(self._model.columnCount())]) return ## diff --git a/subtle_gui/gui/highlighter.py b/subtle_gui/gui/highlighter.py index 41bc04b..33ac2c7 100644 --- a/subtle_gui/gui/highlighter.py +++ b/subtle_gui/gui/highlighter.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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,7 +38,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): - def __init__(self, document: QTextDocument) -> None: super().__init__(document) @@ -70,7 +67,6 @@ def highlightBlock(self, text: str) -> None: class TextBlockData(QTextBlockUserData): - __slots__ = ("_spellErrors",) def __init__(self) -> None: @@ -93,12 +89,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..09e3f4a 100644 --- a/subtle_gui/gui/imageviewer.py +++ b/subtle_gui/gui/imageviewer.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -32,7 +33,6 @@ class GuiImageViewer(QWidget): - def __init__(self, parent: QWidget) -> None: super().__init__(parent) diff --git a/subtle_gui/gui/mediaview.py b/subtle_gui/gui/mediaview.py index 9d0eed7..2e78a12 100644 --- a/subtle_gui/gui/mediaview.py +++ b/subtle_gui/gui/mediaview.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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,16 @@ class GuiMediaView(QWidget): - - C_TRACK = 0 - C_TYPE = 1 - C_CODEC = 2 - C_LANG = 3 - C_LENGTH = 4 - C_FRAMES = 5 - C_LABEL = 6 + 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 +77,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() @@ -114,9 +129,9 @@ def __init__(self, parent: QWidget) -> None: def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("mediaViewColumns", [ - self.tracksView.columnWidth(i) for i in range(self.tracksView.columnCount()) - ]) + CONFIG.setSizes( + "mediaViewColumns", [self.tracksView.columnWidth(i) for i in range(self.tracksView.columnCount())] + ) return ## diff --git a/subtle_gui/gui/subsview.py b/subtle_gui/gui/subsview.py index be0f365..6f7ee33 100644 --- a/subtle_gui/gui/subsview.py +++ b/subtle_gui/gui/subsview.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -37,12 +38,11 @@ class GuiSubtitleView(QWidget): - - 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 +56,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() @@ -80,9 +78,9 @@ def __init__(self, parent: QWidget) -> None: def saveSettings(self) -> None: """Save widget settings.""" - CONFIG.setSizes("subsViewColumns", [ - self.subEntries.columnWidth(i) for i in range(self.subEntries.columnCount() - 1) - ]) + CONFIG.setSizes( + "subsViewColumns", [self.subEntries.columnWidth(i) for i in range(self.subEntries.columnCount() - 1)] + ) return ## @@ -107,7 +105,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) @@ -153,8 +151,7 @@ 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)) diff --git a/subtle_gui/gui/texteditor.py b/subtle_gui/gui/texteditor.py index a91330f..3c35953 100644 --- a/subtle_gui/gui/texteditor.py +++ b/subtle_gui/gui/texteditor.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -28,15 +29,12 @@ 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): - newTextForFrame = pyqtSignal(FrameBase) requestNewFrame = pyqtSignal(int) @@ -244,17 +242,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")) diff --git a/subtle_gui/gui/toolspanel.py b/subtle_gui/gui/toolspanel.py index af0d63d..1487343 100644 --- a/subtle_gui/gui/toolspanel.py +++ b/subtle_gui/gui/toolspanel.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -29,15 +30,23 @@ 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): - D_SUBS_PATH = Qt.ItemDataRole.UserRole requestSrtSave = pyqtSignal(Path, float) @@ -242,11 +251,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) diff --git a/subtle_gui/guimain.py b/subtle_gui/guimain.py index cef62b1..bfab160 100644 --- a/subtle_gui/guimain.py +++ b/subtle_gui/guimain.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -48,7 +49,6 @@ class GuiMain(QMainWindow): - def __init__(self) -> None: super().__init__() diff --git a/subtle_gui/ocr/base.py b/subtle_gui/ocr/base.py index 79375f7..a43036f 100644 --- a/subtle_gui/ocr/base.py +++ b/subtle_gui/ocr/base.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -32,7 +33,6 @@ class OCRBase(ABC): - def __init__(self) -> None: return diff --git a/subtle_gui/ocr/tesseract.py b/subtle_gui/ocr/tesseract.py index 6ab2e5e..52021fd 100644 --- a/subtle_gui/ocr/tesseract.py +++ b/subtle_gui/ocr/tesseract.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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,23 +66,20 @@ # 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 diff --git a/subtle_gui/shared.py b/subtle_gui/shared.py index ea42f7b..760e480 100644 --- a/subtle_gui/shared.py +++ b/subtle_gui/shared.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from __future__ import annotations import logging @@ -36,7 +37,6 @@ class SharedData(QObject): - spellLanguageChanged = pyqtSignal(str) def __init__(self) -> None: @@ -75,13 +75,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. """ diff --git a/tests/test_common.py b/tests/test_common.py index c3122e6..48dcae8 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -18,20 +18,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + 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 From 7fc5fd4aaa5ff8cb67c4bd86c8ab8349306c6c28 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:51:24 +0200 Subject: [PATCH 3/4] Add docstrings --- pyproject.toml | 48 +++++++++++-------- subtle.py | 2 +- subtle_gui/__init__.py | 18 ++++---- subtle_gui/common.py | 12 ++--- subtle_gui/config.py | 21 +++------ subtle_gui/constants.py | 8 +++- subtle_gui/core/icons.py | 7 ++- subtle_gui/core/media.py | 16 ++----- subtle_gui/core/mediafile.py | 6 +-- subtle_gui/core/mkvextract.py | 9 ++-- subtle_gui/core/spellcheck.py | 24 +++++----- subtle_gui/formats/base.py | 18 ++++---- subtle_gui/formats/pgssubs.py | 86 ++++++++++++++++++++--------------- subtle_gui/formats/srtsubs.py | 14 +++--- subtle_gui/formats/ssasubs.py | 14 +++--- subtle_gui/gui/filetree.py | 9 ++-- subtle_gui/gui/highlighter.py | 11 ++--- subtle_gui/gui/imageviewer.py | 9 ++-- subtle_gui/gui/mediaview.py | 15 ++---- subtle_gui/gui/subsview.py | 15 ++---- subtle_gui/gui/texteditor.py | 21 ++------- subtle_gui/gui/toolspanel.py | 14 ++---- subtle_gui/guimain.py | 7 ++- subtle_gui/ocr/base.py | 5 +- subtle_gui/ocr/tesseract.py | 5 +- subtle_gui/shared.py | 7 ++- tests/test_common.py | 3 +- 27 files changed, 194 insertions(+), 230 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34b9343..c80a9c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,39 +59,49 @@ 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 + "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 e551d50..dd5b69c 100755 --- a/subtle.py +++ b/subtle.py @@ -2,7 +2,7 @@ """ Subtle – Start Script ===================== -""" +""" # noqa import os import sys diff --git a/subtle_gui/__init__.py b/subtle_gui/__init__.py index 0f5842f..16ab784 100644 --- a/subtle_gui/__init__.py +++ b/subtle_gui/__init__.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -39,14 +39,16 @@ # Package Meta # ============ -__package__ = "subtle_gui" -__copyright__ = "Copyright (C) Veronica Berglyd Olsen" -__license__ = "GPLv3" -__author__ = "Veronica Berglyd Olsen" +# fmt: off +__package__ = "subtle_gui" +__copyright__ = "Copyright (C) Veronica Berglyd Olsen" +__license__ = "GPLv3" +__author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" -__email__ = "code@vkbo.net" -__version__ = "26.1.1" -__date__ = "2026-06-24" +__email__ = "code@vkbo.net" +__version__ = "26.1.1" +__date__ = "2026-06-24" +# fmt: on logger = logging.getLogger(__name__) diff --git a/subtle_gui/common.py b/subtle_gui/common.py index 62b4ee8..af106d9 100644 --- a/subtle_gui/common.py +++ b/subtle_gui/common.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -50,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" @@ -75,7 +75,7 @@ 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 @@ -121,7 +121,7 @@ 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: + if n > nmax > 0: buffer.append(chunk) else: buffer.append(chunk[0] + indent + chunk[1:]) @@ -129,13 +129,13 @@ 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: + 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()) diff --git a/subtle_gui/config.py b/subtle_gui/config.py index bf3c06f..c4c2144 100644 --- a/subtle_gui/config.py +++ b/subtle_gui/config.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -63,6 +63,8 @@ class Config: + """Main Config Class.""" + def __init__(self) -> None: self._data: dict[str, dict] = deepcopy(DEFAULTS) @@ -115,8 +117,6 @@ def __init__(self) -> None: self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() - return - ## # Properties ## @@ -173,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.""" @@ -182,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) @@ -202,8 +200,6 @@ def setFontSpec(self, target: T_Fonts, font: QFont | str) -> None: self.subsFont = font self._data["Fonts"]["subsFont"] = font.toString() - return - ## # Methods ## @@ -212,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: @@ -243,7 +238,6 @@ def fonts(self, app: QApplication) -> None: temp = app.font() temp.setPointSizeF(3.0 * temp.pointSizeF()) self.setFontSpec("subs", temp) - return def load(self) -> None: """Load the app config.""" @@ -257,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.""" @@ -267,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 @@ -280,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 b4f85ae..271c00a 100644 --- a/subtle_gui/constants.py +++ b/subtle_gui/constants.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -28,11 +28,13 @@ 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 @@ -40,6 +42,8 @@ class MediaType(Enum): class GuiLabels: + """GUI labels.""" + MEDIA_TYPES: Final[dict[MediaType, str]] = { MediaType.VIDEO: QT_TRANSLATE_NOOP("Constant", "Video"), MediaType.AUDIO: QT_TRANSLATE_NOOP("Constant", "Audio"), diff --git a/subtle_gui/core/icons.py b/subtle_gui/core/icons.py index e548a65..5069acc 100644 --- a/subtle_gui/core/icons.py +++ b/subtle_gui/core/icons.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -65,11 +65,12 @@ 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.""" @@ -84,7 +85,6 @@ class _IconEngine(QIconEngine): def __init__(self, data: bytes) -> None: super().__init__() self._data = data - return def clone(self) -> QIconEngine | None: """Clone the icon engine.""" @@ -103,4 +103,3 @@ def paint(self, painter: QPainter | None, rect: QRect, mode: QIcon.Mode, state: """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 e0e9625..fa5bb05 100644 --- a/subtle_gui/core/media.py +++ b/subtle_gui/core/media.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -45,6 +45,8 @@ class MediaData(QObject): + """Shared data object for media files and tracks.""" + mediaDataCleared = pyqtSignal() newMediaLoaded = pyqtSignal() newTrackSelected = 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,8 @@ 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 self._info = {} @@ -144,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 ## @@ -219,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.""" @@ -244,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 27775ad..b4011cb 100644 --- a/subtle_gui/core/mediafile.py +++ b/subtle_gui/core/mediafile.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -39,6 +39,8 @@ class ContainerType(IntEnum): + """Container Type Enum.""" + UNKNOWN = 0 AVI = 5 MATROSKA = 17 @@ -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 48c0b61..c94f6c4 100644 --- a/subtle_gui/core/mkvextract.py +++ b/subtle_gui/core/mkvextract.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -36,6 +36,8 @@ 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 05de462..658a56d 100644 --- a/subtle_gui/core/spellcheck.py +++ b/subtle_gui/core/spellcheck.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -38,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. @@ -49,7 +49,6 @@ def __init__(self) -> None: self._userDict = UserDictionary() self._language = None self._broker = None - return ## # Properties @@ -57,6 +56,7 @@ def __init__(self) -> None: @property def spellLanguage(self) -> str | None: + """Get the currently loaded spell checking language.""" return self._language ## @@ -94,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: @@ -160,27 +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: @@ -205,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.""" @@ -217,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 d57b1d1..4816aec 100644 --- a/subtle_gui/formats/base.py +++ b/subtle_gui/formats/base.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -38,14 +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: @@ -78,18 +80,20 @@ def checkFrames(self) -> None: (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: @@ -110,18 +114,17 @@ 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 class FrameBase(ABC): + """Base class for subtitle frames.""" + __slots__ = ("_end", "_index", "_start", "_text") def __init__(self, index: int) -> None: @@ -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 85efcbe..f7c6832 100644 --- a/subtle_gui/formats/pgssubs.py +++ b/subtle_gui/formats/pgssubs.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -55,7 +55,6 @@ class PGSSubs(SubtitlesBase): def __init__(self) -> None: super().__init__() - return def read(self, path: Path) -> None: """Read a PGS file.""" @@ -64,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) ## @@ -143,15 +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,8 @@ def getImage(self) -> QImage: class DisplaySet: + """PGS Display Set Class.""" + __slots__ = ("_image", "_ods", "_pcs", "_pds", "_wds") def __init__(self, pcs: PresentationSegment) -> None: @@ -182,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} " @@ -197,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: @@ -210,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.""" @@ -218,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.""" @@ -229,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: @@ -305,7 +306,9 @@ 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") def __init__(self, ts: int, data: bytes) -> None: @@ -313,36 +316,39 @@ def __init__(self, ts: int, data: bytes) -> None: self._data = data self._valid = False self.validate() - return def __repr__(self) -> str: + """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 @property def size(self) -> QSize: @@ -361,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 @@ -370,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 """ @@ -390,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. @@ -413,8 +424,8 @@ def compObjects(self) -> Iterable[tuple[int, int, QPoint]]: 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 @@ -424,12 +435,13 @@ class WindowSegment(BaseSegment): """ def validate(self) -> None: - """Length is 1 + n*9""" + """Validate the segment data.""" + # Length is 1 + n*9 self._valid = len(self._data) % 9 == 1 - return @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]]: @@ -447,8 +459,8 @@ def windows(self) -> Iterable[tuple[int, QRect]]: return -class PaletteSegment(BaseSegment): - """Palette Definition Segment +class PaletteSegment(SegmentBase): + """Palette Definition Segment. This segment is used to define a palette for color conversion. """ @@ -456,11 +468,11 @@ class PaletteSegment(BaseSegment): __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._col: dict[int, QColor] = {} - return @property def id(self) -> int: @@ -490,17 +502,17 @@ def palette(self) -> list[bytes]: 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: @@ -514,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 @@ -541,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 4672615..bedcf84 100644 --- a/subtle_gui/formats/srtsubs.py +++ b/subtle_gui/formats/srtsubs.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -37,9 +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.""" @@ -59,9 +59,9 @@ def write(self, path: Path | None = None, offset: float = 0.0) -> None: 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,16 +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 73f2cf3..d072c59 100644 --- a/subtle_gui/formats/ssasubs.py +++ b/subtle_gui/formats/ssasubs.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -44,6 +44,8 @@ class EventFormat(NamedTuple): + """Format of an SSA event.""" + length: int start: int end: int @@ -51,11 +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.""" @@ -64,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) ## @@ -91,7 +94,6 @@ 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.""" @@ -100,7 +102,6 @@ def _parseFormat(self, line: str) -> None: 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.""" @@ -139,12 +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 19f6114..bbaaab2 100644 --- a/subtle_gui/gui/filetree.py +++ b/subtle_gui/gui/filetree.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -35,6 +35,8 @@ class GuiFileTree(QTreeView): + """GUI File Tree.""" + def __init__(self, parent: QWidget) -> None: super().__init__(parent) @@ -54,8 +56,6 @@ def __init__(self, parent: QWidget) -> None: self.doubleClicked.connect(self._itemDoubleClicked) - return - ## # Methods ## @@ -63,7 +63,6 @@ def __init__(self, parent: QWidget) -> None: def saveSettings(self) -> None: """Save widget settings.""" CONFIG.setSizes("fileTreeColumns", [self.columnWidth(i) for i in range(self._model.columnCount())]) - return ## # Private Slots @@ -76,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 33ac2c7..abb214a 100644 --- a/subtle_gui/gui/highlighter.py +++ b/subtle_gui/gui/highlighter.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -38,6 +38,8 @@ class GuiDocHighlighter(QSyntaxHighlighter): + """GUI Document Syntax Highlighter.""" + def __init__(self, document: QTextDocument) -> None: super().__init__(document) @@ -48,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() @@ -63,16 +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]]: diff --git a/subtle_gui/gui/imageviewer.py b/subtle_gui/gui/imageviewer.py index 09e3f4a..cad9b84 100644 --- a/subtle_gui/gui/imageviewer.py +++ b/subtle_gui/gui/imageviewer.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -33,6 +33,8 @@ 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 2e78a12..3d8b322 100644 --- a/subtle_gui/gui/mediaview.py +++ b/subtle_gui/gui/mediaview.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -53,6 +53,8 @@ class GuiMediaView(QWidget): + """GUI Media View.""" + C_TRACK = 0 C_TYPE = 1 C_CODEC = 2 @@ -121,8 +123,6 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Methods ## @@ -132,7 +132,6 @@ def saveSettings(self) -> None: CONFIG.setSizes( "mediaViewColumns", [self.tracksView.columnWidth(i) for i in range(self.tracksView.columnCount())] ) - return ## # Public Slots @@ -152,7 +151,6 @@ def processNewMediaLoaded(self) -> None: self._setTrackInfo(track, item) self.tracksView.addTopLevelItem(item) self._map[track.trackID] = item - return ## # Private Slots @@ -170,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: @@ -180,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: @@ -195,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: @@ -223,8 +217,6 @@ def _extractFinished(self) -> None: SHARED.media.setCurrentTrack(self._emitTrack) self._emitTrack = None - return - ## # Internal Functions ## @@ -244,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 6f7ee33..c749f09 100644 --- a/subtle_gui/gui/subsview.py +++ b/subtle_gui/gui/subsview.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -38,6 +38,8 @@ class GuiSubtitleView(QWidget): + """GUI Subtitle View.""" + C_DATA = 0 C_ID = 0 C_TIME = 1 @@ -70,8 +72,6 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Methods ## @@ -81,7 +81,6 @@ def saveSettings(self) -> None: CONFIG.setSizes( "subsViewColumns", [self.subEntries.columnWidth(i) for i in range(self.subEntries.columnCount() - 1)] ) - return ## # Public Slots @@ -92,7 +91,6 @@ def processNewMediaLoaded(self) -> None: """Clear previous content.""" self._map = {} self.subEntries.clear() - return @pyqtSlot() def processNewTrackLoaded(self) -> None: @@ -113,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: @@ -129,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: @@ -141,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: @@ -155,7 +149,6 @@ def selectNearby(self, step: int) -> None: ) item.setSelected(True) self._itemClicked(self.subEntries.indexFromItem(item)) - return ## # Private Slots @@ -173,7 +166,6 @@ def _itemClicked(self, index: QModelIndex) -> None: frame.setText(text) self.updateText(frame) self.subsFrameUpdated.emit(frame) - return ## # Internal Functions @@ -182,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 3c35953..78249c6 100644 --- a/subtle_gui/gui/texteditor.py +++ b/subtle_gui/gui/texteditor.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -35,6 +35,8 @@ class GuiTextEditor(QWidget): + """GUI Text Editor.""" + newTextForFrame = pyqtSignal(FrameBase) requestNewFrame = pyqtSignal(int) @@ -110,8 +112,6 @@ def __init__(self, parent: QWidget) -> None: self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut) self.keyContext.activated.connect(self._openContextFromCursor) - return - ## # Public Slots ## @@ -139,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.""" @@ -150,7 +148,6 @@ def updateSpellLanguage(self, language: str) -> None: else: self.spellLang.setCurrentIndex(0) self.spellLang.blockSignals(False) - return ## # Private Slots @@ -162,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: @@ -212,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: @@ -263,8 +252,6 @@ def _openContextMenu(self, pos: QPoint) -> None: ctxMenu.exec(viewport.mapToGlobal(pos)) ctxMenu.deleteLater() - return - ## # Internal Functions ## @@ -301,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 @@ -311,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 1487343..5462b1d 100644 --- a/subtle_gui/gui/toolspanel.py +++ b/subtle_gui/gui/toolspanel.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -47,6 +47,8 @@ class GuiToolsPanel(QWidget): + """GUI Tools Panel.""" + D_SUBS_PATH = Qt.ItemDataRole.UserRole requestSrtSave = pyqtSignal(Path, float) @@ -156,8 +158,6 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) - return - ## # Public Slots ## @@ -171,7 +171,6 @@ def processNewMediaLoaded(self) -> None: self.mediaFile.setText(path.name) self._scanForSubs(path) self._updateTrackInfo() - return @pyqtSlot() def processNewTrackLoaded(self) -> None: @@ -180,7 +179,6 @@ def processNewTrackLoaded(self) -> None: self.srtForced.setChecked(track.forced) self.srtSDH.setChecked(track.sdh) self._updateTrackInfo() - return ## # Private Slots @@ -196,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: @@ -204,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: @@ -228,8 +224,6 @@ def _updateTrackInfo(self) -> None: self.srtSaveDir.setText(str(folder)) self.srtFileName.setText(".".join(bits)) - return - ## # Internal Functions ## @@ -261,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 bfab160..3493e1c 100644 --- a/subtle_gui/guimain.py +++ b/subtle_gui/guimain.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -49,6 +49,8 @@ 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 a43036f..f9e6562 100644 --- a/subtle_gui/ocr/base.py +++ b/subtle_gui/ocr/base.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -33,9 +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 52021fd..17bb20f 100644 --- a/subtle_gui/ocr/tesseract.py +++ b/subtle_gui/ocr/tesseract.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -80,9 +80,10 @@ class TesseractOCR(OCRBase): + """Tesseract OCR implementation.""" + def __init__(self) -> 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 760e480..49ef281 100644 --- a/subtle_gui/shared.py +++ b/subtle_gui/shared.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -37,6 +37,8 @@ class SharedData(QObject): + """Shared Data Object.""" + spellLanguageChanged = pyqtSignal(str) def __init__(self) -> None: @@ -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: @@ -83,7 +84,6 @@ def initSharedData(self, media: MediaData, ocr: OCRBase, spell: SpellEnchant, ic self._ocr = ocr self._spell = spell self._icons = icons - return def setSpellLanguage(self, track: MediaTrack | None) -> None: """Set the current spell checking language.""" @@ -94,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 48dcae8..e21e6e8 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations @@ -36,4 +36,3 @@ def testCommon_closeItalics(): "Foo foo foo", "Bar", ] - return From 8dc0024ac4045516e2753160cff733448f7fa508 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:52:03 +0200 Subject: [PATCH 4/4] Update workflow --- .github/workflows/syntax.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 .