diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index e3dfbf2d..2fc85b7c 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -12,9 +12,9 @@ jobs: uses: actions/checkout@v4 - name: Execute Ruff format - uses: chartboost/ruff-action@v1 + uses: astral-sh/ruff-action@v3 with: - version: 0.8.1 + version: 0.11.0 args: format --check ruff-check: @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Execute Ruff check - uses: chartboost/ruff-action@v1 + uses: astral-sh/ruff-action@v3 with: - version: 0.8.1 + version: 0.11.8 args: check diff --git a/flake.nix b/flake.nix index 62143cb4..f81c2f35 100644 --- a/flake.nix +++ b/flake.nix @@ -30,21 +30,18 @@ { packages = let - pythonPackages = pkgs.python312Packages; + python3Packages = pkgs.python312Packages; - pillow-jxl-plugin = pythonPackages.callPackage ./nix/package/pillow-jxl-plugin.nix { + pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix { inherit (pkgs) cmake; inherit pyexiv2; - inherit (pkgs) rustPlatform; }; - pyexiv2 = pythonPackages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; }; - vtf2img = pythonPackages.callPackage ./nix/package/vtf2img.nix { }; + pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; }; + vtf2img = python3Packages.callPackage ./nix/package/vtf2img.nix { }; in rec { default = tagstudio; - tagstudio = pythonPackages.callPackage ./nix/package { - inherit pillow-jxl-plugin vtf2img; - }; + tagstudio = pkgs.callPackage ./nix/package { inherit pillow-jxl-plugin vtf2img; }; tagstudio-jxl = tagstudio.override { withJXLSupport = true; }; inherit pillow-jxl-plugin pyexiv2 vtf2img; diff --git a/nix/package/default.nix b/nix/package/default.nix index 952e770e..bcc6f7e3 100644 --- a/nix/package/default.nix +++ b/nix/package/default.nix @@ -1,44 +1,22 @@ { - buildPythonApplication, - chardet, ffmpeg-headless, - ffmpeg-python, - hatchling, - humanfriendly, lib, - mutagen, - numpy, - opencv-python, - pillow, - pillow-heif, - pillow-jxl-plugin, pipewire, - pydantic, - pydub, - pyside6, - pytest-qt, - pytest-xdist, - pytestCheckHook, - pythonRelaxDepsHook, + python3Packages, qt6, - rawpy, - send2trash, - sqlalchemy, stdenv, - structlog, - syrupy, - toml, - ujson, - vtf2img, wrapGAppsHook, + pillow-jxl-plugin, + vtf2img, + withJXLSupport ? false, }: let pyproject = (lib.importTOML ../../pyproject.toml).project; in -buildPythonApplication { +python3Packages.buildPythonApplication { pname = pyproject.name; inherit (pyproject) version; pyproject = true; @@ -46,7 +24,7 @@ buildPythonApplication { src = ../../.; nativeBuildInputs = [ - pythonRelaxDepsHook + python3Packages.pythonRelaxDepsHook qt6.wrapQtAppsHook # INFO: Should be unnecessary once PR is pulled. @@ -59,7 +37,7 @@ buildPythonApplication { qt6.qtmultimedia ]; - nativeCheckInputs = [ + nativeCheckInputs = with python3Packages; [ pytest-qt pytest-xdist pytestCheckHook @@ -80,30 +58,41 @@ buildPythonApplication { lib.makeLibraryPath [ pipewire ] }"; - pythonRemoveDeps = true; + pythonRemoveDeps = lib.optional (!withJXLSupport) [ "pillow_jxl" ]; + pythonRelaxDeps = [ + "numpy" + "pillow" + "pillow-heif" + "pillow-jxl-plugin" + "structlog" + "typing-extensions" + ]; pythonImportsCheck = [ "tagstudio" ]; - build-system = [ hatchling ]; - dependencies = [ - chardet - ffmpeg-python - humanfriendly - mutagen - numpy - opencv-python - pillow - pillow-heif - pydantic - pydub - pyside6 - rawpy - send2trash - sqlalchemy - structlog - toml - ujson - vtf2img - ] ++ lib.optional withJXLSupport pillow-jxl-plugin; + build-system = with python3Packages; [ hatchling ]; + dependencies = + with python3Packages; + [ + chardet + ffmpeg-python + humanfriendly + mutagen + numpy + opencv-python + pillow + pillow-heif + pydantic + pydub + pyside6 + rawpy + send2trash + sqlalchemy + structlog + toml + ujson + vtf2img + ] + ++ lib.optional withJXLSupport pillow-jxl-plugin; disabledTests = [ # INFO: These tests require modifications to a library, which does not work diff --git a/pyproject.toml b/pyproject.toml index 34706916..22503b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,42 +8,43 @@ description = "A User-Focused Photo & File Management System." version = "9.5.2" license = "GPL-3.0-only" readme = "README.md" +requires-python = ">=3.12,<3.13" dependencies = [ - "chardet==5.2.0", - "ffmpeg-python==0.2.0", - "humanfriendly==10.0", - "mutagen==1.47.0", - "numpy==2.1.0", - "opencv_python==4.10.0.84", - "Pillow==10.3.0", - "pillow-heif==0.16.0", - "pillow-jxl-plugin==1.3.0", - "pydub==0.25.1", - "PySide6==6.8.0.1", - "rawpy==0.22.0", - "Send2Trash==1.8.3", - "SQLAlchemy==2.0.34", - "structlog==24.4.0", - "typing_extensions>=3.10.0.0,<4.11.0", - "ujson>=5.8.0,<5.9.0", - "vtf2img==0.1.0", - "toml==0.10.2", - "pydantic==2.9.2", + "chardet~=5.2", + "ffmpeg-python~=0.2", + "humanfriendly==10.*", + "mutagen~=1.47", + "numpy~=2.2", + "opencv_python~=4.11", + "Pillow~=11.2", + "pillow-heif~=0.22", + "pillow-jxl-plugin~=1.3", + "pydantic~=2.10", + "pydub~=0.25", + "PySide6==6.8.0.*", + "rawpy~=0.24", + "Send2Trash~=1.8", + "SQLAlchemy~=2.0", + "structlog~=25.3", + "toml~=0.10", + "typing_extensions~=4.13", + "ujson~=5.10", + "vtf2img~=0.1", ] [project.optional-dependencies] dev = ["tagstudio[mkdocs,mypy,pre-commit,pyinstaller,pytest,ruff]"] -mkdocs = ["mkdocs-material[imaging]==9.*"] -mypy = ["mypy==1.11.2", "mypy-extensions==1.*", "types-ujson>=5.8.0,<5.9.0"] -pre-commit = ["pre-commit==3.7.0"] -pyinstaller = ["Pyinstaller==6.6.0"] +mkdocs = ["mkdocs-material==9.*"] +mypy = ["mypy==1.15.0", "mypy-extensions==1.*", "types-ujson~=5.10"] +pre-commit = ["pre-commit~=4.2"] +pyinstaller = ["Pyinstaller~=6.13"] pytest = [ - "pytest==8.2.0", - "pytest-cov==5.0.0", + "pytest==8.3.5", + "pytest-cov==6.1.1", "pytest-qt==4.4.0", - "syrupy==4.7.1", + "syrupy==4.9.1", ] -ruff = ["ruff==0.8.1"] +ruff = ["ruff==0.11.8"] [project.gui-scripts] tagstudio = "tagstudio.main:main" diff --git a/src/tagstudio/qt/modals/ffmpeg_checker.py b/src/tagstudio/qt/modals/ffmpeg_checker.py index c87f538a..776032b2 100644 --- a/src/tagstudio/qt/modals/ffmpeg_checker.py +++ b/src/tagstudio/qt/modals/ffmpeg_checker.py @@ -41,7 +41,7 @@ class FfmpegChecker(QMessageBox): red = get_ui_color(ColorType.PRIMARY, UiColor.RED) green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN) - missing = f"{Translations["generic.missing"]}" + missing = f"{Translations['generic.missing']}" found = f"{Translations['about.module.found']}" status = Translations.format( "ffmpeg.missing.status", @@ -50,4 +50,4 @@ class FfmpegChecker(QMessageBox): ffprobe=ffprobe, ffprobe_status=found if which(FFPROBE_CMD) else missing, ) - self.setText(f"{Translations["ffmpeg.missing.description"]}

{status}") + self.setText(f"{Translations['ffmpeg.missing.description']}

{status}") diff --git a/src/tagstudio/qt/modals/tag_color_manager.py b/src/tagstudio/qt/modals/tag_color_manager.py index 3f2fe0af..8a1403dc 100644 --- a/src/tagstudio/qt/modals/tag_color_manager.py +++ b/src/tagstudio/qt/modals/tag_color_manager.py @@ -2,7 +2,8 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import TYPE_CHECKING, Callable, override +from collections.abc import Callable +from typing import TYPE_CHECKING, override import structlog from PySide6 import QtCore, QtGui diff --git a/src/tagstudio/qt/widgets/fields.py b/src/tagstudio/qt/widgets/fields.py index c787110f..d2678b55 100644 --- a/src/tagstudio/qt/widgets/fields.py +++ b/src/tagstudio/qt/widgets/fields.py @@ -4,8 +4,9 @@ import math +from collections.abc import Callable from pathlib import Path -from typing import Callable, override +from typing import override from warnings import catch_warnings import structlog diff --git a/src/tagstudio/qt/widgets/migration_modal.py b/src/tagstudio/qt/widgets/migration_modal.py index 068334fd..975c738b 100644 --- a/src/tagstudio/qt/widgets/migration_modal.py +++ b/src/tagstudio/qt/widgets/migration_modal.py @@ -616,7 +616,7 @@ class JsonMigrationModal(QObject): logger.info( "[Field Comparison]", - fields="\n".join([str(x) for x in zip(json_fields, sql_fields)]), + fields="\n".join([str(x) for x in zip(json_fields, sql_fields, strict=False)]), ) self.field_parity = True diff --git a/src/tagstudio/qt/widgets/panel.py b/src/tagstudio/qt/widgets/panel.py index cdd59b2c..db923bab 100755 --- a/src/tagstudio/qt/widgets/panel.py +++ b/src/tagstudio/qt/widgets/panel.py @@ -3,7 +3,8 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import Callable, override +from collections.abc import Callable +from typing import override import structlog from PySide6 import QtCore, QtGui diff --git a/src/tagstudio/qt/widgets/preview/field_containers.py b/src/tagstudio/qt/widgets/preview/field_containers.py index bb366808..f93b5432 100644 --- a/src/tagstudio/qt/widgets/preview/field_containers.py +++ b/src/tagstudio/qt/widgets/preview/field_containers.py @@ -315,7 +315,7 @@ class FieldContainers(QWidget): # Normalize line endings in any text content. if not is_mixed: - assert isinstance(field.value, (str, type(None))) + assert isinstance(field.value, str | type(None)) text = field.value or "" else: text = "Mixed Data" @@ -355,7 +355,7 @@ class FieldContainers(QWidget): container.set_inline(False) # Normalize line endings in any text content. if not is_mixed: - assert isinstance(field.value, (str, type(None))) + assert isinstance(field.value, str | type(None)) text = (field.value or "").replace("\r", "\n") else: text = "Mixed Data" @@ -514,7 +514,7 @@ class FieldContainers(QWidget): """Update a field in all selected Entries, given a field object.""" assert isinstance( field, - (TextField, DatetimeField), + TextField | DatetimeField, ), f"instance: {type(field)}" entry_ids = [e.id for e in self.cached_entries] diff --git a/src/tagstudio/qt/widgets/preview/file_attributes.py b/src/tagstudio/qt/widgets/preview/file_attributes.py index 2ca9010a..268ce7e9 100644 --- a/src/tagstudio/qt/widgets/preview/file_attributes.py +++ b/src/tagstudio/qt/widgets/preview/file_attributes.py @@ -165,11 +165,11 @@ class FileAttributes(QWidget): for i, part in enumerate(display_path.parts): part_ = part.strip(os.path.sep) if i != len(display_path.parts) - 1: - file_str += f"{"\u200b".join(part_)}{separator}" + file_str += f"{'\u200b'.join(part_)}{separator}" else: if file_str != "": file_str += "
" - file_str += f"{"\u200b".join(part_)}" + file_str += f"{'\u200b'.join(part_)}" self.file_label.setText(file_str) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) self.opener = FileOpenerHelper(filepath) diff --git a/src/tagstudio/qt/widgets/progress.py b/src/tagstudio/qt/widgets/progress.py index dcd1ed16..205aed8a 100644 --- a/src/tagstudio/qt/widgets/progress.py +++ b/src/tagstudio/qt/widgets/progress.py @@ -3,7 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import Callable +from collections.abc import Callable from PySide6.QtCore import Qt, QThreadPool from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget diff --git a/src/tagstudio/qt/widgets/text_line_edit.py b/src/tagstudio/qt/widgets/text_line_edit.py index 9822abea..1c719fab 100644 --- a/src/tagstudio/qt/widgets/text_line_edit.py +++ b/src/tagstudio/qt/widgets/text_line_edit.py @@ -3,7 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import Callable +from collections.abc import Callable from PySide6.QtWidgets import QLineEdit, QVBoxLayout diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index f0116c68..e23351c3 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -11,6 +11,7 @@ import zipfile from copy import deepcopy from io import BytesIO from pathlib import Path +from typing import cast from warnings import catch_warnings import cv2 @@ -754,8 +755,8 @@ class ThumbRenderer(QObject): data = np.asarray(raw.getchannel(0)) m, n = data.shape[:2] - col: np.ndarray = data.any(0) - row: np.ndarray = data.any(1) + col: np.ndarray = cast(np.ndarray, data.any(0)) + row: np.ndarray = cast(np.ndarray, data.any(1)) cropped_data = np.asarray(raw)[ row.argmax() : m - row[::-1].argmax(), col.argmax() : n - col[::-1].argmax(), @@ -802,7 +803,7 @@ class ThumbRenderer(QObject): bg = Image.new("RGBA", (size, size), color="#00000000") draw = ImageDraw.Draw(bg) lines_of_padding = 2 - y_offset = 0 + y_offset = 0.0 for font_size in scaled_sizes: font = ImageFont.truetype(filepath, size=font_size) diff --git a/tests/fixtures/sample.epub b/tests/fixtures/sample.epub deleted file mode 100644 index b625b67b..00000000 Binary files a/tests/fixtures/sample.epub and /dev/null differ diff --git a/tests/fixtures/sample.ods b/tests/fixtures/sample.ods deleted file mode 100644 index ecc97b12..00000000 Binary files a/tests/fixtures/sample.ods and /dev/null differ diff --git a/tests/fixtures/sample.odt b/tests/fixtures/sample.odt deleted file mode 100644 index 4cb6f2f1..00000000 Binary files a/tests/fixtures/sample.odt and /dev/null differ diff --git a/tests/fixtures/sample.pdf b/tests/fixtures/sample.pdf deleted file mode 100644 index 0293578a..00000000 Binary files a/tests/fixtures/sample.pdf and /dev/null differ diff --git a/tests/fixtures/sample.svg b/tests/fixtures/sample.svg deleted file mode 100644 index 99c924a8..00000000 --- a/tests/fixtures/sample.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.epub-_epub_cover].png b/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.epub-_epub_cover].png deleted file mode 100644 index 2b5a2581..00000000 Binary files a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.epub-_epub_cover].png and /dev/null differ diff --git a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png b/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png deleted file mode 100644 index 5e749f2d..00000000 Binary files a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png and /dev/null differ diff --git a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png b/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png deleted file mode 100644 index ac2158e7..00000000 Binary files a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png and /dev/null differ diff --git a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.pdf-thumbnailer3].png b/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.pdf-thumbnailer3].png deleted file mode 100644 index 0ba9ea61..00000000 Binary files a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.pdf-thumbnailer3].png and /dev/null differ diff --git a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.svg-thumbnailer4].png b/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.svg-thumbnailer4].png deleted file mode 100644 index ebd90431..00000000 Binary files a/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.svg-thumbnailer4].png and /dev/null differ diff --git a/tests/qt/test_file_path_options.py b/tests/qt/test_file_path_options.py index 97052e09..40dfeab1 100644 --- a/tests/qt/test_file_path_options.py +++ b/tests/qt/test_file_path_options.py @@ -79,11 +79,11 @@ def test_file_path_display( for i, part in enumerate(display_path.parts): part_ = part.strip(os.path.sep) if i != len(display_path.parts) - 1: - file_str += f"{"\u200b".join(part_)}{separator}" + file_str += f"{'\u200b'.join(part_)}{separator}" else: if file_str != "": file_str += "
" - file_str += f"{"\u200b".join(part_)}" + file_str += f"{'\u200b'.join(part_)}" # Assert the file path is displayed correctly assert panel.file_attrs.file_label.text() == file_str diff --git a/tests/qt/test_thumb_renderer.py b/tests/qt/test_thumb_renderer.py deleted file mode 100644 index 721d0ffc..00000000 --- a/tests/qt/test_thumb_renderer.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2024 Travis Abendshien (CyanVoxel). -# Licensed under the GPL-3.0 License. -# Created for TagStudio: https://github.com/CyanVoxel/TagStudio - -import io -from functools import partial -from pathlib import Path - -import pytest -from PIL import Image -from syrupy.extensions.image import PNGImageSnapshotExtension - -from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer - - -@pytest.mark.parametrize( - ["fixture_file", "thumbnailer"], - [ - ( - "sample.odt", - ThumbRenderer._open_doc_thumb, - ), - ( - "sample.ods", - ThumbRenderer._open_doc_thumb, - ), - ( - "sample.epub", - ThumbRenderer._epub_cover, - ), - ( - "sample.pdf", - partial(ThumbRenderer._pdf_thumb, size=200), - ), - ( - "sample.svg", - partial(ThumbRenderer._image_vector_thumb, size=200), - ), - ], -) -def test_preview_render(cwd, fixture_file, thumbnailer, snapshot): - file_path: Path = cwd / "fixtures" / fixture_file - img: Image.Image = thumbnailer(file_path) - - img_bytes = io.BytesIO() - img.save(img_bytes, format="PNG") - img_bytes.seek(0) - - assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)