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)