diff --git a/faststack/app.py b/faststack/app.py index 36aae2b..e3b8333 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -20,10 +20,6 @@ # Must set before importing PySide6 os.environ["QT_LOGGING_RULES"] = "qt.qpa.mime.warning=false" -# Type Aliases for readability -DeletePair = Tuple[Optional[Path], Optional[Path]] # (src_path, recycle_bin_path) -DeleteRecord = Tuple[DeletePair, DeletePair] # (jpg_pair, raw_pair) - import concurrent.futures import threading import subprocess @@ -41,7 +37,7 @@ QMimeData, Qt, QPoint, - QCoreApplication, + QCoreApplication, # noqa: F401 — patched by tests ) from PySide6.QtWidgets import QApplication, QFileDialog from PySide6.QtQml import QQmlApplicationEngine @@ -52,7 +48,10 @@ from faststack.config import config from faststack.logging_setup import setup_logging from faststack.models import ImageFile, DecodedImage -from faststack.io.indexer import find_images, find_images_with_variants +from faststack.io.indexer import ( + find_images, + find_images_with_variants, +) # noqa: F401 — find_images patched by tests from faststack.io.variants import ( VariantGroup, build_badge_list, @@ -6554,10 +6553,15 @@ def execute_crop(self): ) # Coerce elements to int and clamp to [0, 1000] - l, t, r, b = [max(0, min(1000, int(x))) for x in crop_box_raw] + left, top, right, bottom = [max(0, min(1000, int(x))) for x in crop_box_raw] # Ensure correct order (left <= right, top <= bottom) - crop_box_raw = (min(l, r), min(t, b), max(l, r), max(t, b)) + crop_box_raw = ( + min(left, right), + min(top, bottom), + max(left, right), + max(top, bottom), + ) except (ValueError, TypeError, AttributeError) as e: log.warning("Invalid crop box format: %s", e) @@ -7536,11 +7540,7 @@ def _collect_active_bins(self) -> set: if p.exists() and p.is_dir() and p not in pending_bins } local_bin = self.image_dir / "image recycle bin" - if ( - local_bin.exists() - and local_bin.is_dir() - and local_bin not in pending_bins - ): + if local_bin.exists() and local_bin.is_dir() and local_bin not in pending_bins: active.add(local_bin) return active @@ -7696,9 +7696,8 @@ def _record_stale(record: DeleteRecord) -> bool: """True if any recycled path in this record was restored.""" (_, jpg_bin), (_, raw_bin) = record return ( - (jpg_bin is not None and jpg_bin.resolve() in resolved_restored) - or (raw_bin is not None and raw_bin.resolve() in resolved_restored) - ) + jpg_bin is not None and jpg_bin.resolve() in resolved_restored + ) or (raw_bin is not None and raw_bin.resolve() in resolved_restored) self.delete_history = [ r for r in self.delete_history if not _record_stale(r) diff --git a/faststack/config.py b/faststack/config.py index 1d9a3d4..c586f74 100644 --- a/faststack/config.py +++ b/faststack/config.py @@ -1,11 +1,11 @@ """Manages application configuration via an INI file.""" import configparser -import logging -import sys import glob +import logging import os import re +import sys from pathlib import PureWindowsPath from faststack.logging_setup import get_app_data_dir diff --git a/faststack/deletion_types.py b/faststack/deletion_types.py index 69dce5c..7d0ddf8 100644 --- a/faststack/deletion_types.py +++ b/faststack/deletion_types.py @@ -6,13 +6,11 @@ import threading from dataclasses import dataclass, field +from enum import Enum from pathlib import Path from typing import Any, List, Optional, Tuple -from enum import Enum - - class DeletionErrorCodes(str, Enum): """Standardized error codes for deletion failures.""" diff --git a/faststack/imaging/cache.py b/faststack/imaging/cache.py index 430c489..6ac0353 100644 --- a/faststack/imaging/cache.py +++ b/faststack/imaging/cache.py @@ -2,10 +2,11 @@ import inspect import logging +import threading +import time from pathlib import Path from typing import Any, Callable, Optional, Union -import time -import threading + from cachetools import Cache, LRUCache log = logging.getLogger(__name__) diff --git a/faststack/imaging/editor.py b/faststack/imaging/editor.py index a340ae8..8871648 100644 --- a/faststack/imaging/editor.py +++ b/faststack/imaging/editor.py @@ -9,23 +9,23 @@ import time import uuid from pathlib import Path -from typing import Optional, Dict, Any, List, Tuple +from typing import Any, Dict, List, Optional, Tuple import numpy as np -from PIL import Image, ImageFilter, ImageOps, ExifTags +from PIL import ExifTags, Image, ImageFilter, ImageOps -from faststack.models import DecodedImage from faststack.imaging.math_utils import ( - _srgb_to_linear, - _linear_to_srgb, - _smoothstep01, - _apply_headroom_shoulder, _analyze_highlight_state, - _lerp, - _highlight_recover_linear, + _apply_headroom_shoulder, _highlight_boost_linear, + _highlight_recover_linear, + _lerp, + _linear_to_srgb, + _smoothstep01, + _srgb_to_linear, ) -from faststack.imaging.orientation import get_exif_orientation, apply_orientation_to_np +from faststack.imaging.orientation import apply_orientation_to_np, get_exif_orientation +from faststack.models import DecodedImage try: from PySide6.QtGui import QImage @@ -34,7 +34,6 @@ from faststack.imaging.optional_deps import cv2 - log = logging.getLogger(__name__) # Aspect Ratios for cropping diff --git a/faststack/imaging/math_utils.py b/faststack/imaging/math_utils.py index 1cac080..a186a76 100644 --- a/faststack/imaging/math_utils.py +++ b/faststack/imaging/math_utils.py @@ -1,6 +1,7 @@ -import numpy as np from typing import Optional +import numpy as np + # ---------------------------- # sRGB ↔ Linear Conversion Helpers # ---------------------------- @@ -200,10 +201,6 @@ def _highlight_recover_linear( # Use max-channel as brightness metric - handles saturated highlights better than luminance brightness = rgb_linear.max(axis=2) - # Build smooth highlight mask: 0 below pivot, 1 in highlights - # Use headroom_ceiling instead of 1.0 for the normalization range - mask = _smoothstep01((brightness - pivot) / (headroom_ceiling - pivot + eps)) - # Highlights recovery: we want to pull down highlights to reveal detail. # Rational compression formula: y = x / (1 + kx). # We apply this relative to the pivot. diff --git a/faststack/imaging/metadata.py b/faststack/imaging/metadata.py index 35f1cd8..be044cb 100644 --- a/faststack/imaging/metadata.py +++ b/faststack/imaging/metadata.py @@ -3,7 +3,8 @@ from fractions import Fraction from pathlib import Path from typing import Any, Dict, Optional, Union -from PIL import Image, ExifTags + +from PIL import ExifTags, Image log = logging.getLogger(__name__) diff --git a/faststack/imaging/orientation.py b/faststack/imaging/orientation.py index edc7605..692c54c 100644 --- a/faststack/imaging/orientation.py +++ b/faststack/imaging/orientation.py @@ -3,6 +3,7 @@ import logging from pathlib import Path from typing import Optional + import numpy as np from PIL import Image diff --git a/faststack/imaging/prefetch.py b/faststack/imaging/prefetch.py index 583d1e3..a0bbe2a 100644 --- a/faststack/imaging/prefetch.py +++ b/faststack/imaging/prefetch.py @@ -1,19 +1,19 @@ """Handles prefetching and decoding of adjacent images in a background thread pool.""" -import logging -import os -import io import hashlib +import io +import logging import mmap -from pathlib import Path -from concurrent.futures import Future -from typing import List, Dict, Optional, Callable +import os import threading import time - +from concurrent.futures import Future +from pathlib import Path +from typing import Callable, Dict, List, Optional import numpy as np -from PIL import Image as PILImage, ImageCms +from PIL import Image as PILImage +from PIL import ImageCms try: from PySide6.QtCore import QTimer @@ -22,11 +22,11 @@ QTimer = None QImage = None -from faststack.models import ImageFile, DecodedImage -from faststack.imaging.jpeg import decode_jpeg_rgb, decode_jpeg_resized +from faststack.config import config from faststack.imaging.cache import build_cache_key +from faststack.imaging.jpeg import decode_jpeg_resized, decode_jpeg_rgb from faststack.imaging.orientation import apply_orientation_to_np -from faststack.config import config +from faststack.models import DecodedImage, ImageFile from faststack.util.executors import create_daemon_threadpool_executor log = logging.getLogger(__name__) @@ -103,6 +103,7 @@ def _make_raw_placeholder(width: int, height: int) -> np.ndarray: return np.array(img) + # ---- Option C: ICC Color Management Setup ---- SRGB_PROFILE = ImageCms.createProfile("sRGB") diff --git a/faststack/imaging/turbo.py b/faststack/imaging/turbo.py index a9742f3..006d381 100644 --- a/faststack/imaging/turbo.py +++ b/faststack/imaging/turbo.py @@ -10,7 +10,7 @@ log = logging.getLogger(__name__) try: - from turbojpeg import TurboJPEG, TJPF_RGB + from turbojpeg import TJPF_RGB, TurboJPEG except ImportError: # pragma: no cover - exercised via create_turbojpeg TurboJPEG = None TJPF_RGB = None diff --git a/faststack/io/deletion.py b/faststack/io/deletion.py index 64ac168..79a7ef8 100644 --- a/faststack/io/deletion.py +++ b/faststack/io/deletion.py @@ -2,8 +2,8 @@ import logging from pathlib import Path -from PySide6.QtWidgets import QMessageBox +from PySide6.QtWidgets import QMessageBox log = logging.getLogger(__name__) diff --git a/faststack/io/indexer.py b/faststack/io/indexer.py index 059ee60..2deeedb 100644 --- a/faststack/io/indexer.py +++ b/faststack/io/indexer.py @@ -5,15 +5,15 @@ import re import time from pathlib import Path -from typing import List, Dict, Tuple +from typing import Dict, List, Tuple -from faststack.models import ImageFile from faststack.io.variants import ( VariantGroup, build_variant_map, norm_path, parse_variant_stem, ) +from faststack.models import ImageFile log = logging.getLogger(__name__) diff --git a/faststack/io/sidecar.py b/faststack/io/sidecar.py index 4daba69..251ceff 100644 --- a/faststack/io/sidecar.py +++ b/faststack/io/sidecar.py @@ -8,10 +8,12 @@ from typing import Literal, Optional, Union, overload from faststack.io.indexer import JPG_EXTENSIONS, RAW_EXTENSIONS -from faststack.models import Sidecar, EntryMetadata +from faststack.models import EntryMetadata, Sidecar log = logging.getLogger(__name__) -KNOWN_IMAGE_EXTENSIONS = frozenset(ext.lower() for ext in JPG_EXTENSIONS | RAW_EXTENSIONS) +KNOWN_IMAGE_EXTENSIONS = frozenset( + ext.lower() for ext in JPG_EXTENSIONS | RAW_EXTENSIONS +) def _entrymetadata_from_json(meta: dict) -> EntryMetadata: diff --git a/faststack/io/variants.py b/faststack/io/variants.py index 1be67ee..af02b7a 100644 --- a/faststack/io/variants.py +++ b/faststack/io/variants.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Tuple + from faststack.io.utils import normalize_path_key as norm_path # Token-boundary regex: match `-developed` as a real dash-delimited token. diff --git a/faststack/models.py b/faststack/models.py index e36d322..c83c5fb 100644 --- a/faststack/models.py +++ b/faststack/models.py @@ -2,7 +2,7 @@ import dataclasses from pathlib import Path -from typing import Any, Optional, Dict, List +from typing import Any, Dict, List, Optional @dataclasses.dataclass diff --git a/faststack/qml/Main.qml b/faststack/qml/Main.qml index d7ff313..e84b987 100644 --- a/faststack/qml/Main.qml +++ b/faststack/qml/Main.qml @@ -353,7 +353,7 @@ ApplicationWindow { if (uiState && uiState.isGridViewActive) return "" var ratio = zs / fs if (Math.abs(ratio - 1.0) < 0.03) return "Zoom: Fit to window (" + Math.round(zs * 100) + "%)" - return "Zoom: " + Math.round(ratio * 100) + "%" + return "Zoom: " + Math.round(zs * 100) + "%" } } diff --git a/faststack/tests/benchmark_decode.py b/faststack/tests/benchmark_decode.py index 29cbd51..6022463 100644 --- a/faststack/tests/benchmark_decode.py +++ b/faststack/tests/benchmark_decode.py @@ -1,8 +1,10 @@ -import time import io +import time + import numpy as np from PIL import Image -from faststack.imaging.jpeg import decode_jpeg_resized, TURBO_AVAILABLE + +from faststack.imaging.jpeg import TURBO_AVAILABLE, decode_jpeg_resized def create_test_jpeg(width=6000, height=4000): diff --git a/faststack/tests/benchmark_decode_bilinear.py b/faststack/tests/benchmark_decode_bilinear.py index 6b5b069..22a8cf3 100644 --- a/faststack/tests/benchmark_decode_bilinear.py +++ b/faststack/tests/benchmark_decode_bilinear.py @@ -1,13 +1,15 @@ -import time import io +import time + import numpy as np from PIL import Image + from faststack.imaging.jpeg import ( - decode_jpeg_rgb, - _get_turbojpeg_scaling_factor, - TURBO_AVAILABLE, JPEG_DECODER, TJPF_RGB, + TURBO_AVAILABLE, + _get_turbojpeg_scaling_factor, + decode_jpeg_rgb, ) diff --git a/faststack/tests/check_imports.py b/faststack/tests/check_imports.py index 7e27361..cb69a43 100644 --- a/faststack/tests/check_imports.py +++ b/faststack/tests/check_imports.py @@ -1,34 +1,39 @@ -import sys +import importlib import os +import sys import traceback -# Add current directory to path -sys.path.append(os.getcwd()) - -try: - print("Importing faststack.app...") - import faststack.app - print("Success faststack.app") -except ImportError as e: - print(f"ImportError faststack.app: {e}") +def check_import(module_name: str) -> bool: + """Try importing a module and print the result.""" + try: + print(f"Importing {module_name}...") + importlib.import_module(module_name) + print(f"Success {module_name}") + return True + except ImportError as e: + print(f"ImportError {module_name}: {e}") + traceback.print_exc() + return False + except Exception as e: + print(f"Non-ImportError during import of {module_name}: {e}") + traceback.print_exc() + return False - traceback.print_exc() -except Exception as e: - print(f"Non-ImportError during import of faststack.app: {e}") - traceback.print_exc() +def main() -> None: + # Add current directory to path + sys.path.append(os.getcwd()) -try: - print("Importing faststack.tests.test_raw_pipeline...") - import faststack.tests.test_raw_pipeline + failures = [] + for module in ["faststack.app", "faststack.tests.test_raw_pipeline"]: + if not check_import(module): + failures.append(module) - print("Success test_raw_pipeline") -except ImportError as e: - print(f"ImportError test_raw_pipeline: {e}") + if failures: + print(f"\nFailed imports: {', '.join(failures)}") + sys.exit(1) - traceback.print_exc() -except Exception as e: - print(f"Non-ImportError during import of test_raw_pipeline: {e}") - traceback.print_exc() +if __name__ == "__main__": + main() diff --git a/faststack/tests/debug_app_init.py b/faststack/tests/debug_app_init.py index 6c06fbc..f1a3fc8 100644 --- a/faststack/tests/debug_app_init.py +++ b/faststack/tests/debug_app_init.py @@ -1,9 +1,10 @@ -import pytest -from unittest.mock import MagicMock, patch +import sys from pathlib import Path -from faststack.app import AppController +from unittest.mock import MagicMock, patch + from PySide6.QtWidgets import QApplication -import sys + +from faststack.app import AppController # Ensure QApplication exists before AppController is imported/used if not QApplication.instance(): @@ -24,9 +25,10 @@ def test_app_init_only(): ): # Create QApplication instance - from PySide6.QtWidgets import QApplication import sys + from PySide6.QtWidgets import QApplication + if not QApplication.instance(): _ = QApplication(sys.argv) else: @@ -34,7 +36,7 @@ def test_app_init_only(): mock_engine = MagicMock() try: - app = AppController(Path("."), mock_engine) + _app = AppController(Path("."), mock_engine) print("AppController instantiated successfully") except Exception as e: print(f"AppController instantiation failed: {e}") diff --git a/faststack/tests/debug_editor_error.py b/faststack/tests/debug_editor_error.py index 6ffb2d4..bde987e 100644 --- a/faststack/tests/debug_editor_error.py +++ b/faststack/tests/debug_editor_error.py @@ -1,12 +1,11 @@ import unittest +from pathlib import Path from unittest.mock import MagicMock, patch + import numpy as np -from pathlib import Path -import sys # Ensure faststack is in path from faststack.imaging.editor import ImageEditor -from PIL import Image class TestDebugError(unittest.TestCase): diff --git a/faststack/tests/debug_metadata.py b/faststack/tests/debug_metadata.py index 6e94086..5b2df78 100644 --- a/faststack/tests/debug_metadata.py +++ b/faststack/tests/debug_metadata.py @@ -1,8 +1,9 @@ +import json import sys from pathlib import Path from unittest.mock import MagicMock, patch + from PIL import ExifTags -import json # Add parent directory to path to import faststack sys.path.append(str(Path(__file__).parent.parent)) diff --git a/faststack/tests/manual_test_error_handling.py b/faststack/tests/manual_test_error_handling.py index 4edb2dd..eeca08a 100644 --- a/faststack/tests/manual_test_error_handling.py +++ b/faststack/tests/manual_test_error_handling.py @@ -1,8 +1,9 @@ +import logging import sys +from pathlib import Path from unittest.mock import MagicMock, patch + import numpy as np -from pathlib import Path -import logging # Configure logging to swallow output logging.basicConfig(level=logging.CRITICAL) diff --git a/faststack/tests/mini_test.py b/faststack/tests/mini_test.py index 2e30d13..dc2df16 100644 --- a/faststack/tests/mini_test.py +++ b/faststack/tests/mini_test.py @@ -1,7 +1,7 @@ -import sys -from unittest.mock import MagicMock, patch import os +import sys import tempfile +from unittest.mock import MagicMock, patch print("START_TEST") try: diff --git a/faststack/tests/repro_futures_cleanup.py b/faststack/tests/repro_futures_cleanup.py index c39960a..20abddc 100644 --- a/faststack/tests/repro_futures_cleanup.py +++ b/faststack/tests/repro_futures_cleanup.py @@ -1,10 +1,8 @@ -import unittest -from unittest.mock import MagicMock -from concurrent.futures import Future, ThreadPoolExecutor -import threading -import time import sys +import unittest +from concurrent.futures import Future from pathlib import Path +from unittest.mock import MagicMock # Mock config sys.modules["faststack.config"] = MagicMock() diff --git a/faststack/tests/reproduce_exif_bug.py b/faststack/tests/reproduce_exif_bug.py index cbfce5f..884ae74 100644 --- a/faststack/tests/reproduce_exif_bug.py +++ b/faststack/tests/reproduce_exif_bug.py @@ -1,5 +1,5 @@ -import sys import os +import sys from unittest.mock import MagicMock # Add parent directory to path for standalone execution @@ -10,6 +10,7 @@ import unittest from unittest.mock import patch + from PIL import Image from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_auto_levels.py b/faststack/tests/test_auto_levels.py index 114380c..240d642 100644 --- a/faststack/tests/test_auto_levels.py +++ b/faststack/tests/test_auto_levels.py @@ -1,5 +1,6 @@ import numpy as np from PIL import Image + from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_cache.py b/faststack/tests/test_cache.py index 6e0c7cf..490b691 100644 --- a/faststack/tests/test_cache.py +++ b/faststack/tests/test_cache.py @@ -100,9 +100,10 @@ class MockBuffer: def test_get_decoded_image_size_fallback_default(): """Tests fallback when metadata is missing (should default to 4).""" - from faststack.imaging.cache import get_decoded_image_size from types import SimpleNamespace + from faststack.imaging.cache import get_decoded_image_size + class MockBuffer: pass diff --git a/faststack/tests/test_cache_invalidation.py b/faststack/tests/test_cache_invalidation.py index 2b481cf..6c78903 100644 --- a/faststack/tests/test_cache_invalidation.py +++ b/faststack/tests/test_cache_invalidation.py @@ -1,8 +1,9 @@ -import sys import os -import time import shutil +import sys +import time from pathlib import Path + import numpy as np from PIL import Image @@ -68,7 +69,7 @@ def test_cache_stability(): # Cleanup try: shutil.rmtree(test_dir) - except: + except OSError: pass diff --git a/faststack/tests/test_cache_replacement_callback.py b/faststack/tests/test_cache_replacement_callback.py index e70f449..64f915f 100644 --- a/faststack/tests/test_cache_replacement_callback.py +++ b/faststack/tests/test_cache_replacement_callback.py @@ -1,6 +1,7 @@ """Tests for ByteLRUCache eviction callbacks.""" import threading + from faststack.imaging.cache import ByteLRUCache diff --git a/faststack/tests/test_config_setters.py b/faststack/tests/test_config_setters.py index 7c84f1c..e10899e 100644 --- a/faststack/tests/test_config_setters.py +++ b/faststack/tests/test_config_setters.py @@ -1,10 +1,8 @@ import unittest from unittest.mock import MagicMock, patch -import sys # Important: Do NOT mock sys.modules at the top level. # This causes pollution that breaks other tests (like test_cache_invalidation.py). - from faststack.app import AppController diff --git a/faststack/tests/test_delete_worker_edge_cases.py b/faststack/tests/test_delete_worker_edge_cases.py index 58e7c52..ca2f74d 100644 --- a/faststack/tests/test_delete_worker_edge_cases.py +++ b/faststack/tests/test_delete_worker_edge_cases.py @@ -1,7 +1,7 @@ import threading -from unittest.mock import MagicMock, patch -import pytest from pathlib import Path +from unittest.mock import patch + from faststack.app import AppController from faststack.deletion_types import DeletionErrorCodes diff --git a/faststack/tests/test_deletion_perf_structure.py b/faststack/tests/test_deletion_perf_structure.py index 2b195d6..a8f8902 100644 --- a/faststack/tests/test_deletion_perf_structure.py +++ b/faststack/tests/test_deletion_perf_structure.py @@ -1,14 +1,13 @@ -import pytest -import os -from unittest.mock import MagicMock, call, patch +import sys from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from PySide6.QtWidgets import QApplication from faststack.app import AppController from faststack.models import ImageFile -from PySide6.QtWidgets import QApplication -import sys - # Ensure QApplication exists before AppController is imported/used in tests if not QApplication.instance(): _qapp = QApplication(sys.argv) @@ -121,9 +120,7 @@ def test_evict_paths_windows_handling(): def test_model_hashing_no_resolve(): """Verify PathResolver and ThumbnailModel do NOT call resolve().""" - from faststack.thumbnail_view.model import ThumbnailModel from faststack.thumbnail_view.provider import PathResolver - from faststack.models import ImageFile as ModelImageFile # Mock Path.resolve to raise exception with patch( diff --git a/faststack/tests/test_deletion_unification.py b/faststack/tests/test_deletion_unification.py index 7f63ac2..0d8bc85 100644 --- a/faststack/tests/test_deletion_unification.py +++ b/faststack/tests/test_deletion_unification.py @@ -1,9 +1,11 @@ -import pytest -from unittest.mock import Mock, patch from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + from faststack.app import AppController -from faststack.models import ImageFile from faststack.deletion_types import DeletionErrorCodes +from faststack.models import ImageFile @pytest.fixture(scope="session") diff --git a/faststack/tests/test_developed_sorting.py b/faststack/tests/test_developed_sorting.py index 8d03a27..ff3d5db 100644 --- a/faststack/tests/test_developed_sorting.py +++ b/faststack/tests/test_developed_sorting.py @@ -1,4 +1,5 @@ import os + from faststack.io.indexer import find_images diff --git a/faststack/tests/test_editor.py b/faststack/tests/test_editor.py index da812e3..aa589a3 100644 --- a/faststack/tests/test_editor.py +++ b/faststack/tests/test_editor.py @@ -1,5 +1,6 @@ import os import unittest + from PIL import Image try: @@ -25,9 +26,9 @@ def abs_val(x): class TestEditor(unittest.TestCase): def test_save_image_preserves_mtime(self): + import shutil import tempfile from pathlib import Path - import shutil tmp_dir = tempfile.mkdtemp() try: @@ -58,9 +59,9 @@ def test_save_image_preserves_mtime(self): def test_texture_edit(self): editor = ImageEditor() + import shutil import tempfile from pathlib import Path - import shutil try: import cv2 diff --git a/faststack/tests/test_editor_error_handling.py b/faststack/tests/test_editor_error_handling.py index 550c60e..36c5baa 100644 --- a/faststack/tests/test_editor_error_handling.py +++ b/faststack/tests/test_editor_error_handling.py @@ -1,8 +1,9 @@ import sys import unittest +from pathlib import Path from unittest.mock import MagicMock, patch + import numpy as np -from pathlib import Path # We need to mock cv2 before importing editor if it's not already imported, # but since tests run in the same process, we just rely on patching. diff --git a/faststack/tests/test_editor_integration.py b/faststack/tests/test_editor_integration.py index ebba82d..058d301 100644 --- a/faststack/tests/test_editor_integration.py +++ b/faststack/tests/test_editor_integration.py @@ -1,7 +1,7 @@ +import sys import unittest -from unittest.mock import MagicMock, patch from pathlib import Path -import sys +from unittest.mock import MagicMock, patch # Ensure we can import faststack sys.path.append(str(Path(__file__).parents[2])) diff --git a/faststack/tests/test_editor_lifecycle_and_safety.py b/faststack/tests/test_editor_lifecycle_and_safety.py index 48b745f..23022ad 100644 --- a/faststack/tests/test_editor_lifecycle_and_safety.py +++ b/faststack/tests/test_editor_lifecycle_and_safety.py @@ -1,7 +1,7 @@ +import sys import unittest -from unittest.mock import MagicMock, patch from pathlib import Path -import sys +from unittest.mock import MagicMock, patch # Ensure we can import faststack sys.path.append(str(Path(__file__).parents[2])) @@ -38,7 +38,7 @@ def setUp(self): self.mock_qapp.instance.return_value.aboutToQuit.connect = MagicMock() # Instantiate AppController - with patch("faststack.app.ImageEditor") as mock_editor_cls: + with patch("faststack.app.ImageEditor") as _mock_editor_cls: self.controller = AppController(Path("."), self.mock_engine) self.mock_editor_instance = self.controller.image_editor diff --git a/faststack/tests/test_editor_loading.py b/faststack/tests/test_editor_loading.py index 56c8755..7a992ce 100644 --- a/faststack/tests/test_editor_loading.py +++ b/faststack/tests/test_editor_loading.py @@ -6,12 +6,13 @@ patch sys.modules['cv2'] before the import happens. """ +import os import sys +import tempfile import unittest from unittest.mock import MagicMock, patch + import numpy as np -import tempfile -import os class TestImageLoadingFallback(unittest.TestCase): diff --git a/faststack/tests/test_editor_no_copy.py b/faststack/tests/test_editor_no_copy.py index 925f7ab..686e62c 100644 --- a/faststack/tests/test_editor_no_copy.py +++ b/faststack/tests/test_editor_no_copy.py @@ -1,8 +1,8 @@ import hashlib -import numpy as np -from pathlib import Path from unittest.mock import MagicMock, patch +import numpy as np + from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_editor_rotation.py b/faststack/tests/test_editor_rotation.py index 3756778..21b0b5a 100644 --- a/faststack/tests/test_editor_rotation.py +++ b/faststack/tests/test_editor_rotation.py @@ -1,11 +1,13 @@ -import pytest import math + import numpy as np +import pytest from PIL import Image + from faststack.imaging.editor import ( + ImageEditor, _rotated_rect_with_max_area, rotate_autocrop_rgb, - ImageEditor, ) diff --git a/faststack/tests/test_executable_validator.py b/faststack/tests/test_executable_validator.py index ec15f63..3bb40d1 100644 --- a/faststack/tests/test_executable_validator.py +++ b/faststack/tests/test_executable_validator.py @@ -1,12 +1,12 @@ """Tests for executable path validation.""" from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from faststack.io.executable_validator import ( - validate_executable_path, _is_executable, _is_subpath, + validate_executable_path, ) diff --git a/faststack/tests/test_executor_shutdown.py b/faststack/tests/test_executor_shutdown.py index 0f79a23..3c29e2f 100644 --- a/faststack/tests/test_executor_shutdown.py +++ b/faststack/tests/test_executor_shutdown.py @@ -1,11 +1,12 @@ -import time -import threading import concurrent.futures +import threading +import time + import pytest + from faststack.util.executors import ( - create_priority_executor, create_daemon_threadpool_executor, - PriorityExecutor, + create_priority_executor, ) diff --git a/faststack/tests/test_exif_compat.py b/faststack/tests/test_exif_compat.py index ee0c6c2..56de654 100644 --- a/faststack/tests/test_exif_compat.py +++ b/faststack/tests/test_exif_compat.py @@ -1,9 +1,10 @@ -import unittest -from unittest.mock import MagicMock, patch import sys +import unittest from pathlib import Path -from PIL import Image, ExifTags +from unittest.mock import MagicMock, patch + import numpy as np +from PIL import ExifTags, Image # Ensure project root is in sys.path project_root = str(Path(__file__).parents[1]) @@ -137,7 +138,7 @@ def test_save_uses_sanitizer_for_sidecar(self): return_value=Path("test-backup.jpg"), ), patch("PIL.Image.fromarray") as mock_fromarray, - patch.object(self.editor, "_write_tiff_16bit") as mock_tiff, + patch.object(self.editor, "_write_tiff_16bit") as _mock_tiff, ): mock_img = MagicMock() mock_fromarray.return_value = mock_img diff --git a/faststack/tests/test_exif_display_rotation.py b/faststack/tests/test_exif_display_rotation.py index afdda65..ea21092 100644 --- a/faststack/tests/test_exif_display_rotation.py +++ b/faststack/tests/test_exif_display_rotation.py @@ -1,13 +1,13 @@ """Tests for EXIF orientation correction during display.""" -import sys import shutil +import sys import tempfile import unittest from pathlib import Path import numpy as np -from PIL import Image, ExifTags +from PIL import ExifTags, Image # Adjust path to import faststack sys.path.insert(0, str(Path(__file__).parents[1])) diff --git a/faststack/tests/test_exif_orientation.py b/faststack/tests/test_exif_orientation.py index 45bfbe4..f944237 100644 --- a/faststack/tests/test_exif_orientation.py +++ b/faststack/tests/test_exif_orientation.py @@ -2,9 +2,9 @@ import tempfile import unittest from pathlib import Path -from PIL import Image, ExifTags from unittest.mock import MagicMock, patch -import sys + +from PIL import ExifTags, Image from faststack.imaging.editor import ImageEditor @@ -164,7 +164,7 @@ def test_raw_mode_exif_preservation(self): # Current logic: We ALWAYS sanitize to 1 because we bake orientation on load. # This prevents "double rotation". - res = self.editor.save_image(write_developed_jpg=True) + self.editor.save_image(write_developed_jpg=True) developed_path = Path(self.test_dir) / "working_source-developed.jpg" with Image.open(developed_path) as dev: diff --git a/faststack/tests/test_fallback_blur.py b/faststack/tests/test_fallback_blur.py index 2c98e38..90e5b08 100644 --- a/faststack/tests/test_fallback_blur.py +++ b/faststack/tests/test_fallback_blur.py @@ -1,7 +1,8 @@ import unittest -import numpy as np from unittest.mock import patch +import numpy as np + # Import the functionality to test from faststack.imaging import editor diff --git a/faststack/tests/test_feedback_fixes.py b/faststack/tests/test_feedback_fixes.py index d1f6f23..1f27118 100644 --- a/faststack/tests/test_feedback_fixes.py +++ b/faststack/tests/test_feedback_fixes.py @@ -1,11 +1,13 @@ -import pytest -import numpy as np from pathlib import Path + +import numpy as np +import pytest + from faststack.imaging.cache import ByteLRUCache -from faststack.models import DecodedImage from faststack.imaging.editor import ImageEditor -from faststack.io.variants import build_variant_map, norm_path from faststack.io.indexer import find_images_with_variants +from faststack.io.variants import build_variant_map, norm_path +from faststack.models import DecodedImage def test_cache_evict_callback_payload(): diff --git a/faststack/tests/test_file_locking.py b/faststack/tests/test_file_locking.py index 137163d..bb9bf73 100644 --- a/faststack/tests/test_file_locking.py +++ b/faststack/tests/test_file_locking.py @@ -1,10 +1,10 @@ """Tests for file locking handling in undo operations.""" +import shutil +import tempfile import unittest -from unittest.mock import MagicMock, patch from pathlib import Path -import tempfile -import shutil +from unittest.mock import MagicMock, patch class TestRestoreBackupSafe(unittest.TestCase): diff --git a/faststack/tests/test_generation_aware_preview.py b/faststack/tests/test_generation_aware_preview.py index 17be800..6d5ff63 100644 --- a/faststack/tests/test_generation_aware_preview.py +++ b/faststack/tests/test_generation_aware_preview.py @@ -1,11 +1,12 @@ -import unittest -from unittest.mock import MagicMock -from PySide6.QtGui import QImage +import os # Import the class to test (assuming it's importable) # We might need to mock imports if they depend on full Qt app structure import sys -import os +import unittest +from unittest.mock import MagicMock + +from PySide6.QtGui import QImage # Adjust path to allow imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -49,7 +50,7 @@ def test_matching_generation(self): self.mock_controller._last_rendered_preview_gen = 5 # Request with matching generation - img = self.provider.requestImage("0/5", None, None) + _img = self.provider.requestImage("0/5", None, None) # Should be the preview (dark gray placeholder if fails, but here we mocked QImage creation?) # Wait, requestImage creates a QImage from the buffer. diff --git a/faststack/tests/test_handle_failures.py b/faststack/tests/test_handle_failures.py index 3b6cbe5..b5428bf 100644 --- a/faststack/tests/test_handle_failures.py +++ b/faststack/tests/test_handle_failures.py @@ -6,10 +6,10 @@ from PySide6.QtWidgets import QApplication from faststack.deletion_types import ( - DeletionErrorCodes, DeleteFailure, DeleteJob, DeleteResult, + DeletionErrorCodes, ) # Ensure QApplication exists before importing/using Qt classes diff --git a/faststack/tests/test_handle_failures_isolated.py b/faststack/tests/test_handle_failures_isolated.py index 6c634e0..067cc03 100644 --- a/faststack/tests/test_handle_failures_isolated.py +++ b/faststack/tests/test_handle_failures_isolated.py @@ -1,11 +1,11 @@ -import sys -from unittest.mock import MagicMock, patch from pathlib import Path +from unittest.mock import MagicMock, patch + from faststack.deletion_types import ( - DeletionErrorCodes, - DeleteResult, DeleteFailure, DeleteJob, + DeleteResult, + DeletionErrorCodes, ) # Mocks for global functions that might be called diff --git a/faststack/tests/test_headroom_semantics.py b/faststack/tests/test_headroom_semantics.py index e73ab23..9b82541 100644 --- a/faststack/tests/test_headroom_semantics.py +++ b/faststack/tests/test_headroom_semantics.py @@ -1,9 +1,10 @@ -import unittest -import numpy as np -import sys import os +import sys +import unittest from unittest.mock import MagicMock +import numpy as np + # Mock cv2/turbojpeg sys.modules["cv2"] = MagicMock() sys.modules["turbojpeg"] = MagicMock() diff --git a/faststack/tests/test_helicon_cleanup.py b/faststack/tests/test_helicon_cleanup.py index c8f246c..500a6cb 100644 --- a/faststack/tests/test_helicon_cleanup.py +++ b/faststack/tests/test_helicon_cleanup.py @@ -1,8 +1,10 @@ -import pytest -from unittest.mock import MagicMock, patch -from pathlib import Path -import tempfile import os +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + from faststack.app import AppController from faststack.models import ImageFile diff --git a/faststack/tests/test_highlight_recovery.py b/faststack/tests/test_highlight_recovery.py index f01a548..bf338d1 100644 --- a/faststack/tests/test_highlight_recovery.py +++ b/faststack/tests/test_highlight_recovery.py @@ -6,8 +6,8 @@ - Handles both 16-bit (headroom) and 8-bit (JPEG) sources """ -import sys import os +import sys # Add parent directory to path for standalone execution sys.path.insert( @@ -22,14 +22,15 @@ sys.modules["cv2"] = mock.MagicMock() -import numpy as np import time +import numpy as np + from faststack.imaging.math_utils import ( - _highlight_recover_linear, - _highlight_boost_linear, - _apply_headroom_shoulder, _analyze_highlight_state, + _apply_headroom_shoulder, + _highlight_boost_linear, + _highlight_recover_linear, ) diff --git a/faststack/tests/test_highlight_state_normalization.py b/faststack/tests/test_highlight_state_normalization.py index 9876e48..dd04a51 100644 --- a/faststack/tests/test_highlight_state_normalization.py +++ b/faststack/tests/test_highlight_state_normalization.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import MagicMock + from faststack.ui.provider import UIState diff --git a/faststack/tests/test_highlights_responsiveness.py b/faststack/tests/test_highlights_responsiveness.py index 944c80e..2847054 100644 --- a/faststack/tests/test_highlights_responsiveness.py +++ b/faststack/tests/test_highlights_responsiveness.py @@ -1,9 +1,10 @@ -import unittest -import numpy as np -import sys import os +import sys +import unittest from unittest.mock import MagicMock +import numpy as np + # Mock dependencies sys.modules["cv2"] = MagicMock() sys.modules["turbojpeg"] = MagicMock() diff --git a/faststack/tests/test_highlights_v2.py b/faststack/tests/test_highlights_v2.py index 58f2801..3632856 100644 --- a/faststack/tests/test_highlights_v2.py +++ b/faststack/tests/test_highlights_v2.py @@ -1,11 +1,12 @@ -import unittest -import numpy as np +import os # Adjust import path if necessary, but faststack is likely installed or in pythonpath import sys -import os +import unittest from unittest.mock import MagicMock +import numpy as np + # Mock cv2 before importing faststack modules that depend on it sys.modules["cv2"] = MagicMock() sys.modules["turbojpeg"] = MagicMock() @@ -42,7 +43,7 @@ def test_analysis_decoupling(self): # Create a linear image with some headroom linear = np.ones((100, 100, 3), dtype=np.float32) * 1.2 # sRGB mock indicating some clipping (e.g. 255) - srgb = np.ones((100, 100, 3), dtype=np.uint8) * 255 + _srgb = np.ones((100, 100, 3), dtype=np.uint8) * 255 # Setup editor state to simulate the image being loaded # We need this because _apply_edits works on self.float_image/preview logic usually, @@ -84,8 +85,8 @@ def test_robust_ceiling(self): # The hot pixel should be compressed but not NaN self.assertLess(out[50, 50, 0], 1000.0) except Exception: - import traceback import sys + import traceback traceback.print_exc(file=sys.__stderr__) raise diff --git a/faststack/tests/test_jump_to_last_uploaded.py b/faststack/tests/test_jump_to_last_uploaded.py index 80e21da..e445986 100644 --- a/faststack/tests/test_jump_to_last_uploaded.py +++ b/faststack/tests/test_jump_to_last_uploaded.py @@ -1,8 +1,10 @@ -import pytest -from unittest.mock import Mock, patch from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + from faststack.app import AppController -from faststack.models import ImageFile, EntryMetadata +from faststack.models import EntryMetadata, ImageFile @pytest.fixture(scope="session") diff --git a/faststack/tests/test_loupe_delete.py b/faststack/tests/test_loupe_delete.py index 9e5f894..fb60ded 100644 --- a/faststack/tests/test_loupe_delete.py +++ b/faststack/tests/test_loupe_delete.py @@ -1,7 +1,8 @@ -import pytest -from unittest.mock import Mock, patch -from pathlib import Path from dataclasses import dataclass +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest from faststack.app import AppController diff --git a/faststack/tests/test_metadata.py b/faststack/tests/test_metadata.py index 530627e..39dd0b6 100644 --- a/faststack/tests/test_metadata.py +++ b/faststack/tests/test_metadata.py @@ -1,9 +1,11 @@ import unittest -from unittest.mock import MagicMock, patch from pathlib import Path -from faststack.imaging.metadata import get_exif_data, clean_exif_value, get_exif_brief +from unittest.mock import MagicMock, patch + from PIL import ExifTags +from faststack.imaging.metadata import clean_exif_value, get_exif_brief, get_exif_data + class TestMetadata(unittest.TestCase): @patch("pathlib.Path.exists", return_value=True) diff --git a/faststack/tests/test_new_features.py b/faststack/tests/test_new_features.py index 2b9fa87..6c96abf 100644 --- a/faststack/tests/test_new_features.py +++ b/faststack/tests/test_new_features.py @@ -1,6 +1,8 @@ import unittest + import numpy as np from PIL import Image + from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_pairing.py b/faststack/tests/test_pairing.py index 991544a..33cdf13 100644 --- a/faststack/tests/test_pairing.py +++ b/faststack/tests/test_pairing.py @@ -7,7 +7,7 @@ import pytest -from faststack.io.indexer import find_images, _find_raw_pair +from faststack.io.indexer import _find_raw_pair, find_images @pytest.fixture diff --git a/faststack/tests/test_permanent_delete.py b/faststack/tests/test_permanent_delete.py index 85203bb..6097884 100644 --- a/faststack/tests/test_permanent_delete.py +++ b/faststack/tests/test_permanent_delete.py @@ -5,8 +5,8 @@ # Import the standalone module, avoiding heavy app imports from faststack.io.deletion import ( - ensure_recycle_bin_dir, confirm_permanent_delete, + ensure_recycle_bin_dir, permanently_delete_image_files, ) diff --git a/faststack/tests/test_prefetch_concurrency.py b/faststack/tests/test_prefetch_concurrency.py index 6ee94d1..59f8360 100644 --- a/faststack/tests/test_prefetch_concurrency.py +++ b/faststack/tests/test_prefetch_concurrency.py @@ -1,8 +1,9 @@ import threading import time -import pytest from pathlib import Path +import pytest + from faststack.imaging.prefetch import Prefetcher diff --git a/faststack/tests/test_prefetch_logic.py b/faststack/tests/test_prefetch_logic.py index d5004cb..211319c 100644 --- a/faststack/tests/test_prefetch_logic.py +++ b/faststack/tests/test_prefetch_logic.py @@ -1,7 +1,7 @@ +import sys import unittest -from unittest.mock import MagicMock from concurrent.futures import Future -import sys +from unittest.mock import MagicMock # Mock config before importing prefetcher sys.modules["faststack.config"] = MagicMock() diff --git a/faststack/tests/test_raw_pipeline.py b/faststack/tests/test_raw_pipeline.py index 02f845b..affafb6 100644 --- a/faststack/tests/test_raw_pipeline.py +++ b/faststack/tests/test_raw_pipeline.py @@ -1,13 +1,14 @@ -import unittest -from unittest.mock import MagicMock, patch -from pathlib import Path -import tempfile +import logging import shutil import subprocess +import tempfile +import unittest +from dataclasses import dataclass +from pathlib import Path +from unittest.mock import MagicMock, patch + import numpy as np from PIL import Image -from dataclasses import dataclass -import logging from faststack.app import AppController from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_reactive_delete.py b/faststack/tests/test_reactive_delete.py index c4aad5d..5cb63f1 100644 --- a/faststack/tests/test_reactive_delete.py +++ b/faststack/tests/test_reactive_delete.py @@ -1,13 +1,15 @@ -import pytest -import time -from unittest.mock import MagicMock, patch, Mock from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest + from faststack.models import ImageFile @pytest.fixture def app_controller(tmp_path): from PySide6.QtCore import QCoreApplication + from faststack.app import AppController app = QCoreApplication.instance() @@ -101,7 +103,7 @@ def test_async_delete_completion(app_controller): # 1. Enqueue app_controller.delete_current_image() - future = app_controller._delete_executor.submit.return_value + _future = app_controller._delete_executor.submit.return_value # 2. Simulate worker side-effects recycle_bin = (app_controller.image_dir / "image recycle bin").resolve() diff --git a/faststack/tests/test_recycle_bin_tracking.py b/faststack/tests/test_recycle_bin_tracking.py index 521a721..36b6f1d 100644 --- a/faststack/tests/test_recycle_bin_tracking.py +++ b/faststack/tests/test_recycle_bin_tracking.py @@ -1,5 +1,7 @@ -import pytest from unittest.mock import MagicMock, patch + +import pytest + from faststack.app import AppController diff --git a/faststack/tests/test_refresh_crash.py b/faststack/tests/test_refresh_crash.py index 5b4b2fb..4261ba0 100644 --- a/faststack/tests/test_refresh_crash.py +++ b/faststack/tests/test_refresh_crash.py @@ -1,6 +1,7 @@ -import pytest -from pathlib import Path from unittest.mock import Mock, patch + +import pytest + from faststack.thumbnail_view import ThumbnailModel diff --git a/faststack/tests/test_refresh_optimization.py b/faststack/tests/test_refresh_optimization.py index ebba5dc..5d05619 100644 --- a/faststack/tests/test_refresh_optimization.py +++ b/faststack/tests/test_refresh_optimization.py @@ -1,6 +1,7 @@ -import pytest -from pathlib import Path from unittest.mock import Mock, patch + +import pytest + from faststack.app import AppController diff --git a/faststack/tests/test_rolloff.py b/faststack/tests/test_rolloff.py index e8ce307..0c2f5ec 100644 --- a/faststack/tests/test_rolloff.py +++ b/faststack/tests/test_rolloff.py @@ -1,4 +1,5 @@ import unittest + import numpy as np # We check for modules that might be missing and mock them if needed diff --git a/faststack/tests/test_rotation_unittest.py b/faststack/tests/test_rotation_unittest.py index dc7da15..c9f09b3 100644 --- a/faststack/tests/test_rotation_unittest.py +++ b/faststack/tests/test_rotation_unittest.py @@ -1,5 +1,7 @@ import unittest + import numpy as np + from faststack.imaging.editor import ImageEditor diff --git a/faststack/tests/test_sensitivity.py b/faststack/tests/test_sensitivity.py index 8096fd1..f8a82c0 100644 --- a/faststack/tests/test_sensitivity.py +++ b/faststack/tests/test_sensitivity.py @@ -1,6 +1,7 @@ -import numpy as np -import sys import os +import sys + +import numpy as np # Add parent directory to sys.path to allow importing faststack sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) diff --git a/faststack/tests/test_sidecar.py b/faststack/tests/test_sidecar.py index bd0a28e..298fddd 100644 --- a/faststack/tests/test_sidecar.py +++ b/faststack/tests/test_sidecar.py @@ -334,7 +334,7 @@ def test_string_with_path_separator_is_path_normalized(mock_sidecar_dir): d = mock_sidecar_dir() sm = SidecarManager(d, None) - meta = sm.get_metadata("subdir/photo.CR2") + sm.get_metadata("subdir/photo.CR2") expected_key = sm.metadata_key_for_path(Path("subdir/photo.CR2")) assert expected_key in sm.data.entries diff --git a/faststack/tests/test_startup_opt.py b/faststack/tests/test_startup_opt.py index 754be19..ebbfe81 100644 --- a/faststack/tests/test_startup_opt.py +++ b/faststack/tests/test_startup_opt.py @@ -1,6 +1,7 @@ -import pytest -from pathlib import Path from unittest.mock import Mock, patch + +import pytest + from faststack.app import AppController diff --git a/faststack/tests/test_startup_optimization.py b/faststack/tests/test_startup_optimization.py index 732a99a..35bef17 100644 --- a/faststack/tests/test_startup_optimization.py +++ b/faststack/tests/test_startup_optimization.py @@ -1,9 +1,8 @@ import sys -import os from pathlib import Path from unittest.mock import MagicMock, patch -import pytest +import pytest from PySide6.QtWidgets import QApplication # We mock AppController dependencies here @@ -13,7 +12,7 @@ @pytest.fixture(scope="session", autouse=True) def qapplication(): if not QApplication.instance(): - app = QApplication(sys.argv) + _app = QApplication(sys.argv) yield QApplication.instance() diff --git a/faststack/tests/test_thumbnail_ready_emits_datachanged.py b/faststack/tests/test_thumbnail_ready_emits_datachanged.py index 6413338..e1c8aaa 100644 --- a/faststack/tests/test_thumbnail_ready_emits_datachanged.py +++ b/faststack/tests/test_thumbnail_ready_emits_datachanged.py @@ -1,12 +1,12 @@ """Test that _on_thumbnail_ready correctly emits dataChanged for the matching row.""" from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest # Minimal Qt imports needed for the model -from PySide6.QtCore import Qt, QModelIndex, QCoreApplication +from PySide6.QtCore import QCoreApplication @pytest.fixture(scope="session") @@ -21,7 +21,7 @@ def qapp(): @pytest.fixture def thumbnail_model(qapp): """Create a ThumbnailModel with fake entries for testing.""" - from faststack.thumbnail_view.model import ThumbnailModel, ThumbnailEntry + from faststack.thumbnail_view.model import ThumbnailEntry, ThumbnailModel model = ThumbnailModel( base_directory=Path("/fake/dir"), diff --git a/faststack/tests/test_turbo.py b/faststack/tests/test_turbo.py index 6e90094..2192565 100644 --- a/faststack/tests/test_turbo.py +++ b/faststack/tests/test_turbo.py @@ -69,9 +69,7 @@ def fake_decoder(path=None): assert decoder is None assert available is False - warning_records = [ - r for r in caplog.records if r.levelno == logging.WARNING - ] + warning_records = [r for r in caplog.records if r.levelno == logging.WARNING] assert len(warning_records) == 1 assert "Falling back to Pillow" in warning_records[0].message assert "3 location(s) tried" in warning_records[0].message @@ -94,9 +92,7 @@ def fake_decoder(path=None): with caplog.at_level(logging.DEBUG): turbo.create_turbojpeg() - debug_records = [ - r for r in caplog.records if r.levelno == logging.DEBUG - ] + debug_records = [r for r in caplog.records if r.levelno == logging.DEBUG] debug_text = " ".join(r.message for r in debug_records) assert "default loader" in debug_text assert "C:/one/turbojpeg.dll" in debug_text @@ -114,9 +110,7 @@ def test_missing_turbojpeg_package_emits_one_warning(monkeypatch, caplog): assert decoder is None assert available is False - warning_records = [ - r for r in caplog.records if r.levelno == logging.WARNING - ] + warning_records = [r for r in caplog.records if r.levelno == logging.WARNING] assert len(warning_records) == 1 assert "PyTurboJPEG not found" in warning_records[0].message assert "Pillow" in warning_records[0].message diff --git a/faststack/tests/test_ui_prefetch_safety.py b/faststack/tests/test_ui_prefetch_safety.py index f8f0492..9a0616a 100644 --- a/faststack/tests/test_ui_prefetch_safety.py +++ b/faststack/tests/test_ui_prefetch_safety.py @@ -1,6 +1,7 @@ import unittest -from unittest.mock import MagicMock, patch from pathlib import Path +from unittest.mock import MagicMock + from faststack.ui.provider import UIState diff --git a/faststack/tests/test_ui_state_recycle.py b/faststack/tests/test_ui_state_recycle.py index e759b42..144bf2d 100644 --- a/faststack/tests/test_ui_state_recycle.py +++ b/faststack/tests/test_ui_state_recycle.py @@ -1,6 +1,5 @@ -import pytest from unittest.mock import Mock -from pathlib import Path + from faststack.ui.provider import UIState diff --git a/faststack/tests/test_undo_refactor.py b/faststack/tests/test_undo_refactor.py index a80e1cc..f624545 100644 --- a/faststack/tests/test_undo_refactor.py +++ b/faststack/tests/test_undo_refactor.py @@ -1,12 +1,14 @@ -import pytest -from unittest.mock import MagicMock, patch import shutil +from unittest.mock import MagicMock, patch + +import pytest # Create a dummy fixture for AppController that uses the real class but mocks dependencies @pytest.fixture def app_controller(tmp_path): from PySide6.QtCore import QCoreApplication + from faststack.app import AppController # Ensure QCoreApplication exists diff --git a/faststack/tests/test_variants.py b/faststack/tests/test_variants.py index 9e46ad0..f492996 100644 --- a/faststack/tests/test_variants.py +++ b/faststack/tests/test_variants.py @@ -1,20 +1,14 @@ """Tests for variant (backup + developed) parsing, grouping, and integration.""" -import os -import pytest from pathlib import Path -from unittest.mock import MagicMock from faststack.io.variants import ( - parse_variant_stem, - build_variant_map, build_badge_list, + build_variant_map, norm_path, - VariantInfo, - VariantGroup, + parse_variant_stem, ) - # ── parse_variant_stem ────────────────────────────────────────────────────── diff --git a/faststack/tests/test_variants_logic.py b/faststack/tests/test_variants_logic.py index 193282f..5870a8d 100644 --- a/faststack/tests/test_variants_logic.py +++ b/faststack/tests/test_variants_logic.py @@ -1,7 +1,8 @@ -import unittest import os +import unittest from pathlib import Path -from faststack.io.variants import build_variant_map, VariantGroup, norm_path + +from faststack.io.variants import build_variant_map, norm_path class TestVariantsLogic(unittest.TestCase): diff --git a/faststack/tests/test_version_sort.py b/faststack/tests/test_version_sort.py index e57b72a..e4abf0a 100644 --- a/faststack/tests/test_version_sort.py +++ b/faststack/tests/test_version_sort.py @@ -1,5 +1,5 @@ -import unittest import re +import unittest from pathlib import PureWindowsPath diff --git a/faststack/tests/thumbnail_view/test_folder_stats.py b/faststack/tests/thumbnail_view/test_folder_stats.py index fb36a10..2220377 100644 --- a/faststack/tests/thumbnail_view/test_folder_stats.py +++ b/faststack/tests/thumbnail_view/test_folder_stats.py @@ -1,17 +1,19 @@ """Tests for folder_stats module.""" import json + import pytest + from faststack.thumbnail_view.folder_stats import ( FolderStats, - read_folder_stats, - clear_stats_cache, - clear_raw_count_cache, - _stats_cache, - _scan_folder_files, _compute_coverage_buckets, + _scan_folder_files, + _stats_cache, + clear_raw_count_cache, + clear_stats_cache, count_images_in_folder, get_file_counts_by_extension, + read_folder_stats, ) diff --git a/faststack/tests/thumbnail_view/test_model.py b/faststack/tests/thumbnail_view/test_model.py index 7cdd83a..d9d0e50 100644 --- a/faststack/tests/thumbnail_view/test_model.py +++ b/faststack/tests/thumbnail_view/test_model.py @@ -1,9 +1,8 @@ # faststack/thumbnail_view/model.py from __future__ import annotations -import os import sys -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Iterable, Mapping, Optional @@ -242,7 +241,6 @@ def refresh(self) -> None: Rebuild the entries list based on filesystem + filters. """ cur = self.current_directory.resolve() - base = self.base_directory.resolve() folders: list[ThumbnailEntry] = [] files: list[ThumbnailEntry] = [] diff --git a/faststack/tests/thumbnail_view/test_prefetcher_priority.py b/faststack/tests/thumbnail_view/test_prefetcher_priority.py index 46b7629..21c563d 100644 --- a/faststack/tests/thumbnail_view/test_prefetcher_priority.py +++ b/faststack/tests/thumbnail_view/test_prefetcher_priority.py @@ -1,7 +1,9 @@ import time -from unittest.mock import MagicMock, patch -import pytest from pathlib import Path +from unittest.mock import patch + +import pytest + from faststack.thumbnail_view.prefetcher import ThumbnailCache, ThumbnailPrefetcher diff --git a/faststack/tests/thumbnail_view/test_provider_logic.py b/faststack/tests/thumbnail_view/test_provider_logic.py index 09dfe69..1f16584 100644 --- a/faststack/tests/thumbnail_view/test_provider_logic.py +++ b/faststack/tests/thumbnail_view/test_provider_logic.py @@ -1,16 +1,17 @@ """Tests for _parse_id logic and cache-hit decode recovery in ThumbnailProvider.""" -import pytest from pathlib import Path from unittest.mock import MagicMock +import pytest +from PySide6.QtCore import QSize + +from faststack.thumbnail_view.prefetcher import ThumbnailCache from faststack.thumbnail_view.provider import ( - ThumbnailProvider, - PLACEHOLDER_COLOR, ERROR_COLOR, + PLACEHOLDER_COLOR, + ThumbnailProvider, ) -from faststack.thumbnail_view.prefetcher import ThumbnailCache -from PySide6.QtCore import QSize class TestProviderLogic: diff --git a/faststack/tests/thumbnail_view/test_reason_tracking.py b/faststack/tests/thumbnail_view/test_reason_tracking.py index 6aae26c..d545828 100644 --- a/faststack/tests/thumbnail_view/test_reason_tracking.py +++ b/faststack/tests/thumbnail_view/test_reason_tracking.py @@ -1,9 +1,10 @@ """Tests for thumbnail source reason tracking in ThumbnailModel.""" from pathlib import Path + import pytest -from PySide6.QtCore import Qt, QModelIndex -from faststack.thumbnail_view.model import ThumbnailModel, ThumbnailEntry + +from faststack.thumbnail_view.model import ThumbnailEntry, ThumbnailModel @pytest.fixture @@ -95,6 +96,7 @@ def test_clear_next_source_reason_method(model): def test_refresh_sets_deferred_clear(model): """refresh() should NOT synchronously clear reason.""" from unittest.mock import patch + from faststack.models import ImageFile with patch("faststack.thumbnail_view.model.find_images") as mock_find: diff --git a/faststack/tests/thumbnail_view/test_selection.py b/faststack/tests/thumbnail_view/test_selection.py index 6406721..153dc1b 100644 --- a/faststack/tests/thumbnail_view/test_selection.py +++ b/faststack/tests/thumbnail_view/test_selection.py @@ -2,7 +2,7 @@ import pytest -from faststack.thumbnail_view.model import ThumbnailModel, ThumbnailEntry +from faststack.thumbnail_view.model import ThumbnailEntry, ThumbnailModel @pytest.fixture diff --git a/faststack/tests/thumbnail_view/test_thumbnail_ready.py b/faststack/tests/thumbnail_view/test_thumbnail_ready.py index f93347b..64bc55b 100644 --- a/faststack/tests/thumbnail_view/test_thumbnail_ready.py +++ b/faststack/tests/thumbnail_view/test_thumbnail_ready.py @@ -1,11 +1,9 @@ """Test that _on_thumbnail_ready only fires for matching size IDs.""" import sys -from pathlib import Path from unittest.mock import MagicMock import pytest - from PySide6.QtCore import QCoreApplication from faststack.io.utils import compute_path_hash diff --git a/faststack/thumbnail_view/__init__.py b/faststack/thumbnail_view/__init__.py index b6c85ec..6cd99ff 100644 --- a/faststack/thumbnail_view/__init__.py +++ b/faststack/thumbnail_view/__init__.py @@ -1,9 +1,9 @@ """Thumbnail grid view components for FastStack.""" from .folder_stats import FolderStats, read_folder_stats -from .model import ThumbnailModel, ThumbnailEntry -from .prefetcher import ThumbnailPrefetcher, ThumbnailCache -from .provider import ThumbnailProvider, PathResolver +from .model import ThumbnailEntry, ThumbnailModel +from .prefetcher import ThumbnailCache, ThumbnailPrefetcher +from .provider import PathResolver, ThumbnailProvider __all__ = [ "FolderStats", diff --git a/faststack/thumbnail_view/model.py b/faststack/thumbnail_view/model.py index 50c175a..342fbd8 100644 --- a/faststack/thumbnail_view/model.py +++ b/faststack/thumbnail_view/model.py @@ -1,27 +1,25 @@ """ThumbnailModel for QML GridView with file/folder entries.""" -import hashlib import logging import os import time from bisect import bisect_left from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Set, Callable +from typing import Callable, Dict, List, Optional, Set from PySide6.QtCore import ( QAbstractListModel, QModelIndex, + Qt, QThread, QTimer, - Qt, Signal, Slot, ) -from faststack.models import ImageFile -from faststack.io.utils import compute_path_hash, normalize_path_key from faststack.io.indexer import find_images +from faststack.io.utils import compute_path_hash, normalize_path_key from faststack.thumbnail_view.folder_stats import ( FolderStats, count_images_in_folder, @@ -411,7 +409,9 @@ def refresh(self): meta = self._get_metadata(img.path) if not isinstance(meta, dict): # Ensure it's a dict before .get() - log.debug("Metadata for %s is not a dict: %r", img.path, meta) + log.debug( + "Metadata for %s is not a dict: %r", img.path, meta + ) continue if all(meta.get(flag, False) for flag in flags): @@ -630,9 +630,7 @@ def _add_images_to_entries( is_favorite = meta.get("favorite", False) is_todo = meta.get("todo", False) else: - log.debug( - "Metadata for %s is not a dict: %r", img.path, meta - ) + log.debug("Metadata for %s is not a dict: %r", img.path, meta) except Exception as e: log.debug("Error getting metadata for %s: %s", img.path, e) diff --git a/faststack/thumbnail_view/prefetcher.py b/faststack/thumbnail_view/prefetcher.py index afae214..bd69377 100644 --- a/faststack/thumbnail_view/prefetcher.py +++ b/faststack/thumbnail_view/prefetcher.py @@ -2,33 +2,34 @@ import logging import os +import threading import time from collections import OrderedDict from concurrent.futures import Future from pathlib import Path from threading import Lock -import threading -from typing import Dict, Optional, Set, Tuple, Callable, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple if TYPE_CHECKING: from faststack.imaging.cache import ByteLRUCache -import numpy as np import io -from PIL import Image from contextlib import nullcontext +import numpy as np +from PIL import Image + +import faststack.util.thumb_debug as thumb_debug +from faststack.imaging.orientation import apply_orientation_to_np, get_exif_orientation from faststack.imaging.turbo import TJPF_RGB, create_turbojpeg -from faststack.util.executors import create_priority_executor -from faststack.imaging.orientation import get_exif_orientation, apply_orientation_to_np from faststack.io.utils import compute_path_hash -import faststack.util.thumb_debug as thumb_debug +from faststack.util.executors import create_priority_executor log = logging.getLogger(__name__) # Optional Qt dispatch so callbacks always run on Qt thread when available try: - from PySide6.QtCore import QObject, Signal, Qt, QCoreApplication + from PySide6.QtCore import QCoreApplication, QObject, Qt, Signal class _ReadyEmitter(QObject): ready = Signal(str) diff --git a/faststack/thumbnail_view/provider.py b/faststack/thumbnail_view/provider.py index d27ccf5..d684955 100644 --- a/faststack/thumbnail_view/provider.py +++ b/faststack/thumbnail_view/provider.py @@ -2,21 +2,20 @@ import logging import time -from urllib.parse import unquote from pathlib import Path -from typing import TYPE_CHECKING, Optional, NamedTuple - -import faststack.util.thumb_debug as thumb_debug +from typing import TYPE_CHECKING, NamedTuple, Optional +from urllib.parse import unquote from PySide6.QtCore import QSize -from PySide6.QtGui import QImage, QColor +from PySide6.QtGui import QColor, QImage from PySide6.QtQuick import QQuickImageProvider +import faststack.util.thumb_debug as thumb_debug from faststack.io.utils import compute_path_hash if TYPE_CHECKING: from faststack.thumbnail_view.model import ThumbnailModel - from faststack.thumbnail_view.prefetcher import ThumbnailPrefetcher, ThumbnailCache + from faststack.thumbnail_view.prefetcher import ThumbnailCache, ThumbnailPrefetcher log = logging.getLogger(__name__) @@ -100,7 +99,7 @@ def _create_placeholder(self, size: int, color: QColor) -> QImage: def _create_folder_placeholder(self, size: int) -> QImage: """Create a folder icon placeholder.""" - from PySide6.QtGui import QPainter, QPen, QBrush + from PySide6.QtGui import QBrush, QPainter, QPen image = QImage(size, size, QImage.Format.Format_RGB888) image.fill(FOLDER_COLOR) diff --git a/faststack/ui/keystrokes.py b/faststack/ui/keystrokes.py index 7807528..21ed5af 100644 --- a/faststack/ui/keystrokes.py +++ b/faststack/ui/keystrokes.py @@ -1,5 +1,6 @@ # faststack/ui/keystrokes.py import logging + from PySide6.QtCore import Qt log = logging.getLogger(__name__) diff --git a/faststack/ui/provider.py b/faststack/ui/provider.py index d0c93bb..9f74476 100644 --- a/faststack/ui/provider.py +++ b/faststack/ui/provider.py @@ -1,15 +1,16 @@ """QML Image Provider and application state bridge.""" -import logging import collections +import logging import threading -from PySide6.QtCore import QObject, Signal, Property, Slot, Qt +from pathlib import Path + +from PySide6.QtCore import Property, QObject, Qt, Signal, Slot from PySide6.QtGui import QImage from PySide6.QtQuick import QQuickImageProvider from faststack.config import config from faststack.imaging.optional_deps import HAS_OPENCV -from pathlib import Path # Try to import QColorSpace if available (Qt 6+) try: diff --git a/faststack/util/executors.py b/faststack/util/executors.py index 880ce81..72bb2cc 100644 --- a/faststack/util/executors.py +++ b/faststack/util/executors.py @@ -12,7 +12,7 @@ import weakref -from concurrent.futures.thread import _worker, _threads_queues +from concurrent.futures.thread import _threads_queues, _worker class DaemonThreadPoolExecutor(ThreadPoolExecutor): diff --git a/faststack/util/thumb_debug.py b/faststack/util/thumb_debug.py index 7a8dfc7..5616356 100644 --- a/faststack/util/thumb_debug.py +++ b/faststack/util/thumb_debug.py @@ -1,12 +1,12 @@ """Thumbnail pipeline debug logging and performance tracing.""" +import itertools import logging -import time import threading -import itertools +import time from contextlib import contextmanager -from typing import Dict, Optional, Any from pathlib import Path +from typing import Any, Dict, Optional log = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 5983085..e6562ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "faststack" version = "1.6.1" authors = [ - { name="Alan Rockefeller"}, + { name = "Alan Rockefeller" }, ] description = "Ultra-fast JPG Viewer for Focus Stacking Selection" readme = "README.md" @@ -25,13 +25,15 @@ dependencies = [ "cachetools>=5.0,<6.0", "watchdog>=4.0,<5.0", "Pillow>=10.0,<11.0", - # CRITICAL FIX: OpenCV 4.10+ is required for NumPy 2.0 binary compatibility + # OpenCV 4.10+ might be required for NumPy 2.0 binary compatibility "opencv-python>=4.10.0,<5.0", ] [project.optional-dependencies] dev = [ "pytest>=8.0,<9.0", + "ruff>=0.13,<0.14", + "black>=25.0,<26.0", "build", "twine", ] @@ -54,8 +56,34 @@ minversion = "8.0" testpaths = ["faststack/tests"] python_files = ["test_*.py"] -# FIX: Add import-mode=importlib to fix resolution errors +# Use importlib mode to avoid test import-path resolution issues addopts = "--import-mode=importlib -p no:cacheprovider -p no:doctest --basetemp=./var/pytest-temp" norecursedirs = ["var", ".venv", "cache", "faststack.egg-info", "__pycache__"] pythonpath = ["."] + + +[tool.black] +line-length = 88 +target-version = ["py312"] + +[tool.ruff] +line-length = 88 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"faststack/app.py" = ["E402", "I001", "F401"] +"faststack/tests/run_loading_tests.py" = ["E402"] +"faststack/tests/repro_futures_cleanup.py" = ["E402"] +"faststack/tests/reproduce_exif_bug.py" = ["E402"] +"faststack/tests/test_exif_compat.py" = ["E402"] +"faststack/tests/test_headroom_semantics.py" = ["E402"] +"faststack/tests/test_highlights_responsiveness.py" = ["E402"] +"faststack/tests/test_highlights_v2.py" = ["E402"] +"faststack/tests/test_prefetch_logic.py" = ["E402"] +"faststack/util/executors.py" = ["E402"] +"faststack/tests/test_highlight_recovery.py" = ["F401"]