From e9f6e7787da1f2f4c3f9d555cb60cd59380fdf06 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Aug 2025 15:50:36 +0200 Subject: [PATCH 1/4] Add ruff linting settings --- pyproject.toml | 65 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f33ccd3..6b3c3ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,12 +54,61 @@ force_grid_wrap = 0 lines_between_types = 1 forced_separate = ["tests.*", "PyQt6.*"] -[tool.flake8] -max-line-length = 99 -ignore = ["E133", "E221", "E226", "E228", "E241", "W503", "ANN101", "ANN102", "ANN401"] -per-file-ignores = ["tests/*:ANN"] -exclude = ["docs/*"] +[tool.ruff] +line-length = 99 -[tool.autopep8] -max_line_length = 99 -ignore = ["E133", "E221", "E226", "E228", "E241", "W503"] +[tool.ruff.lint] +preview = true + +# 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) +] +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 + "PLW2901", # redefined-loop-name + "RUF001", # ambiguous-unicode-character-string + "RUF002", # ambiguous-unicode-character-docstring + "RUF015", # unnecessary-iterable-allocation-for-first-element + "UP015", # redundant-open-modes + "UP030", # format-literals +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["ANN", "SLF", "TC", "PLC2701"] +"utils/*" = ["ANN", "SLF", "TC"] + +[tool.ruff.format] +quote-style = "double" + +[tool.pyright] +include = ["subtle"] +exclude = ["**/__pycache__"] + +reportIncompatibleMethodOverride = false + +pythonVersion = "3.11" From 9aac7e342646ace129506b6cd1dc4504a70be089 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:08:13 +0200 Subject: [PATCH 2/4] Update project settings --- .gitignore | 5 ++++- pyproject.toml | 8 +++----- requirements-dev.txt | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 requirements-dev.txt diff --git a/.gitignore b/.gitignore index 684a80a..a0975ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ -# Code +# Python +.venv/ +.ruff_cache/ *.pyc # Tests .pytest_cache .coverage coverage.* +htmlcov/ diff --git a/pyproject.toml b/pyproject.toml index 6b3c3ef..7b133b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,19 +12,17 @@ readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GNU General Public License v3"} classifiers = [ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Development Status :: 3 - Alpha", "Operating System :: OS Independent", "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Topic :: Multimedia", ] -requires-python = ">=3.9" +requires-python = ">=3.11" dependencies = [ "pyqt6>=6.4", "pyenchant>=3.0.0", @@ -46,7 +44,7 @@ version = {attr = "subtle.__version__"} include = ["subtle*"] [tool.isort] -py_version="310" +py_version="311" line_length = 99 wrap_length = 79 multi_line_output = 5 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5d158db --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +ruff +pyright From aeecdf1690ed5ed19f6b1620145f886f40ed3ae9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:09:51 +0200 Subject: [PATCH 3/4] Fix linting errors --- subtle/__init__.py | 8 ++++---- subtle/common.py | 14 ++++++++------ subtle/constants.py | 3 ++- subtle/core/media.py | 10 +++++++--- subtle/core/mediafile.py | 7 +++++-- subtle/core/mkvextract.py | 5 ++++- subtle/core/spellcheck.py | 5 ++++- subtle/formats/base.py | 13 ++++++++----- subtle/formats/pgssubs.py | 15 +++++++++------ subtle/formats/srtsubs.py | 7 +++++-- subtle/formats/ssasubs.py | 8 +++++--- subtle/gui/highlighter.py | 2 +- subtle/gui/mediaview.py | 1 - subtle/guimain.py | 6 +++++- subtle/ocr/base.py | 4 +++- subtle/ocr/tesseract.py | 9 ++++++--- 16 files changed, 76 insertions(+), 41 deletions(-) diff --git a/subtle/__init__.py b/subtle/__init__.py index 9749a01..1718536 100644 --- a/subtle/__init__.py +++ b/subtle/__init__.py @@ -118,18 +118,18 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Parse Options try: - inOpts, inRemain = getopt.getopt(sysArgs, shortOpt, longOpt) + inOpts, _ = getopt.getopt(sysArgs, shortOpt, longOpt) except getopt.GetoptError as exc: print(helpMsg) - print(f"ERROR: {str(exc)}") + print(f"ERROR: {exc!s}") sys.exit(2) - for inOpt, inArg in inOpts: + for inOpt, _ in inOpts: if inOpt in ("-h", "--help"): print(helpMsg) sys.exit(0) elif inOpt in ("-v", "--version"): - print("Subtle Version %s [%s]" % (__version__, __date__)) + print(f"Subtle Version {__version__} [{__date__}]") sys.exit(0) elif inOpt in ("-i", "--info"): logLevel = logging.INFO diff --git a/subtle/common.py b/subtle/common.py index ea8f8ba..690d816 100644 --- a/subtle/common.py +++ b/subtle/common.py @@ -22,9 +22,11 @@ import json import logging -import re -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal + +if TYPE_CHECKING: + import re logger = logging.getLogger(__name__) @@ -74,10 +76,10 @@ def textCleanup(text: str) -> str: def regexCleanup(text: str, patterns: list[tuple[re.Pattern, str]]) -> str: """Replaces all occurrences of match group 1 in patterns.""" for regEx, value in patterns: - matches = [] - for match in regEx.finditer(text): - if (s := match.start(1)) >= 0 and (e := match.end(1)) >= 0: - matches.append((s, e, value)) + matches = [ + (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:] return text diff --git a/subtle/constants.py b/subtle/constants.py index f98ec85..a2facb8 100644 --- a/subtle/constants.py +++ b/subtle/constants.py @@ -21,6 +21,7 @@ from __future__ import annotations from enum import Enum +from typing import Final from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication @@ -40,7 +41,7 @@ class MediaType(Enum): class GuiLabels: - MEDIA_TYPES = { + 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"), diff --git a/subtle/core/media.py b/subtle/core/media.py index 3be0658..8ab7120 100644 --- a/subtle/core/media.py +++ b/subtle/core/media.py @@ -22,20 +22,24 @@ import logging -from collections.abc import Iterable -from pathlib import Path +from typing import TYPE_CHECKING from subtle import SHARED from subtle.common import decodeTS from subtle.constants import MediaType from subtle.core.mediafile import MediaFile -from subtle.formats.base import FrameBase, SubtitlesBase from subtle.formats.pgssubs import PGSSubs from subtle.formats.srtsubs import SRTSubs from subtle.formats.ssasubs import SSASubs from PyQt6.QtCore import QObject, pyqtSignal +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + + from subtle.formats.base import FrameBase, SubtitlesBase + logger = logging.getLogger(__name__) diff --git a/subtle/core/mediafile.py b/subtle/core/mediafile.py index 0b4e548..a6edc19 100644 --- a/subtle/core/mediafile.py +++ b/subtle/core/mediafile.py @@ -24,13 +24,16 @@ import logging import subprocess -from collections.abc import Iterable from enum import IntEnum from hashlib import sha1 -from pathlib import Path +from typing import TYPE_CHECKING from subtle import CONFIG +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + logger = logging.getLogger(__name__) diff --git a/subtle/core/mkvextract.py b/subtle/core/mkvextract.py index 54612c0..318f3f6 100644 --- a/subtle/core/mkvextract.py +++ b/subtle/core/mkvextract.py @@ -22,12 +22,15 @@ import logging -from pathlib import Path +from typing import TYPE_CHECKING from subtle.common import checkInt from PyQt6.QtCore import QObject, QProcess, pyqtSignal, pyqtSlot +if TYPE_CHECKING: + from pathlib import Path + logger = logging.getLogger(__name__) diff --git a/subtle/core/spellcheck.py b/subtle/core/spellcheck.py index f744de9..b5d2e78 100644 --- a/subtle/core/spellcheck.py +++ b/subtle/core/spellcheck.py @@ -23,13 +23,16 @@ import json import logging -from collections.abc import Iterator from pathlib import Path +from typing import TYPE_CHECKING from subtle import CONFIG from PyQt6.QtCore import QLocale +if TYPE_CHECKING: + from collections.abc import Iterator + logger = logging.getLogger(__name__) diff --git a/subtle/formats/base.py b/subtle/formats/base.py index a3ae04f..18343d2 100644 --- a/subtle/formats/base.py +++ b/subtle/formats/base.py @@ -23,19 +23,22 @@ import logging from abc import ABC, abstractmethod -from collections.abc import Iterable -from pathlib import Path +from typing import TYPE_CHECKING from subtle.common import formatTS -from PyQt6.QtGui import QImage +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + + from PyQt6.QtGui import QImage logger = logging.getLogger(__name__) class SubtitlesBase(ABC): - __slots__ = ("_path", "_frames") + __slots__ = ("_frames", "_path") def __init__(self) -> None: self._path: Path | None = None @@ -119,7 +122,7 @@ def _copyFrames(self, frameType: type[FrameBase], other: SubtitlesBase) -> None: class FrameBase(ABC): - __slots__ = ("_index", "_start", "_end", "_text") + __slots__ = ("_end", "_index", "_start", "_text") def __init__(self, index: int) -> None: self._index: int = index diff --git a/subtle/formats/pgssubs.py b/subtle/formats/pgssubs.py index 453bc14..3aebd3c 100644 --- a/subtle/formats/pgssubs.py +++ b/subtle/formats/pgssubs.py @@ -23,8 +23,7 @@ import logging from abc import ABC, abstractmethod -from collections.abc import Iterable -from pathlib import Path +from typing import TYPE_CHECKING from subtle.common import formatTS from subtle.formats.base import FrameBase, SubtitlesBase @@ -32,6 +31,10 @@ from PyQt6.QtCore import QMargins, QPoint, QRect, QSize from PyQt6.QtGui import QColor, QImage, QPainter, qRgba +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + logger = logging.getLogger(__name__) COMP_NORMAL = 0x00 @@ -172,7 +175,7 @@ def getImage(self) -> QImage: class DisplaySet: - __slots__ = ("_pcs", "_wds", "_pds", "_ods", "_image") + __slots__ = ("_image", "_ods", "_pcs", "_pds", "_wds") def __init__(self, pcs: PresentationSegment) -> None: self._pcs: PresentationSegment = pcs @@ -293,7 +296,7 @@ def render(self, crop: bool = True) -> QImage: frame = frame.united(QRect(offset, box)) painter.drawImage(offset, QImage( - raw, box.width(), box.height(), QImage.Format.Format_ARGB32 + bytes(raw), box.width(), box.height(), QImage.Format.Format_ARGB32 )) painter.end() @@ -307,7 +310,7 @@ def render(self, crop: bool = True) -> QImage: class BaseSegment(ABC): - __slots__ = ("_ts", "_data", "_valid") + __slots__ = ("_data", "_ts", "_valid") def __init__(self, ts: int, data: bytes) -> None: self._ts = ts @@ -453,7 +456,7 @@ class PaletteSegment(BaseSegment): This segment is used to define a palette for color conversion. """ - __slots__ = ("_col") + __slots__ = ("_col",) def validate(self) -> None: """Length is 2 + n*5""" diff --git a/subtle/formats/srtsubs.py b/subtle/formats/srtsubs.py index 76f2394..f00246d 100644 --- a/subtle/formats/srtsubs.py +++ b/subtle/formats/srtsubs.py @@ -22,12 +22,15 @@ import logging -from pathlib import Path +from typing import TYPE_CHECKING from subtle.common import closeItalics, decodeTS, formatTS, textCleanup from subtle.formats.base import FrameBase, SubtitlesBase -from PyQt6.QtGui import QImage +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6.QtGui import QImage logger = logging.getLogger(__name__) diff --git a/subtle/formats/ssasubs.py b/subtle/formats/ssasubs.py index 3b04e1b..74cff44 100644 --- a/subtle/formats/ssasubs.py +++ b/subtle/formats/ssasubs.py @@ -23,13 +23,15 @@ import logging import re -from pathlib import Path -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from subtle.common import closeItalics, decodeTS, regexCleanup, simplified, textCleanup from subtle.formats.base import FrameBase, SubtitlesBase -from PyQt6.QtGui import QImage +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6.QtGui import QImage logger = logging.getLogger(__name__) diff --git a/subtle/gui/highlighter.py b/subtle/gui/highlighter.py index d9004f3..6affab3 100644 --- a/subtle/gui/highlighter.py +++ b/subtle/gui/highlighter.py @@ -71,7 +71,7 @@ def highlightBlock(self, text: str) -> None: class TextBlockData(QTextBlockUserData): - __slots__ = ("_spellErrors") + __slots__ = ("_spellErrors",) def __init__(self) -> None: super().__init__() diff --git a/subtle/gui/mediaview.py b/subtle/gui/mediaview.py index efb82a6..cd4f45a 100644 --- a/subtle/gui/mediaview.py +++ b/subtle/gui/mediaview.py @@ -192,7 +192,6 @@ def _extractProgress(self, value: int) -> None: def _extractFinished(self) -> None: """Process track extraction finished.""" self.progressText.setText(self.tr("Reading tracks ...")) - print(self._extracted) for idx in self._extracted: if track := SHARED.media.getTrack(idx): track.readTrackFile() diff --git a/subtle/guimain.py b/subtle/guimain.py index b156f17..8e627b7 100644 --- a/subtle/guimain.py +++ b/subtle/guimain.py @@ -23,6 +23,8 @@ import logging import sys +from typing import TYPE_CHECKING + from subtle import CONFIG, SHARED from subtle.core.icons import GuiIcons from subtle.core.media import MediaData @@ -36,9 +38,11 @@ from subtle.ocr.tesseract import TesseractOCR from PyQt6.QtCore import Qt -from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import QMainWindow, QSplitter +if TYPE_CHECKING: + from PyQt6.QtGui import QCloseEvent + logger = logging.getLogger(__name__) diff --git a/subtle/ocr/base.py b/subtle/ocr/base.py index 189dffc..646220a 100644 --- a/subtle/ocr/base.py +++ b/subtle/ocr/base.py @@ -23,8 +23,10 @@ import logging from abc import ABC, abstractmethod +from typing import TYPE_CHECKING -from PyQt6.QtGui import QImage +if TYPE_CHECKING: + from PyQt6.QtGui import QImage logger = logging.getLogger(__name__) diff --git a/subtle/ocr/tesseract.py b/subtle/ocr/tesseract.py index 9be0345..2686e31 100644 --- a/subtle/ocr/tesseract.py +++ b/subtle/ocr/tesseract.py @@ -25,13 +25,16 @@ import subprocess import uuid -from pathlib import Path +from typing import TYPE_CHECKING from subtle import CONFIG from subtle.common import regexCleanup, simplified from subtle.ocr.base import OCRBase -from PyQt6.QtGui import QImage +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6.QtGui import QImage logger = logging.getLogger(__name__) @@ -88,7 +91,7 @@ def __init__(self) -> None: def processImage(self, index: int, image: QImage, lang: list[str]) -> list[str]: """Perform OCR on a QImage.""" - tmpFile = CONFIG.dumpPath / f"{str(uuid.uuid4())}.png" + tmpFile = CONFIG.dumpPath / f"{uuid.uuid4()!s}.png" image.save(str(tmpFile), quality=100) result = self._processText(self._callTesseract(tmpFile, lang), lang) tmpFile.unlink(missing_ok=True) From c5a3246551e8e2f04aba7b0353220964ea39c513 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:13:20 +0200 Subject: [PATCH 4/4] Add linting check CI/CD job --- .github/workflows/syntax.yml | 35 +++++++++++++++++++++++++++++++++++ requirements-dev.txt | 3 ++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/syntax.yml diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml new file mode 100644 index 0000000..4b63c70 --- /dev/null +++ b/.github/workflows/syntax.yml @@ -0,0 +1,35 @@ +name: Linting + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + checkSyntax: + runs-on: ubuntu-latest + steps: + - name: Python Setup + uses: actions/setup-python@v5 + with: + python-version: "3" + architecture: x64 + - name: Checkout Source + uses: actions/checkout@v4 + - name: Install Dependencies + run: pip install -r requirements.txt -r requirements-dev.txt + - name: Ruff Check + run: | + ruff --version + ruff check + - name: Pyright Check + run: | + pyright --version + pyright + - name: Isort Check + run: | + isort --version + isort --check . diff --git a/requirements-dev.txt b/requirements-dev.txt index 5d158db..18c97a3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ -ruff +isort pyright +ruff