Skip to content

Test#40

Merged
AlanRockefeller merged 5 commits intomainfrom
test
Feb 7, 2026
Merged

Test#40
AlanRockefeller merged 5 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Feb 5, 2026

Summary by CodeRabbit

  • New Features

    • Non-blocking background save for the editor; saves complete without freezing the UI
    • Unified deletion options (single, grid, batch) with safer rollback and clearer status messaging
  • Bug Fixes

    • More robust crop/rotation math to avoid rounding/errors
    • Suppress "empty folder" message until a folder has finished loading
    • Better cache validation and highlight-recovery behavior for images
  • UI Improvements

    • Save button shows "Saving..." and disables while saving; shortcut behavior updated
    • Recycle bin dialog shows detailed, collapsible file list; improved Escape-key focus
  • Documentation & Tests

    • Release notes/version updated; expanded test coverage for deletion and highlight recovery

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 5, 2026

Walkthrough

Release bump to 1.5.5: adds a centralized deletion engine with undo/rollback and recycle/permanent-delete flows, background save offload with _saveFinished signaling, rotation/crop math fixes, adaptive highlight-recovery, UI/dialog enhancements, cache invalidation helpers, and extensive tests for deletion and recovery.

Changes

Cohort / File(s) Summary
Version & docs
pyproject.toml, README.md, ChangeLog.md, .gitignore
Version updated (1.5.5), changelog/README updated, and new local test ignore patterns added.
Centralized deletion & undo
faststack/app.py, faststack/io/deletion.py, faststack/qml/Components.qml, faststack/qml/Main.qml
Introduces unified _delete_indices engine, public deletion entry points (delete_current_image, grid_delete_at_cursor, delete_batch_images), recycle/permanent-delete coordination, rollback, optimistic UI updates, deferred refresh, and UI focus fixes and a redesigned recycle dialog.
Background save & UI integration
faststack/app.py, faststack/qml/ImageEditorDialog.qml, faststack/ui/provider.py
Adds ThreadPoolExecutor-backed background save, _saveFinished signal/handler, isSaving property with UI bindings; editor shows "Saving..." and auto-closes on save completion.
Image rotation & crop math
faststack/imaging/editor.py
Crop/rotation math refined: use floor for crop dims, preserve orig_h/orig_w pre-rotation, adjust inset logic for exact 90° rotations, and cache-shape validation.
Highlight recovery algorithm
faststack/imaging/math_utils.py
Replaces linear recovery with rational compression formulation; adds k parameter for compression control and behavior tweaks.
Cache helpers & metadata
faststack/imaging/cache.py, faststack/imaging/metadata.py
Adds ByteLRUCache.pop_path(path) for targeted invalidation; minor exception-handling formatting change.
Tests & tooling
faststack/tests/*, faststack/all_verification_results_utf8.txt, faststack/repro_type_error.py
New/updated tests for deletion unification, loupe delete, reactive delete, highlight recovery, editor integration/rotation; removed test_pil_blur.py; added repro script and test run log.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant EditorUI as ImageEditorDialog
    participant AppCtrl as AppController
    participant SaveExec as Background Executor
    participant Handler as _on_save_finished
    participant UI as UIState

    User->>EditorUI: Click Save / press S
    EditorUI->>AppCtrl: save_edited_image()
    EditorUI->>UI: set isSaving = true
    AppCtrl->>SaveExec: offload save task
    SaveExec-->>AppCtrl: emit _saveFinished(result)
    AppCtrl->>Handler: _on_save_finished(result)
    Handler->>UI: set isSaving = false, refresh/select
    Handler->>EditorUI: close editor (auto)
Loading
sequenceDiagram
    participant User as User
    participant GridUI as ThumbnailGridView
    participant Loupe as LoupeView
    participant AppCtrl as AppController
    participant DeleteEngine as _delete_indices
    participant Recycle as RecycleBin
    participant UI as UIState

    User->>GridUI: request delete (selection / cursor / batch)
    GridUI->>AppCtrl: delete_current_image() / grid_delete_at_cursor() / delete_batch_images()
    AppCtrl->>DeleteEngine: _delete_indices(indices, action_type)
    DeleteEngine->>UI: optimistic sync_ui_state()
    DeleteEngine->>Recycle: attempt move to recycle
    alt all moved
        DeleteEngine->>AppCtrl: schedule deferred refresh
        AppCtrl->>UI: refresh_image_list()
    else some failures
        DeleteEngine->>User: prompt confirm_permanent_delete
        alt user confirms
            DeleteEngine->>Recycle: permanently_delete_image_files
        else user cancels
            DeleteEngine->>AppCtrl: rollback UI/model
        end
    end
    DeleteEngine->>User: show status / undo info
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #31: Directly related — also refactors AppController deletion flows, _move_to_recycle, and new deletion entry points.
  • PR #25: Related — touches editor crop/rotate logic and QML crop handling, overlapping editor/math changes.
  • PR #13: Related — modifies image-deletion lifecycle and recycle/undo semantics at the AppController level.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Test' is vague and non-descriptive, providing no meaningful information about the substantive changes in this PR (deletion refactoring, background saves, highlight recovery, UI improvements, etc.). Revise the title to clearly describe the main changes, such as 'Refactor deletion system and add background save support' or reference the primary technical changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
faststack/app.py (1)

1040-1074: Prefer targeted cache invalidation after save.
Clearing the entire cache on save is heavier than necessary; you already have saved_path, and other edit flows use pop_path. Consider mirroring that behavior here to avoid evicting unrelated images.

🔧 Proposed change
-                # 5. Force UI Sync / Prefetch
-                self.image_cache.clear()  # Clear cache to ensure we reload valid image
+                # 5. Force UI Sync / Prefetch
+                if saved_path:
+                    self.image_cache.pop_path(saved_path)
+                else:
+                    self.image_cache.clear()

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: 6

Caution

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

⚠️ Outside diff range comments (1)
faststack/app.py (1)

1018-1068: ⚠️ Potential issue | 🟠 Major

Gate editor cleanup and image reselection to prevent disrupting in-progress edits.

When a user navigates to a different image during a background save, _on_save_finished should not clear the new image's editor state or jump the selection back to the saved image. Currently, self.image_editor.clear() executes unconditionally (line 1031), destroying any edits in progress on the new image. Additionally, the image reselection logic (lines 1037–1055) attempts to find and select the originally saved image regardless of whether the user has moved on. Gate both of these operations on editor_still_on_same_image to preserve the new editing session.

🛠️ Suggested fix
-            # 2. Clear Editor State (release memory)
-            self.image_editor.clear()
+            # 2. Clear Editor State (release memory) only if still on initiating image
+            if editor_still_on_same_image:
+                self.image_editor.clear()

-            # 4. Find and Select the saved image
-            new_index = self.current_index  # Default to keeping selection if not found
-
-            # Try to find by exact path match
-            if saved_path:
+            # 4. Find and Select the saved image only if still on initiating image
+            new_index = self.current_index  # Default to keeping selection if not found
+            if editor_still_on_same_image and saved_path:
                 try:
                     target_resolve = saved_path.resolve()
🤖 Fix all issues with AI agents
In @.gitignore:
- Line 92: The .gitignore entry "test*" is too broad and may exclude legitimate
files/directories starting with "test"; replace it with targeted patterns (for
example "test_output*", "test_temp*", "test_debug*") or explicitly ignore the
specific temp artifacts you intend to exclude, and if you need to ignore only
root-level artifacts consider anchoring or directory-specific patterns (e.g., a
dedicated pattern for the temp artifact names rather than the generic "test*").

In `@faststack/app.py`:
- Around line 2858-2925: The loop currently calls self._move_to_recycle for JPG
and RAW together which can orphan RAW if JPG fails; change the logic so you call
recycled_jpg = self._move_to_recycle(jpg_path) first, check its truthiness, and
only then call self._move_to_recycle(raw_path) (if raw_path exists); if you must
keep the existing order instead, ensure that any RAW moved while JPG failed is
undone by moving RAW back from the recycle bin and logging/marking the failure.
Update references in this block (functions/vars: _move_to_recycle, recycled_jpg,
recycled_raw, failed_recycles, successfully_deleted, delete_history,
undo_history) so RAW moves occur only after JPG success or are rolled back when
JPG fails.
- Around line 2817-2844: The in-memory deletion in _delete_indices removes
entries from self.image_files but leaves self._path_to_index stale until the
deferred refresh; call self._rebuild_path_to_index() immediately after the
in-memory removals and UI sync so the path->index map reflects the new list, and
also call self._rebuild_path_to_index() inside the rollback path where
removed_items are restored so the map is updated after restoration (reference
methods/attributes: _delete_indices, _rebuild_path_to_index, self.image_files,
self._path_to_index, removed_items, previous_index).

In `@faststack/imaging/editor.py`:
- Around line 647-648: The current code captures orig_h, orig_w but only
recenters the box after expand-rotation and fails to rotate the crop-box when
straighten_angle != 0; update the logic that computes the transformed crop
rectangle (used around the orig_h, orig_w calculation and duplicated in the
block covering lines ~702-742) to: compute the original image center from
orig_h, orig_w, create the four crop-box corner coordinates relative to that
center, apply a 2D rotation by straighten_angle to each corner, translate those
rotated corners into the expanded canvas coordinate system (using the same
translation used for the image expansion/recentering), then derive the final
crop bounds by taking min/max of the rotated-translated corner coordinates;
ensure this replaces the existing “recentering only” step and is used wherever
the code currently recenters the box after expand-rotation.

In `@faststack/qml/Main.qml`:
- Around line 1362-1372: The Delete and Quit button calls
uiState.cleanupRecycleBins() without checking for null; wrap the call to
uiState.cleanupRecycleBins() in a guard (if (uiState) { ... }) so the cleanup is
only invoked when uiState is defined, then proceed to set
allowCloseWithRecycleBins, call recycleBinCleanupDialog.close(), and Qt.quit()
as before; update the Button's onClicked handler to use this null-check around
the uiState call referencing uiState.cleanupRecycleBins().

In `@faststack/tests/test_deletion_unification.py`:
- Around line 184-201: The test currently mocks mock_controller._delete_indices
after calling mock_controller.delete_current_image(), making the "not called"
assertion meaningless; move the mock setup so mock_controller._delete_indices =
Mock() is assigned before invoking mock_controller.delete_current_image(), keep
the existing mock for mock_controller.get_batch_count_for_current_image and
mock_controller.main_window, then call delete_current_image and assert
mock_controller.main_window.show_delete_batch_dialog.assert_called_once_with(5)
and mock_controller._delete_indices.call_count == 0 to ensure deletion was
deferred to the dialog.
🧹 Nitpick comments (2)
faststack/tests/test_reactive_delete.py (1)

83-114: Remove unused tmp_path parameter.

The tmp_path fixture parameter is not used in this test function—img_path is derived from app_controller.image_dir instead.

Proposed fix
-def test_reactive_delete_fallback_cancelled(app_controller, tmp_path):
+def test_reactive_delete_fallback_cancelled(app_controller):
faststack/tests/test_loupe_delete.py (1)

77-82: Prefix unused ts variable with underscore.

The timestamp variable is unpacked but not used in assertions. Prefix it with an underscore to indicate it's intentionally unused.

Proposed fix
-    action, record, ts = mock_controller.undo_history[0]
+    action, record, _ts = mock_controller.undo_history[0]

Comment thread .gitignore Outdated
Comment thread faststack/app.py
Comment thread faststack/app.py Outdated
Comment thread faststack/imaging/editor.py
Comment thread faststack/qml/Main.qml
Comment thread faststack/tests/test_deletion_unification.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: 5

Caution

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

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

88-93: ⚠️ Potential issue | 🟠 Major

save_edited_image is now async — this assertion is racy.

save_edited_image() was refactored to offload the actual save_image() call to _save_executor (a real ThreadPoolExecutor). The method returns immediately after submitting the task, so save_image.assert_called_once() at line 91 may execute before the background thread invokes save_image.

This test can intermittently pass or fail depending on thread scheduling. To fix, either:

  1. Mock _save_executor so the work runs synchronously, or
  2. Wait for the future to complete before asserting.
Option 1: Make save executor synchronous in tests
         # 5. save_edited_image
         try:
+            # Mock executor to run synchronously for deterministic test
+            from unittest.mock import patch as _patch
+            with _patch.object(self.controller._save_executor, 'submit', side_effect=lambda fn, *a, **kw: (fn(), MagicMock())[1]) as mock_submit:
+                # Or simpler: just verify the method exists and the intent
             self.controller.save_edited_image()
-            self.controller.image_editor.save_image.assert_called_once()
+            # save_image is called asynchronously; for a unit test, verify
+            # that submit was called on the executor instead
         except AttributeError:
             self.fail("AppController is missing method 'save_edited_image'")

A cleaner approach would be to inject/mock the _save_executor during setUp with a synchronous executor:

from concurrent.futures import ThreadPoolExecutor

# Replace with a single-threaded immediate executor or mock
# so save_image runs synchronously in tests
🤖 Fix all issues with AI agents
In `@faststack/app.py`:
- Around line 2993-2999: The rollback loop inserts restored items using their
original indices but does so in ascending order which lets earlier insertions
shift later targets; change the loop that processes items_to_restore in the
delete/rollback code to iterate in descending original index order (i.e., sort
or reverse items_to_restore by idx before inserting) so each insertion doesn't
affect positions of yet-to-be-inserted items, still clamping insert_idx with
min(idx, len(self.image_files)); reference items_to_restore, self.image_files
and refresh_image_list() when making the change.
- Around line 2905-2910: The except block catching (OSError, shutil.Error) for
undoing the JPG move currently calls log.error which loses the traceback; change
that call to log.exception so the exception traceback is recorded while
preserving the same message about failing to undo JPG move for jpg_path.name
(the except block handling undo_err for jpg_path). Keep the existing message
text and variable usage, only replace log.error(...) with log.exception(...) to
match other exception logging in this file.

In `@faststack/repro_type_error.py`:
- Around line 4-6: Replace the hardcoded Windows path passed to sys.path.insert
with a computed repository root derived from the current file location: use
__file__ (via pathlib.Path(__file__).resolve().parent or .parents[n]) to find
the repo root and insert that string into sys.path with the existing
sys.path.insert call so the import path is portable across environments.
- Around line 15-26: The repro script uses a broad catch "except Exception as
e:" around the call to editor._apply_edits(img) which triggers BLE001; to
suppress this for the debugging harness, append a per-line flake8 suppression to
that except line (change "except Exception as e:" to "except Exception as e:  #
noqa: BLE001") and ensure the try/except block around editor._apply_edits and
the debug prints covers the intended lines (the block containing the
print("Calling _apply_edits..."), the call to editor._apply_edits, the result
prints, and the except).

In `@faststack/tests/test_deletion_unification.py`:
- Around line 19-23: The mock_controller fixture currently accepts qapp but
never references it, triggering ARG001; inside the mock_controller function
(fixture name: mock_controller) add a no-op reference to qapp (for example
assign it to a throwaway variable or assert it) so the QApplication fixture is
retained and the linter sees qapp as used; keep the reference as a single line
near the top of the fixture so it has no runtime effect but satisfies Ruff.
🧹 Nitpick comments (3)
faststack/tests/test_editor_rotation.py (1)

270-272: Add strict= to zip() for Ruff B905 compliance.
This keeps the helper assertions explicit about length matching.

🔧 Suggested fix (apply to both helpers)
-        assert all(abs(a - b) <= 1 for a, b in zip(c1, c2)), f"{msg}: {c1} != {c2}"
+        assert all(abs(a - b) <= 1 for a, b in zip(c1, c2, strict=True)), f"{msg}: {c1} != {c2}"

Also applies to: 313-315

faststack/app.py (2)

946-1000: Background save offloading looks well-designed.

The signal-based hop from the background thread to the main thread is correct for Qt, and the _save_initiated_path guard against "surprise close" on navigation is a nice touch. A couple of minor notes from static analysis:

  • Line 984: log.exception(f"Unexpected error during save: {e}") — the {e} is redundant since log.exception already appends the traceback including the exception. Use log.exception("Unexpected error during save") instead.
  • Line 980: Ruff TRY300 suggests moving the success return into an else block of the try, though this is a style nit.
Minor cleanup
             try:
                 result = self.image_editor.save_image(
                     write_developed_jpg=write_sidecar, developed_path=dev_path
                 )
                 return {"success": True, "result": result}
             except RuntimeError as e:
                 return {"success": False, "error": str(e)}
             except Exception as e:
-                log.exception(f"Unexpected error during save: {e}")
+                log.exception("Unexpected error during save")
                 return {"success": False, "error": "Failed to save image"}

956-958: Double-save guard relies on UI state — consider a dedicated lock or flag.

self.ui_state.isSaving is a Qt property likely bound to QML. If the background thread hasn't finished but the UI flag is somehow toggled externally (e.g., QML binding reset), a second save could be submitted. A simple self._save_in_progress boolean checked and set atomically would be more robust than relying on the UI property. That said, this is low-risk since only this method sets isSaving to True.

Comment thread faststack/app.py
Comment thread faststack/app.py
Comment thread faststack/repro_type_error.py
Comment thread faststack/repro_type_error.py
Comment thread faststack/tests/test_deletion_unification.py
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