Skip to content

Refine grid view and fix lots of bugs#37

Merged
AlanRockefeller merged 2 commits intomainfrom
test
Jan 29, 2026
Merged

Refine grid view and fix lots of bugs#37
AlanRockefeller merged 2 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Jan 29, 2026

Summary by CodeRabbit

  • New Features

    • Grid navigation and keyboard controls (including Escape), recycle-bin cleanup dialog, folder statistics with counts/coverage, and metadata flags for upload/edit/restack with timestamps.
  • Bug Fixes

    • JPEG fallback now returns RGB images; improved watcher/startup directory validation.
  • Tests

    • Added extensive tests for grid navigation, recycle/bin operations, permanent delete, undo/refactor, and related behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 29, 2026

Walkthrough

Adds recycle-bin and permanent-delete utilities, folder statistics with coverage buckets and caching, expanded thumbnail/model metadata and navigation, QML grid and tile cursor/selection UX, new UIState signals/properties and keybindings, improvements to pairing/indexing, plus many formatting/test updates.

Changes

Cohort / File(s) Summary
Deletion / Recycle Bin
faststack/io/deletion.py
New module: ensure_recycle_bin_dir, confirm_permanent_delete(s), permanently_delete_image_files — GUI confirmation and permanent-delete logic.
Recycle-bin Tracking & Reactive Delete Tests
faststack/tests/test_recycle_bin_tracking.py, faststack/tests/test_reactive_delete.py, faststack/tests/test_permanent_delete.py, faststack/tests/test_undo_refactor.py
New tests covering active_recycle_bins, move/cleanup, reactive delete fallback, permanent-delete confirmations, and undo/restore edge cases.
Folder Statistics & Thumbnail Model
faststack/thumbnail_view/folder_stats.py, faststack/thumbnail_view/model.py
Added FolderStats fields (jpg_count, raw_count, coverage_buckets), caching and helpers (_scan_folder_files, _compute_coverage_buckets, count_images_in_folder), expanded ThumbnailEntry metadata and navigate_to behavior.
Thumbnail Grid & Tile UI
faststack/qml/ThumbnailGridView.qml, faststack/qml/ThumbnailTile.qml, faststack/qml/Main.qml
Grid focus/navigation redesign, tileHasCursor + visuals (counters, sparkline), right-click selection, new Main.qml close flow with recycleBinCleanupDialog and allowCloseWithRecycleBins property, added grid controls.
UI Provider / State & Keystrokes
faststack/ui/provider.py, faststack/ui/keystrokes.py
Added UIState signals/properties (recycleBinStatsText, hasRecycleBinItems, preloadingStateChanged, gridCanGoBack, gridScrollToIndex), slots (gridGoBack, gridAddSelectionToBatch, gridDeleteAtCursor), keepalive lock, Escape → switch_to_grid_view binding.
Indexing & Pairing
faststack/io/indexer.py
Introduced JPG/RAW extension constants, _DEVELOPED_SUFFIX, casefold-based pairing, developed-JPG handling and improved scanning error handling.
Imaging & Optional Deps
faststack/imaging/jpeg.py, faststack/imaging/optional_deps.py, faststack/imaging/*
HAS_OPENCV boolean exposed; Pillow fallback now converts JPEG to RGB; multiple formatting/import cleanups and minor EXIF orientation handling tweaks.
Watcher / IO Helpers
faststack/io/watcher.py, faststack/io/sidecar.py, faststack/io/helicon.py, faststack/io/executable_validator.py
Watcher start now validates dir, schedules/starts observer; stop clears observer; small formatting and logging refinements across IO helpers.
Models / Metadata
faststack/models.py
EntryMetadata gains uploaded/edited/restacked boolean/date fields; ImageFile annotated as dataclass.
Thumbnail Prefetcher / Provider
faststack/thumbnail_view/prefetcher.py, faststack/thumbnail_view/provider.py
Mostly formatting; minor prefetch and provider drawing tweaks; no behavioral change.
Tests & Test Utils
faststack/tests/*, pyproject.toml
Many test formatting and import cleanups; added create_test_jpeg, new test modules (esc_histogram, permanent_delete, folder_stats tests); pytest pythonpath added.
Formatting / Misc
faststack/check_scipy.py, faststack/config.py, faststack/logging_setup.py, faststack/verify_wb.py, others...
Wide quote/whitespace standardization, minor syntax fixes, and non-functional refactors across the codebase.

Sequence Diagrams

sequenceDiagram
    participant User as User
    participant MainWindow as Main Window
    participant UIState as UIState
    participant AppController as App Controller
    participant FileSystem as File System
    participant Sidecar as Sidecar Manager

    User->>MainWindow: Trigger Delete (Recycle)
    MainWindow->>AppController: request_delete(image)
    AppController->>FileSystem: move files → recycle bin
    FileSystem-->>AppController: move result
    AppController->>Sidecar: record undo entry
    AppController->>UIState: update active_recycle_bins
    UIState-->>MainWindow: hasRecycleBinItems changed
    MainWindow-->>User: show confirmation / UI update
Loading
sequenceDiagram
    participant User as User
    participant MainWindow as Main Window
    participant UIState as UIState
    participant Dialog as Recycle Dialog
    participant AppController as App Controller
    participant FileSystem as File System

    User->>MainWindow: Close window
    MainWindow->>UIState: hasRecycleBinItems?
    UIState-->>MainWindow: true
    MainWindow->>Dialog: show recycleBinCleanupDialog
    alt User confirms cleanup
        Dialog->>AppController: cleanup_recycle_bins()
        AppController->>FileSystem: permanently delete bins
        FileSystem-->>AppController: deleted
        AppController->>UIState: clear active_recycle_bins
        Dialog-->>MainWindow: allow close
    else User cancels
        Dialog-->>MainWindow: block/keep files
    end
Loading
sequenceDiagram
    participant User as User
    participant Grid as Thumbnail Grid
    participant Model as Thumbnail Model
    participant FolderStats as Folder Stats
    participant Prefetcher as Prefetcher

    User->>Grid: navigate_to(path)
    Grid->>Model: navigate_to(path)
    Model->>FolderStats: read_folder_stats(path)
    FolderStats-->>Model: FolderStats(jpg_count, raw_count, buckets)
    Model-->>Grid: refresh entries with folder_stats
    Grid->>Prefetcher: adjust prefetch window
    Prefetcher-->>Grid: decode/cache thumbnails
    Grid-->>User: display grid with overlays
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • Release version 1.5.1 #32: Overlaps on thumbnail/indexer, imaging/editor, config and UI provider changes; likely to touch the same functions and integrations.
  • Version 0.8 #13: Related deletion/undo and recycle-bin flows, AppController and UIState wiring for delete/undo and keybindings.
  • Add grid view mode with image previews #35: Direct overlap on grid/thumbnail UI (ThumbnailGridView/ThumbnailTile) and thumbnail_view modules; likely touching same QML and model interactions.
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: grid view refinements and bug fixes across multiple modules including QML UI components, image handling, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
faststack/tests/test_editor_error_handling.py (1)

14-31: Patch order and target for cv2 will not affect the editor module due to early binding in optional_deps.

faststack.imaging.editor imports cv2 from faststack.imaging.optional_deps at module scope (line 30), which itself performs a try/except import of cv2 at module scope (optional_deps.py lines 3–9). Since ImageEditor is imported before patch.dict(sys.modules, {"cv2": ...}) is applied, the cv2 reference in optional_deps is already bound and won't be affected by later sys.modules patches.

When load_image calls cv2.imread() (line ~454), it uses the already-bound reference from optional_deps, not the patched sys.modules["cv2"].

The PIL.Image.open patch target is correct since Image is imported directly at module scope and used as Image.open().

Apply patch.dict(sys.modules, {"cv2": MagicMock()}) before importing ImageEditor, or patch the module-scoped binding directly at faststack.imaging.optional_deps.cv2:

🔧 Recommended fix
-        from faststack.imaging.editor import ImageEditor
-        editor = ImageEditor()
-
-        with patch("PIL.Image.open", side_effect=OSError("Mocked file error")):
-            with patch.dict(sys.modules, {"cv2": MagicMock()}):
-                sys.modules["cv2"].imread.return_value = None
-                result = editor.load_image("non_existent_file.jpg")
-                self.assertFalse(result)
+        with patch.dict(sys.modules, {"cv2": MagicMock()}):
+            sys.modules["cv2"].imread.return_value = None
+            from faststack.imaging.editor import ImageEditor
+            editor = ImageEditor()
+            with patch("PIL.Image.open", side_effect=OSError("Mocked file error")):
+                result = editor.load_image("non_existent_file.jpg")
+                self.assertFalse(result)
faststack/tests/test_highlight_state_normalization.py (1)

16-34: Fix duplicate test name and make the legacy-key test assertive.

Line 16 redefines the same test name used on Line 34, so the first test is overwritten and never runs. Rename it and add assertions so it enforces the legacy-key behavior.

Proposed fix
-    def test_highlight_state_normalization_standard(self):
-        """Test with standard keys."""
+    def test_highlight_state_normalization_legacy_keys(self):
+        """Test with legacy keys."""
         self.mock_editor._last_highlight_state = {
             "headroom_pct": 0.1,
             "clipped_pct": 0.2,
             "near_white_pct": 0.3,
         }
-        # Controller returns canonical keys using the passed dict (even if they were wrong in backend, provider normalizes?
+        state = self.ui_state.highlightState
+        self.assertEqual(state["headroom_pct"], 0.1)
+        self.assertEqual(state["source_clipped_pct"], 0.0)
+        self.assertEqual(state["current_nearwhite_pct"], 0.0)
faststack/io/watcher.py (1)

76-78: Return a strict bool from is_alive().

The expression self.observer and self.observer.is_alive() returns None when self.observer is None, violating the -> bool type annotation. Use bool() to ensure the return is always a strict bool.

Suggested fix
-        return self.observer and self.observer.is_alive()
+        return bool(self.observer and self.observer.is_alive())
🤖 Fix all issues with AI agents
In `@faststack/tests/check_imports.py`:
- Around line 7-23: Replace the no-op print-only try blocks in check_imports.py
with real imports of the modules (import faststack.app and import
faststack.tests.test_raw_pipeline) and narrow the except clauses to catch
ImportError instead of Exception; in each except block, log the import error and
print the traceback (use traceback.print_exc()) so failures are reported with
details. Ensure the import statements are placed inside the try blocks that
currently reference those modules so the script actually verifies importability.

In `@faststack/tests/test_permanent_delete.py`:
- Around line 14-64: Replace the inlined mock helper functions with tests that
invoke the real AppController methods: instantiate AppController, set its
self.recycle_bin_dir to the tmp_path value before calling
_ensure_recycle_bin_dir(), and assert it returns True/False and that the
directory exists or not; for _confirm_permanent_delete(), patch QMessageBox (or
monkeypatch AppController._show_confirm_dialog) to simulate user clicking
confirm/cancel and call AppController._confirm_permanent_delete(mock_img) to
assert returned bool; if testing _permanently_delete_image_files(), create temp
files, set AppController.recycle_bin_dir, and patch logging/OS operations as
needed to assert correct moves, deletions, and logged errors. Use the actual
method names (_ensure_recycle_bin_dir, _confirm_permanent_delete,
_permanently_delete_image_files), the AppController class, and patching of
QMessageBox/self.recycle_bin_dir to avoid GUI and environment side effects.

In `@faststack/ui/provider.py`:
- Around line 1348-1373: The properties and method (recycleBinStatsText,
hasRecycleBinItems, cleanupRecycleBins) reference self._app_controller which
doesn't exist because the controller is stored as self.app_controller; change
those references to self.app_controller so get_recycle_bin_stats() and
cleanup_recycle_bins() are called on the correct attribute and avoid
AttributeError.
🟡 Minor comments (20)
faststack/tests/test_editor_error_handling.py-48-55 (1)

48-55: Fix patch target for module-scoped Image reference.

The faststack.imaging.editor module imports Image directly with from PIL import Image (line 9), so Image.fromarray() calls use a module-scoped reference. Patching PIL.Image.fromarray won't intercept this; patch faststack.imaging.editor.Image.fromarray instead.

Fix
-            with patch("PIL.Image.fromarray", return_value=mock_img):
+            with patch("faststack.imaging.editor.Image.fromarray", return_value=mock_img):
                 with self.assertRaises(RuntimeError) as cm:
                     editor.save_image()
faststack/tests/reproduce_exif_bug.py-25-44 (1)

25-44: Clarify docstrings in this reproduction script—they contradict the intent.

This file won't be picked up by pytest (doesn't match the test_*.py discovery pattern in pyproject.toml), so it remains a standalone reproduction script. However, the docstrings are misleading: they claim the method "currently drops EXIF data," but the test assertions and inline comments explicitly state "DESIRED BEHAVIOR: It returns the original bytes."

The actual implementation in editor.py (_get_sanitized_exif_bytes, lines 1600–1630) returns None on tobytes failure or missing, which aligns with the assertions in test_exif_compat.py (lines 63–77).

Update the docstrings to remove "currently" or clarify that this reproduces the desired future behavior, not the current behavior. For example, change "Verify that a failure in tobytes() currently drops EXIF data" to "Verify that a failure in tobytes() should return original bytes (desired behavior)."

faststack/test_pil_blur.py-6-29 (1)

6-29: Don't swallow exceptions in test code.

The broad Exception catch only prints to stdout, masking failures. When this test runs (either manually or if pytest configuration changes), failures will silently pass. Either re-raise the exception or use a test assertion.

Proposed fix
     except Exception as e:
         print(f"Failed: {e}")
+        raise
faststack/tests/test_executable_validator.py-64-68 (1)

64-68: Prefix unused error with _ to avoid lint errors.

error is never used in the test functions. Rename to _error (or _) to satisfy RUF059:

  • Line 64 in test_suspicious_path_with_traversal()
  • Line 127 in test_wrong_executable_name_for_type()
Proposed fix
-            is_valid, error = validate_executable_path(suspicious_path)
+            is_valid, _error = validate_executable_path(suspicious_path)
...
-            is_valid, error = validate_executable_path(wrong_exe, app_type="photoshop")
+            is_valid, _error = validate_executable_path(wrong_exe, app_type="photoshop")
faststack/tests/test_executable_validator.py-33-40 (1)

33-40: Fix unused lambda arguments to satisfy Ruff (ARG005).

Rename unused self parameter to _self in three lambda assignments to match Ruff's dummy variable pattern and avoid the unused-lambda-argument violation.

Proposed fix
-        mock_path_instance.__str__ = lambda self: photoshop_path
+        mock_path_instance.__str__ = lambda _self: photoshop_path
-        mock_path_instance.__str__ = lambda self: r"C:\Windows\System32\malware.exe"
+        mock_path_instance.__str__ = lambda _self: r"C:\Windows\System32\malware.exe"
-        mock_path_instance.__str__ = lambda self: wrong_exe
+        mock_path_instance.__str__ = lambda _self: wrong_exe

Also applies to lines 54 and 117.

faststack/tests/mini_test.py-27-40 (1)

27-40: Re-raise exceptions in both test blocks for consistency and visibility.

The inner except Exception as e blocks (lines 20 and 39-40) only print without re-raising. This masks unexpected failures in manual test runs. Additionally, the same pattern appears in Test 1 (lines 19-20) but the suggestion is missing there.

To improve code robustness and consistency:

  • Test 1 (lines 19-20): Add raise after the print
  • Test 2 (lines 39-40): Add raise after the print

When re-raised, unexpected exceptions will propagate to the outer exception handler (lines 45-46) and be clearly reported as "CRASH: {e}", making test failures more visible.

faststack/tests/test_drag_logic.py-7-9 (1)

7-9: Avoid unused parameter to keep tests lint-clean.

current_index is unused on Line 7. If it’s only for signature parity, prefix with _ to satisfy Ruff and keep intent clear.

Proposed fix
-def get_drag_paths(
-    image_files, current_index, existing_indices, current_edit_source_mode
-):
+def get_drag_paths(
+    image_files, _current_index, existing_indices, current_edit_source_mode
+):
faststack/imaging/math_utils.py-80-84 (1)

80-84: Replace the multiplication sign in the docstring to satisfy Ruff.

Line 83 contains H×W, which triggers RUF002 in Ruff.

Proposed fix
-        srgb_u8: Optional uint8 sRGB array (source image) for accurate JPEG clipping detection.
-                 MUST have same H×W dimensions as rgb_linear (or be stride-compatible).
+        srgb_u8: Optional uint8 sRGB array (source image) for accurate JPEG clipping detection.
+                 MUST have same H x W dimensions as rgb_linear (or be stride-compatible).
faststack/tests/test_raw_pipeline.py-33-37 (1)

33-37: Silence unused *args/**kwargs in test helper callbacks.
These are flagged by Ruff; rename to _args/_kwargs.

🧹 Proposed fix
-        def side_effect_start(*args, **kwargs):
+        def side_effect_start(*_args, **_kwargs):
             _, thread_kwargs = mock_thread.call_args
             target = thread_kwargs.get("target")
             if target:
                 target()
-        def side_effect_start(*args, **kwargs):
+        def side_effect_start(*_args, **_kwargs):
             _, thread_kwargs = mock_thread.call_args
             target = thread_kwargs.get("target")
             if target:
                 target()
-        def side_effect_start(*args, **kwargs):
+        def side_effect_start(*_args, **_kwargs):
             _, thread_kwargs = mock_thread.call_args
             target = thread_kwargs.get("target")
             if target:
                 target()

Also applies to: 78-83, 131-136

faststack/tests/test_raw_pipeline.py-260-263 (1)

260-263: Avoid unused backup_path.
Rename to _backup_path to satisfy RUF059.

🧹 Proposed fix
-        saved_path, backup_path = res
+        saved_path, _backup_path = res
faststack/tests/test_editor_lifecycle_and_safety.py-41-44 (1)

41-44: Drop unused mock_editor_cls binding.
Ruff flags this as unused; the patch context works without binding.

🧹 Proposed fix
-        with patch("faststack.app.ImageEditor") as mock_editor_cls:
+        with patch("faststack.app.ImageEditor"):
             self.controller = AppController(Path("."), self.mock_engine)
             self.mock_editor_instance = self.controller.image_editor
faststack/tests/test_raw_pipeline.py-212-229 (1)

212-229: Rename unused mock_run parameter.
Ruff flags it; the patch is still needed, so rename to _mock_run.

🧹 Proposed fix
-    def test_develop_raw_slot(self, mock_config_get, mock_run, mock_exists):
+    def test_develop_raw_slot(self, mock_config_get, _mock_run, mock_exists):
faststack/tests/test_generation_aware_preview.py-50-52 (1)

50-52: Remove unused img assignment.
This triggers Ruff F841 and the initial call is redundant.

🧹 Proposed fix
-        img = self.provider.requestImage("0/5", None, None)
faststack/tests/test_reactive_delete.py-35-36 (1)

35-36: Remove unused tmp_path parameters (Ruff ARG001).

They’re not referenced in either test.

♻️ Proposed cleanup
-def test_reactive_delete_fallback(app_controller, tmp_path):
+def test_reactive_delete_fallback(app_controller):
@@
-def test_reactive_delete_fallback_cancelled(app_controller, tmp_path):
+def test_reactive_delete_fallback_cancelled(app_controller):

Also applies to: 72-73

faststack/tests/test_config_setters.py-14-33 (1)

14-33: Silence Ruff unused-argument warnings in mocks.

name, args, and kwargs are unused and flagged by Ruff. Prefix with _ to keep signatures but avoid lint noise.

🧹 Suggested fix
 class MockQObject:
-    def property(self, name):
+    def property(self, _name):
         return None
 
-    def setProperty(self, name, value):
+    def setProperty(self, _name, _value):
         pass
@@
-def MockSlot(*args, **kwargs):
+def MockSlot(*_args, **_kwargs):
     def decorator(func):
         return func
faststack/io/sidecar.py-13-34 (1)

13-34: Narrow the broad except Exception in metadata parsing.

Catching all exceptions can hide programming errors and is flagged by Ruff (BLE001). Consider validating input type and catching more specific exceptions.

🛡️ Suggested change
 def _entrymetadata_from_json(meta: dict) -> EntryMetadata:
@@
-    try:
+    if not isinstance(meta, dict):
+        log.warning("Error parsing metadata entry: expected dict, got %s", type(meta).__name__)
+        return EntryMetadata()
+    try:
@@
-    except Exception as e:
+    except (TypeError, ValueError) as e:
         log.warning(f"Error parsing metadata entry: {e}")
         return EntryMetadata()
faststack/tests/test_config_setters.py-109-113 (1)

109-113: Remove unused mock_path_cls assignment.

It’s unused and flagged by Ruff. Dropping it keeps the setup clean.

🧹 Suggested fix
-        mock_path_cls = self.patches[
-            -1
-        ].target  # access the mock object ? NO, p.start returns mock
-        # Ideally capture the return of start()
-
         # Simpler: just instantiate. The mocks are active.
faststack/ui/provider.py-139-141 (1)

139-141: Duplicate signal declaration.

preloadingStateChanged is declared twice on lines 139 and 140. Remove the duplicate.

🐛 Proposed fix
     preloadingStateChanged = Signal()
-    preloadingStateChanged = Signal()
     preloadProgressChanged = Signal()
faststack/tests/thumbnail_view/test_folder_stats.py-235-243 (1)

235-243: Avoid the unused variable to keep ruff clean.

jpg_files is unused in this test, which triggers RUF059.

♻️ Suggested tweak
-        jpg_count, raw_count, jpg_files = _scan_folder_files(temp_folder)
+        jpg_count, raw_count, _jpg_files = _scan_folder_files(temp_folder)
faststack/qml/ThumbnailGridView.qml-139-200 (1)

139-200: Add guards for index-sensitive actions to prevent unnecessary backend calls with stale indices.

When the model shrinks (e.g., after deletion), currentIndex can remain stale and exceed the new item count. While the backend methods safely handle out-of-range indices by checking bounds and returning early, guarding at the QML layer prevents unnecessary calls and keeps the code flow clear.

🛡️ Suggested guard for index-sensitive actions
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
-    uiState.gridOpenIndex(thumbnailGrid.currentIndex)
+    if (thumbnailGrid.currentIndex >= 0 && thumbnailGrid.currentIndex < thumbnailGrid.count) {
+        uiState.gridOpenIndex(thumbnailGrid.currentIndex)
+    }
     event.accepted = true
} else if (event.key === Qt.Key_Space) {
-    uiState.gridSelectIndex(thumbnailGrid.currentIndex, false, true)
+    if (thumbnailGrid.currentIndex >= 0 && thumbnailGrid.currentIndex < thumbnailGrid.count) {
+        uiState.gridSelectIndex(thumbnailGrid.currentIndex, false, true)
+    }
     event.accepted = true
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
-    uiState.gridDeleteAtCursor(thumbnailGrid.currentIndex)
+    if (thumbnailGrid.currentIndex >= 0 && thumbnailGrid.currentIndex < thumbnailGrid.count) {
+        uiState.gridDeleteAtCursor(thumbnailGrid.currentIndex)
+    }
     event.accepted = true
}
🧹 Nitpick comments (27)
faststack/tests/debug_metadata.py (1)

58-62: Broad exception catch is acceptable for this debug utility.

The static analysis tool flags BLE001 here, but this is a debug script where catching all exceptions is intentional—the purpose is to capture any failure and write the full traceback to a file for analysis. The production code in faststack/imaging/metadata.py uses the same pattern with an explicit # noqa: BLE001 comment.

If you want to silence the linter, consider adding the same noqa directive:

-        except Exception:
+        except Exception:  # noqa: BLE001 - debug script captures all failures
faststack/tests/test_sidecar.py (1)

16-16: Consider fixing the type hint for PEP 484 compliance.

The parameter content: dict = None uses an implicit Optional, which PEP 484 prohibits. Modern type checkers expect an explicit union with None.

🔧 Proposed fix
-    def _create(content: dict = None):
+    def _create(content: dict | None = None):

As per coding guidelines: Static analysis hint from Ruff (RUF013) - "PEP 484 prohibits implicit Optional".

faststack/tests/manual_test_error_handling.py (1)

23-38: Consider documenting the expected exception flow.

The test expects load_image to raise OSError when PIL.Image.open fails, but Line 26 returns True inside the except OSError block. While this works correctly, the static analysis hint (TRY300) suggests moving success returns to an else block for clarity. This is a minor style preference and not blocking.

Optional: Use else block for success path
                 except OSError as e:
                     if "Mocked file error" in str(e):
-                        print("SUCCESS: load_image raised expected exception")
-                        return True
-                    else:
-                        print(f"FAILURE: load_image raised wrong exception: {e}")
-                        return False
+                        pass  # Expected exception
+                    else:
+                        print(f"FAILURE: load_image raised wrong exception: {e}")
+                        return False
+                else:
+                    print("FAILURE: load_image did NOT raise exception")
+                    return False
+                print("SUCCESS: load_image raised expected exception")
+                return True
faststack/tests/test_auto_levels.py (1)

22-22: Consider using _ prefix for intentionally unused tuple elements.

Static analysis flags blacks and p_low as unused in some tests. While understandable given you're only verifying specific return values, prefixing with _ clarifies intent.

Example for line 22
-    blacks, whites, p_low, p_high = editor.auto_levels(threshold_percent=0.0)
+    _blacks, whites, p_low, p_high = editor.auto_levels(threshold_percent=0.0)

Also applies to: 91-92

faststack/tests/test_cache_invalidation.py (2)

49-66: Consider converting print-based checks to proper assertions.

The test uses print("PASS/FAIL") patterns instead of assertions, which won't cause test failures when run by pytest. This could mask regressions.

Example conversion
     if hash1 == hash2:
         print("PASS: Hash is stable across reloads.")
     else:
-        print("FAIL: Hash changed across reloads (unnecessary invalidation).")
+        raise AssertionError("FAIL: Hash changed across reloads (unnecessary invalidation).")

Or use assert hash1 == hash2, "Hash changed across reloads".


69-72: Avoid bare except: clause.

The bare except: catches all exceptions including KeyboardInterrupt and SystemExit. Consider using except Exception: or except OSError: for the cleanup.

Proposed fix
     try:
         shutil.rmtree(test_dir)
-    except:
+    except Exception:
         pass
faststack/tests/test_new_features.py (2)

164-184: Consider using _ prefix for intentionally unused variables.

At line 174, blacks and whites are unpacked but not used since this test case only validates the degenerate dynamic range behavior.

Proposed fix
-        blacks, whites, p_low, p_high = self.editor.auto_levels(threshold_percent)
+        _blacks, _whites, p_low, p_high = self.editor.auto_levels(threshold_percent)

241-257: Consider using _ prefix for unused variables in monotonicity check.

At line 248, p_low and p_high are not used in the subsequent monotonicity verification.

Proposed fix
-        blacks, whites, p_low, p_high = self.editor.auto_levels(threshold_percent)
+        blacks, whites, _p_low, _p_high = self.editor.auto_levels(threshold_percent)
faststack/tests/test_prefetch_logic.py (1)

50-52: Use unittest assertions instead of raising a generic Exception.

On Line 51, raise Exception(...) should be an assertion for cleaner test output and to satisfy TRY002/TRY003.

Proposed fix
-            if 4 not in prefetcher.futures:
-                raise Exception("Task 4 was not added to futures!")
+            self.assertIn(4, prefetcher.futures, "Task 4 was not added to futures!")
faststack/config.py (1)

145-152: Avoid unconditional config writes on every load.

self.save() runs even when nothing was added, which can cause unnecessary disk churn. Consider tracking whether defaults were inserted before saving.

♻️ Suggested change
-            for section, keys in DEFAULT_CONFIG.items():
+            updated = False
+            for section, keys in DEFAULT_CONFIG.items():
                 if not self.config.has_section(section):
                     self.config.add_section(section)
+                    updated = True
                 for key, value in keys.items():
                     if not self.config.has_option(section, key):
                         self.config.set(section, key, value)
-            self.save()  # Save to add any missing keys
+                        updated = True
+            if updated:
+                self.save()  # Save to add any missing keys
faststack/imaging/editor.py (1)

1625-1634: Consider narrowing the exception catch (optional).

The broad except Exception catch at line 1627 is flagged by static analysis. While the defensive fallback to None is appropriate for EXIF handling (which can fail in many unpredictable ways), consider narrowing to more specific exception types if the failure modes are well-understood:

except (OSError, ValueError, TypeError, AttributeError) as e:

However, given that EXIF libraries can raise various unexpected exceptions from corrupt/malformed data, the current approach is pragmatic and the warning log provides visibility.

faststack/imaging/prefetch.py (2)

453-458: Broad exception catches are pragmatic for image decoding fallbacks.

Static analysis flags multiple except Exception blocks (lines 453, 473, 609, 687, 749, 762, 795). In the context of image decoding from arbitrary files, these broad catches are reasonable - corrupt images, unsupported formats, and I/O errors can manifest as many different exception types.

The current pattern:

  1. Logs the specific error with file path
  2. Falls back gracefully (returns None or tries next decoder)
  3. Doesn't crash the prefetch worker thread

If you want to tighten this in the future, consider catching (OSError, IOError, ValueError, RuntimeError) as the most common failure modes.


900-901: Consider moving success return to else block (optional).

Static analysis suggests moving return image_file.path, display_generation to an else block for clarity, distinguishing the success path from exception handling. This is a minor stylistic improvement:

♻️ Optional refactor
             self.cache_put(cache_key, decoded_image)
             log.debug(...)
-            return image_file.path, display_generation
 
         except (OSError, IOError, ValueError, MemoryError) as e:
             log.warning(...)
+        else:
+            return image_file.path, display_generation
 
         return None
faststack/tests/test_metadata.py (2)

9-11: Avoid unused patched arg (mock_exists).
Rename it to _mock_exists to silence ARG002 while keeping the patch.

♻️ Proposed fix
-    def test_get_exif_data_success(self, mock_open, mock_exists):
+    def test_get_exif_data_success(self, mock_open, _mock_exists):

68-72: Use bare raise when rethrowing.
Keeps the original traceback intact and resolves TRY201.

♻️ Proposed fix
-        except Exception as e:
+        except Exception:
             import traceback

             traceback.print_exc()
-            raise e
+            raise
faststack/tests/test_exif_compat.py (1)

127-138: Drop unused mock_tiff alias.
The patch is needed, but the bound name isn’t used.

♻️ Proposed fix
-            patch.object(self.editor, "_write_tiff_16bit") as mock_tiff,
+            patch.object(self.editor, "_write_tiff_16bit"),
faststack/tests/test_exif_orientation.py (2)

24-30: Remove redundant try/except around import.
The exception is immediately re‑raised, so the handler adds no value.

♻️ Proposed fix
-        try:
-            from faststack.imaging.editor import ImageEditor
-
-            self.ImageEditorClass = ImageEditor
-        except ImportError:
-            # Fallback if path issues persist (shouldn't with sys.path.append)
-            raise
+        from faststack.imaging.editor import ImageEditor
+        self.ImageEditorClass = ImageEditor

179-179: Remove unused res assignment.
It’s unused and flagged by Ruff F841.

♻️ Proposed fix
-        res = self.editor.save_image(write_developed_jpg=True)
+        self.editor.save_image(write_developed_jpg=True)
faststack/tests/test_recycle_bin_tracking.py (1)

46-46: Remove unused tmp_path parameter.
It’s unused in the test body and triggers ARG001.

♻️ Proposed fix
-def test_move_to_recycle_tracks_bin(app_controller, tmp_path):
+def test_move_to_recycle_tracks_bin(app_controller):
faststack/io/indexer.py (2)

94-110: Unused loop variable stem.

The loop iterates over raws.items() but only uses raw_list. Consider using raws.values() instead to make the intent clearer.

♻️ Suggested fix
-    for stem, raw_list in raws.items():
+    for raw_list in raws.values():
         for raw_path, raw_stat in raw_list:
             if raw_path not in used_raws:

163-167: Unused parameter jpg_path.

The jpg_path parameter is not used within the function body. Consider removing it if not needed for future expansion, or prefix with underscore to indicate it's intentionally unused.

♻️ Suggested fix
 def _find_raw_pair(
-    jpg_path: Path,
+    _jpg_path: Path,  # Unused but kept for potential future use
     jpg_stat: os.stat_result,
     potential_raws: List[Tuple[Path, os.stat_result]],
 ) -> Path | None:

Or remove the parameter entirely and update the call site at line 64.

faststack/thumbnail_view/folder_stats.py (4)

34-39: Duplicate comment on line 38.

The comment about folder_mtime_ns is duplicated on lines 37 and 38.

♻️ Suggested fix
 # Cache by (folder_path, json_mtime_ns, folder_mtime_ns) to avoid re-parsing during scroll
 # IMPORTANT: Both json_mtime_ns and folder_mtime_ns are needed:
 # - json_mtime_ns: changes when faststack.json is modified (flags, metadata)
 # - folder_mtime_ns: changes when files are added/removed/renamed in folder
-# - folder_mtime_ns: changes when files are added/removed/renamed in folder
 _stats_cache: Dict[Tuple[Path, int, int], Optional[FolderStats]] = {}

98-101: Duplicate comment on line 99.

The comment "Cache the result (even if None)" is duplicated.

♻️ Suggested fix
     # Cache the result (even if None)
-    # Cache the result (even if None)
     _check_cache_size(_stats_cache)

106-120: Naming inconsistency: JPG_EXTENSIONS includes non-JPG formats.

This JPG_EXTENSIONS set includes PNG, GIF, BMP, TIFF, and WEBP, which is inconsistent with the same-named constant in indexer.py that only includes actual JPEG extensions. Consider renaming to IMAGE_EXTENSIONS to better reflect its purpose and avoid confusion.


323-324: Cache declaration appears after its first usage.

The _raw_count_cache dictionary is used at line 300 but declared at line 324. Consider moving this declaration near line 39 with the other cache declarations for better code organization.

faststack/tests/test_permanent_delete.py (1)

6-12: Use explicit Optional or | None for type annotation.

PEP 484 prohibits implicit Optional. When a parameter has a default of None, the type should explicitly indicate it can be None.

♻️ Suggested fix
+from typing import Optional
+
+
 class MockImageFile:
     """Mock ImageFile for testing."""

-    def __init__(self, jpg_path: Path, raw_path: Path = None):
+    def __init__(self, jpg_path: Path, raw_path: Optional[Path] = None):
         self.path = jpg_path
         self.raw_pair = raw_path

Or using Python 3.10+ syntax:

-    def __init__(self, jpg_path: Path, raw_path: Path = None):
+    def __init__(self, jpg_path: Path, raw_path: Path | None = None):
faststack/thumbnail_view/model.py (1)

356-364: Avoid swallowing metadata errors silently.
A blind except Exception hides failures and makes debugging harder. Consider at least a debug log (or narrowing the exception).

🔧 Suggested logging tweak
                 if self._get_metadata:
                     try:
                         meta = self._get_metadata(img.path.stem)
                         is_stacked = meta.get("stacked", False)
                         is_uploaded = meta.get("uploaded", False)
                         is_edited = meta.get("edited", False)
                         is_restacked = meta.get("restacked", False)
-                    except Exception:
-                        pass
+                    except Exception as exc:
+                        log.debug("Metadata lookup failed for %s: %s", img.path, exc)

Comment thread faststack/tests/check_imports.py
Comment thread faststack/tests/test_permanent_delete.py Outdated
Comment thread faststack/ui/provider.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@faststack/io/deletion.py`:
- Around line 33-39: The code assumes image_file.path is non-None when building
files_to_delete (using jpg_path.name) which can raise AttributeError; update the
block that references image_file.path and image_file.raw_pair (jpg_path,
raw_path, files_to_delete) to defend against None by checking if jpg_path is
None before accessing .name — either raise a clear ValueError or skip adding the
JPG to files_to_delete (and log the condition) and then continue to check
raw_path.exists() safely; ensure the chosen behavior (error vs skip) is applied
consistently and documented in the function's docstring or comment.

In `@faststack/ui/provider.py`:
- Around line 139-141: The file defines the signal preloadingStateChanged twice
which causes the first definition to be shadowed; remove the duplicate
declaration so there is only a single preloadingStateChanged = Signal() (keep
the other signals like preloadProgressChanged intact) and ensure any references
in the class/module use that single symbol.
🧹 Nitpick comments (4)
faststack/tests/check_imports.py (1)

23-37: LGTM - same approved pattern as above.

The second import check follows the same structure. One optional cleanup: consider moving import traceback to the top of the file alongside the other imports to avoid the repeated conditional imports.

,

♻️ Optional: Move traceback import to top
 import sys
 import os
+import traceback
 
 # Add current directory to path
 sys.path.append(os.getcwd())

Then remove the import traceback lines from each except block (lines 14, 19, 30, 35).

faststack/io/watcher.py (1)

47-51: Unused self.callback attribute.

The callback is stored in self.callback on line 51, but it's never used elsewhere in the class. The callback is already passed to ImageDirectoryEventHandler on line 49. Consider removing the unused assignment to avoid confusion.

♻️ Proposed fix
     def __init__(self, directory: Path, callback):
         self.observer: Optional[Observer] = None  # Initialize to None
         self.event_handler = ImageDirectoryEventHandler(callback)
         self.directory = directory
-        self.callback = callback  # Store callback for new observer
faststack/io/deletion.py (1)

19-21: Use logging.exception to preserve stack trace.

When catching exceptions, log.exception automatically includes the traceback, which is valuable for debugging. This applies here and in the permanently_delete_image_files function (lines 142, 151).

♻️ Proposed fix
     except (PermissionError, OSError) as e:
-        log.error("Failed to create recycle bin directory: %s", e)
+        log.exception("Failed to create recycle bin directory: %s", e)
         return False

And similarly in permanently_delete_image_files:

         except OSError as e:
-            log.error("Failed to permanently delete %s: %s", jpg_path.name, e)
+            log.exception("Failed to permanently delete %s: %s", jpg_path.name, e)
faststack/tests/test_permanent_delete.py (1)

14-19: Use explicit Path | None type hint instead of implicit Optional.

PEP 484 prohibits implicit Optional. The raw_path parameter should use an explicit union type.

♻️ Proposed fix
 class MockImageFile:
     """Simple mock for ImageFile."""
-    def __init__(self, jpg_path: Path, raw_path: Path = None):
+    def __init__(self, jpg_path: Path, raw_path: Path | None = None):
         self.path = jpg_path
         self.raw_pair = raw_path
         self.is_video = False

Comment thread faststack/io/deletion.py
Comment on lines +33 to +39
jpg_path = image_file.path
raw_path = image_file.raw_pair

# Build list of files that will be deleted
files_to_delete = [str(jpg_path.name)]
if raw_path and raw_path.exists():
files_to_delete.append(str(raw_path.name))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential AttributeError if image_file.path is None.

Line 37 accesses jpg_path.name without checking if jpg_path is None. If image_file.path could be None, this will raise an AttributeError. Consider adding a guard or documenting that image_file.path must be non-None.

🛡️ Proposed defensive fix
     jpg_path = image_file.path
     raw_path = image_file.raw_pair

     # Build list of files that will be deleted
-    files_to_delete = [str(jpg_path.name)]
+    files_to_delete = [str(jpg_path.name)] if jpg_path else []
     if raw_path and raw_path.exists():
         files_to_delete.append(str(raw_path.name))
+
+    if not files_to_delete:
+        return False  # Nothing to delete
🤖 Prompt for AI Agents
In `@faststack/io/deletion.py` around lines 33 - 39, The code assumes
image_file.path is non-None when building files_to_delete (using jpg_path.name)
which can raise AttributeError; update the block that references image_file.path
and image_file.raw_pair (jpg_path, raw_path, files_to_delete) to defend against
None by checking if jpg_path is None before accessing .name — either raise a
clear ValueError or skip adding the JPG to files_to_delete (and log the
condition) and then continue to check raw_path.exists() safely; ensure the
chosen behavior (error vs skip) is applied consistently and documented in the
function's docstring or comment.

Comment thread faststack/ui/provider.py
Comment on lines 139 to 141
preloadingStateChanged = Signal()
preloadingStateChanged = Signal()
preloadProgressChanged = Signal()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate signal declaration will shadow the first definition.

preloadingStateChanged is declared twice (lines 139 and 140). The second declaration shadows the first. Remove the duplicate.

🐛 Proposed fix
     preloadingStateChanged = Signal()
-    preloadingStateChanged = Signal()
     preloadProgressChanged = Signal()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
preloadingStateChanged = Signal()
preloadingStateChanged = Signal()
preloadProgressChanged = Signal()
preloadingStateChanged = Signal()
preloadProgressChanged = Signal()
🤖 Prompt for AI Agents
In `@faststack/ui/provider.py` around lines 139 - 141, The file defines the signal
preloadingStateChanged twice which causes the first definition to be shadowed;
remove the duplicate declaration so there is only a single
preloadingStateChanged = Signal() (keep the other signals like
preloadProgressChanged intact) and ensure any references in the class/module use
that single symbol.

@AlanRockefeller AlanRockefeller merged commit abfa7f1 into main Jan 29, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant