Compare commits

..

6 Commits

Author SHA1 Message Date
Xarvex
08a10ef288 chore(thumb_renderer): bump Pillow
Builds upon #1065, mkdocs-material has now bumped the Pillow version
being used, and now the built in AVIF support can be used
2025-11-24 17:10:24 -06:00
Sola-ris
6397b228eb feat: add windows runner for pytest (#1201)
* feat: run tests on windows and macOS.

* resue steps via anchors.

* remove macOS job.
2025-11-23 20:23:41 -08:00
Trigam
4d882a7156 fix: 'Add Tag to Selected' action fails (#1224)
* Fix

* Fix preview panel being reset

* Fix 'Add Tag to Selected' not emitting badge signals
2025-11-23 20:20:14 -08:00
Travis Abendshien
4c0cb1648f chore: update PULL_REQUEST_TEMPLATE.md 2025-11-14 15:31:13 -08:00
CallMeHein
0529925cd1 fix: "Search for Tag" in Tag Manager executes multiple queries (#1173)
* test: add test to ensure actions are replaced when widgets are replaced

* fix: disconnect previous action before adding new action
2025-11-07 16:18:07 -08:00
Timo Gottszky
5dcad418f7 fix(nix): replace wrapGAppsHook with wrapGAppsHook3 (#1189)
wrapGAppsHook has been aliased to wrapGAppsHook3 for a long time.
Recently this alias was converted to a throw, breaking the build with
newer nixpkgs versions.
2025-11-07 16:28:18 -06:00
11 changed files with 107 additions and 53 deletions

View File

@@ -4,7 +4,8 @@
^^^ Summarize the changes done and why they were done above.
By submitting this pull request, you certify that you have read the
[CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md).
[Contributing](https://docs.tagstud.io/contributing) page on our documentation site,
or in the project's [CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/docs/contributing.md) file.
IMPORTANT FOR FEATURES: Please verify that a feature request or some other form
of communication with maintainers was already conducted in terms of approving.

View File

@@ -4,21 +4,24 @@ name: pytest
on: [push, pull_request]
jobs:
pytest:
name: Run pytest
pytest-linux:
name: Run pytest (Linux)
runs-on: ubuntu-24.04
steps:
- name: Checkout repo
- &checkout
name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
- &setup-python
name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- name: Install Python dependencies
- &install-dependencies
name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pytest]
@@ -52,10 +55,27 @@ jobs:
name: coverage
path: coverage.xml
pytest-windows:
name: Run pytest (Windows)
runs-on: windows-2025
steps:
- *checkout
- *setup-python
- *install-dependencies
- name: Install system dependencies
run: |
choco install ripgrep
- name: Execute pytest
run: |
pytest
coverage:
name: Check coverage
runs-on: ubuntu-latest
needs: pytest
needs: pytest-linux
steps:
- name: Fetch coverage

12
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"lastModified": 1763759067,
"narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1757487488,
"narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=",
"lastModified": 1763835633,
"narHash": "sha256-HzxeGVID5MChuCPESuC0dlQL1/scDKu+MmzoVBJxulM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0",
"rev": "050e09e091117c3d7328c7b2b7b577492c43c134",
"type": "github"
},
"original": {

View File

@@ -6,7 +6,7 @@
qt6,
ripgrep,
stdenv,
wrapGAppsHook,
wrapGAppsHook3,
pillow-jxl-plugin,
@@ -30,7 +30,7 @@ python3Packages.buildPythonApplication {
# Should be unnecessary once PR is pulled.
# PR: https://github.com/NixOS/nixpkgs/pull/271037
# Issue: https://github.com/NixOS/nixpkgs/issues/149812
wrapGAppsHook
wrapGAppsHook3
];
buildInputs = [
qt6.qtbase
@@ -70,7 +70,7 @@ python3Packages.buildPythonApplication {
"\${qtWrapperArgs[@]}"
];
pythonRemoveDeps = lib.optional (!withJXLSupport) [ "pillow_jxl" ];
pythonRemoveDeps = lib.optional (!withJXLSupport) "pillow_jxl";
pythonRelaxDeps = [
"numpy"
"pillow"
@@ -96,7 +96,6 @@ python3Packages.buildPythonApplication {
numpy
opencv-python
pillow
pillow-avif-plugin
pillow-heif
py7zr
pydantic

View File

@@ -51,7 +51,7 @@ let
# Should be unnecessary once PR is pulled.
# PR: https://github.com/NixOS/nixpkgs/pull/271037
# Issue: https://github.com/NixOS/nixpkgs/issues/149812
wrapGAppsHook
wrapGAppsHook3
];
buildInputs = with pkgs.qt6; [
qtbase
@@ -87,7 +87,7 @@ pkgs.mkShellNoCC {
env = {
QT_QPA_PLATFORM = "wayland;xcb";
UV_NO_SYNC = "1";
UV_NO_SYNC = 1;
UV_PYTHON_DOWNLOADS = "never";
};
@@ -111,7 +111,8 @@ pkgs.mkShellNoCC {
fi
source "''${venv}"/bin/activate
PYTHONPATH=${pythonPath}''${PYTHONPATH:+:}''${PYTHONPATH:-}
PYTHONPATH=${pythonPath}''${PYTHONPATH:+:''${PYTHONPATH}}
export PYTHONPATH
if [ ! -f "''${venv}"/pyproject.toml ] || ! diff --brief pyproject.toml "''${venv}"/pyproject.toml >/dev/null; then
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2

View File

@@ -16,8 +16,7 @@ dependencies = [
"mutagen~=1.47",
"numpy~=2.2",
"opencv_python~=4.11",
"Pillow>=10.2,<=11",
"pillow-avif-plugin~=1.5",
"Pillow>=10.2,<12",
"pillow-heif~=0.22",
"pillow-jxl-plugin~=1.3",
"py7zr==1.0.0",

View File

@@ -10,7 +10,7 @@ from datetime import datetime as dt
from warnings import catch_warnings
import structlog
from PySide6.QtCore import Qt, Signal
from PySide6.QtCore import Qt
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import (
QFrame,
@@ -22,7 +22,6 @@ from PySide6.QtWidgets import (
QWidget,
)
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from tagstudio.core.enums import Theme
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
from tagstudio.core.library.alchemy.fields import (
@@ -51,9 +50,6 @@ logger = structlog.get_logger(__name__)
class FieldContainers(QWidget):
"""The Preview Panel Widget."""
favorite_updated = Signal(bool)
archived_updated = Signal(bool)
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
@@ -131,7 +127,7 @@ class FieldContainers(QWidget):
container_index += 1
container_len += 1
if update_badges:
self.emit_badge_signals({t.id for t in entry_tags})
self.driver.emit_badge_signals({t.id for t in entry_tags})
# Write field container(s)
for index, field in enumerate(entry_fields, start=container_index):
@@ -242,7 +238,7 @@ class FieldContainers(QWidget):
self.driver.selected,
tag_ids=tags,
)
self.emit_badge_signals(tags, emit_on_absent=False)
self.driver.emit_badge_signals(tags, emit_on_absent=False)
def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
"""Update/Create data for a FieldContainer.
@@ -493,16 +489,3 @@ class FieldContainers(QWidget):
result = remove_mb.exec_()
if result == QMessageBox.ButtonRole.ActionRole.value:
callback()
def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):
"""Emit any connected signals for updating badge icons."""
logger.info("[emit_badge_signals] Emitting", tag_ids=tag_ids, emit_on_absent=emit_on_absent)
if TAG_ARCHIVED in tag_ids:
self.archived_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.archived_updated.emit(False) # noqa: FBT003
if TAG_FAVORITE in tag_ids:
self.favorite_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.favorite_updated.emit(False) # noqa: FBT003

View File

@@ -304,6 +304,7 @@ class TagSearchPanel(PanelWidget):
tag_widget.on_edit.disconnect()
tag_widget.on_remove.disconnect()
tag_widget.bg_button.clicked.disconnect()
tag_widget.search_for_tag_action.triggered.disconnect()
tag_id = tag.id
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))

View File

@@ -19,7 +19,6 @@ from xml.etree.ElementTree import Element
import cv2
import numpy as np
import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport]
import py7zr
import py7zr.io
import rarfile

View File

@@ -177,6 +177,9 @@ class QtDriver(DriverMixin, QObject):
SIGTERM = Signal()
favorite_updated = Signal(bool)
archived_updated = Signal(bool)
tag_manager_panel: PanelModal | None = None
color_manager_panel: TagColorManager | None = None
ignore_modal: PanelModal | None = None
@@ -357,8 +360,9 @@ class QtDriver(DriverMixin, QObject):
self.tag_manager_panel = PanelModal(
widget=TagDatabasePanel(self, self.lib),
title=Translations["tag_manager.title"],
done_callback=lambda checked=False,
s=self.selected: self.main_window.preview_panel.set_selection(s, update_preview=False),
done_callback=lambda checked=False: (
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
),
has_save=False,
)
@@ -369,9 +373,9 @@ class QtDriver(DriverMixin, QObject):
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.tag_chosen.connect(
lambda t, s=self.selected: (
self.add_tags_to_selected_callback(t),
self.main_window.preview_panel.set_selection(s),
lambda chosen_tag: (
self.add_tags_to_selected_callback([chosen_tag]),
self.main_window.preview_panel.set_selection(self.selected),
)
)
@@ -559,12 +563,12 @@ class QtDriver(DriverMixin, QObject):
self.main_window.search_field.textChanged.connect(self.update_completions_list)
self.main_window.preview_panel.field_containers_widget.archived_updated.connect(
self.archived_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.ARCHIVED: hidden}, origin_id=0, add_tags=False
)
)
self.main_window.preview_panel.field_containers_widget.favorite_updated.connect(
self.favorite_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.FAVORITE: hidden}, origin_id=0, add_tags=False
)
@@ -800,6 +804,19 @@ class QtDriver(DriverMixin, QObject):
)
)
def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):
"""Emit any connected signals for updating badge icons."""
logger.info("[emit_badge_signals] Emitting", tag_ids=tag_ids, emit_on_absent=emit_on_absent)
if TAG_ARCHIVED in tag_ids:
self.archived_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.archived_updated.emit(False) # noqa: FBT003
if TAG_FAVORITE in tag_ids:
self.favorite_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.favorite_updated.emit(False) # noqa: FBT003
def add_tag_action_callback(self):
panel = BuildTagPanel(self.lib)
self.modal = PanelModal(
@@ -848,9 +865,10 @@ class QtDriver(DriverMixin, QObject):
self.main_window.preview_panel.set_selection(self.selected)
def add_tags_to_selected_callback(self, tag_ids: list[int]):
selected = self.selected
selected: list[int] = self.selected
self.main_window.thumb_layout.add_tags(selected, tag_ids)
self.lib.add_tags_to_entries(selected, tag_ids)
self.emit_badge_signals(tag_ids)
def delete_files_callback(self, origin_path: str | Path, origin_id: int | None = None):
"""Callback to send on or more files to the system trash.

View File

@@ -1,12 +1,13 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import SIGNAL
from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.mixed.tag_search import TagSearchPanel
from tagstudio.qt.mixed.tag_widget import TagWidget
from tagstudio.qt.ts_qt import QtDriver
def test_update_tags(qtbot: QtBot, library: Library):
@@ -17,3 +18,35 @@ def test_update_tags(qtbot: QtBot, library: Library):
# When
panel.update_tags()
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver, library: Library):
panel = TagSearchPanel(library)
qtbot.addWidget(panel)
panel.driver = qt_driver
# Set the widget
tags = library.tags
panel.set_tag_widget(tags[0], 0)
tag_widget: TagWidget = panel.scroll_layout.itemAt(0).widget()
should_replace_actions = {
tag_widget: ["on_edit()", "on_remove()"],
tag_widget.bg_button: ["clicked()"],
tag_widget.search_for_tag_action: ["triggered()"],
}
# Ensure each action has been set
ensure_one_receiver_per_action(should_replace_actions)
# Set the widget again
panel.set_tag_widget(tags[0], 0)
# Ensure each action has been replaced (amount of receivers is still 1)
ensure_one_receiver_per_action(should_replace_actions)
def ensure_one_receiver_per_action(should_replace_actions):
for action, signal_strings in should_replace_actions.items():
for signal_str in signal_strings:
assert action.receivers(SIGNAL(signal_str)) == 1