From 5676ad510146aabfb37c6302cba180f6248217cf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 17:07:38 +1000 Subject: [PATCH] update find&replace, and remove perform_op() --- qt/aqt/find_and_replace.py | 89 +++++++----------------------- qt/aqt/main.py | 95 ++------------------------------- qt/aqt/operations/__init__.py | 31 ++++++----- qt/aqt/operations/collection.py | 3 ++ qt/aqt/operations/note.py | 30 ++++++++++- qt/aqt/operations/scheduling.py | 3 ++ qt/aqt/operations/tag.py | 26 +++++++++ qt/aqt/taskman.py | 2 +- qt/mypy.ini | 12 +---- qt/tools/genhooks_gui.py | 6 +-- rslib/i18n/build/extract.rs | 2 +- 11 files changed, 108 insertions(+), 191 deletions(-) diff --git a/qt/aqt/find_and_replace.py b/qt/aqt/find_and_replace.py index d0831e2b1..4f6b14e3f 100644 --- a/qt/aqt/find_and_replace.py +++ b/qt/aqt/find_and_replace.py @@ -3,11 +3,13 @@ from __future__ import annotations -from typing import List, Optional, Sequence +from typing import List, Sequence import aqt from anki.notes import NoteId from aqt import AnkiQt, QWidget +from aqt.operations.note import find_and_replace +from aqt.operations.tag import find_and_replace_tag from aqt.qt import QDialog, Qt from aqt.utils import ( HelpPage, @@ -22,63 +24,10 @@ from aqt.utils import ( save_combo_index_for_session, save_is_checked, saveGeom, - tooltip, tr, ) -def find_and_replace( - *, - mw: AnkiQt, - parent: QWidget, - note_ids: Sequence[NoteId], - search: str, - replacement: str, - regex: bool, - field_name: Optional[str], - match_case: bool, -) -> None: - mw.perform_op( - lambda: mw.col.find_and_replace( - note_ids=note_ids, - search=search, - replacement=replacement, - regex=regex, - field_name=field_name, - match_case=match_case, - ), - success=lambda out: tooltip( - tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), - parent=parent, - ), - ) - - -def find_and_replace_tag( - *, - mw: AnkiQt, - parent: QWidget, - note_ids: Sequence[int], - search: str, - replacement: str, - regex: bool, - match_case: bool, -) -> None: - mw.perform_op( - lambda: mw.col.tags.find_and_replace( - note_ids=note_ids, - search=search, - replacement=replacement, - regex=regex, - match_case=match_case, - ), - success=lambda out: tooltip( - tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), - parent=parent, - ), - ) - - class FindAndReplaceDialog(QDialog): COMBO_NAME = "BrowserFindAndReplace" @@ -146,10 +95,9 @@ class FindAndReplaceDialog(QDialog): save_is_checked(self.form.re, self.COMBO_NAME + "Regex") save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") + # tags? if self.form.field.currentIndex() == 1: - # tags find_and_replace_tag( - mw=self.mw, parent=self.parentWidget(), note_ids=self.note_ids, search=search, @@ -157,23 +105,22 @@ class FindAndReplaceDialog(QDialog): regex=regex, match_case=match_case, ) - return - - if self.form.field.currentIndex() == 0: - field = None else: - field = self.field_names[self.form.field.currentIndex() - 2] + # fields + if self.form.field.currentIndex() == 0: + field = None + else: + field = self.field_names[self.form.field.currentIndex() - 2] - find_and_replace( - mw=self.mw, - parent=self.parentWidget(), - note_ids=self.note_ids, - search=search, - replacement=replace, - regex=regex, - field_name=field, - match_case=match_case, - ) + find_and_replace( + parent=self.parentWidget(), + note_ids=self.note_ids, + search=search, + replacement=replace, + regex=regex, + field_name=field, + match_case=match_case, + ) super().accept() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index a977940e9..7b2436558 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -52,11 +52,6 @@ from aqt.emptycards import show_empty_cards from aqt.legacy import install_pylib_legacy from aqt.mediacheck import check_media_db from aqt.mediasync import MediaSyncer -from aqt.operations import ( - CollectionOpFailureCallback, - CollectionOpSuccessCallback, - ResultWithChanges, -) from aqt.operations.collection import undo from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -96,9 +91,6 @@ MainWindowState = Literal[ T = TypeVar("T") -PerformOpOptionalSuccessCallback = Optional[CollectionOpSuccessCallback] -PerformOpOptionalFailureCallback = Optional[CollectionOpFailureCallback] - class AnkiQt(QMainWindow): col: Collection @@ -710,10 +702,9 @@ class AnkiQt(QMainWindow): ) -> None: """Run an operation that queries the DB on a background thread. - Similar interface to perform_op(), but intended to be used for operations - that do not change collection state. Undo status will not be changed, - and `operation_did_execute` will not fire. No progress window will - be shown either. + Intended to be used for operations that do not change collection + state. Undo status will not be changed, and `operation_did_execute` + will not fire. No progress window will be shown either. `operations_will|did_execute` will still fire, so the UI can defer updates during a background task. @@ -743,66 +734,6 @@ class AnkiQt(QMainWindow): # Resetting state ########################################################################## - def perform_op( - self, - op: Callable[[], ResultWithChanges], - *, - success: PerformOpOptionalSuccessCallback = None, - failure: PerformOpOptionalFailureCallback = None, - handler: Optional[object] = None, - ) -> None: - """Run the provided operation on a background thread. - - op() should either return OpChanges, or an object with a 'changes' - property. The changes will be passed to `operation_did_execute` so that - the UI can decide whether it needs to update itself. - - - Shows progress popup for the duration of the op. - - Ensures the browser doesn't try to redraw during the operation, which can lead - to a frozen UI - - Updates undo state at the end of the operation - - Commits changes - - Fires the `operation_(will|did)_reset` hooks - - Fires the legacy `state_did_reset` hook - - Be careful not to call any UI routines in `op`, as that may crash Qt. - This includes things select .selectedCards() in the browse screen. - - success() will be called with the return value of op(). - - If op() throws an exception, it will be shown in a popup, or - passed to failure() if it is provided. - """ - - self._increase_background_ops() - - def wrapped_done(future: Future) -> None: - self._decrease_background_ops() - # did something go wrong? - if exception := future.exception(): - if isinstance(exception, Exception): - if failure: - failure(exception) - else: - showWarning(str(exception)) - return - else: - # BaseException like SystemExit; rethrow it - future.result() - - result = future.result() - try: - if success: - success(result) - finally: - # update undo status - status = self.col.undo_status() - self._update_undo_actions_for_status_and_save(status) - # fire change hooks - self._fire_change_hooks_after_op_performed(result, handler) - - self.taskman.with_progress(op, wrapped_done) - def _increase_background_ops(self) -> None: if not self._background_op_count: gui_hooks.backend_will_block() @@ -814,24 +745,6 @@ class AnkiQt(QMainWindow): gui_hooks.backend_did_block() assert self._background_op_count >= 0 - def _fire_change_hooks_after_op_performed( - self, - result: ResultWithChanges, - handler: Optional[object], - ) -> None: - if isinstance(result, OpChanges): - changes = result - else: - changes = result.changes - - # fire new hook - print("op changes:") - print(changes) - gui_hooks.operation_did_execute(changes, handler) - # fire legacy hook so old code notices changes - if self.col.op_made_changes(changes): - gui_hooks.state_did_reset() - def _synthesize_op_did_execute_from_reset(self) -> None: """Fire the `operation_did_execute` hook with everything marked as changed, after legacy code has called .reset()""" @@ -879,7 +792,7 @@ class AnkiQt(QMainWindow): def reset(self, unused_arg: bool = False) -> None: """Legacy method of telling UI to refresh after changes made to DB. - New code should use mw.perform_op() instead.""" + New code should use CollectionOp() instead.""" if self.col: # fire new `operation_did_execute` hook first. If the overview # or review screen are currently open, they will rebuild the study diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index 360be305d..f416f1075 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -36,9 +36,6 @@ ResultWithChanges = TypeVar( ], ) -CollectionOpSuccessCallback = Callable[[ResultWithChanges], Any] -CollectionOpFailureCallback = Optional[Callable[[Exception], Any]] - class CollectionOp(Generic[ResultWithChanges]): """Helper to perform a mutating DB operation on a background thread, and update UI. @@ -65,7 +62,7 @@ class CollectionOp(Generic[ResultWithChanges]): """ _success: Optional[Callable[[ResultWithChanges], Any]] = None - _failure: Optional[CollectionOpFailureCallback] = None + _failure: Optional[Optional[Callable[[Exception], Any]]] = None def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): self._parent = parent @@ -78,19 +75,25 @@ class CollectionOp(Generic[ResultWithChanges]): return self def failure( - self, failure: Optional[CollectionOpFailureCallback] + self, failure: Optional[Optional[Callable[[Exception], Any]]] ) -> CollectionOp[ResultWithChanges]: self._failure = failure return self def run_in_background(self, *, initiator: Optional[object] = None) -> None: - aqt.mw._increase_background_ops() + from aqt import mw + + assert mw + + mw._increase_background_ops() def wrapped_op() -> ResultWithChanges: - return self._op(aqt.mw.col) + assert mw + return self._op(mw.col) def wrapped_done(future: Future) -> None: - aqt.mw._decrease_background_ops() + assert mw + mw._decrease_background_ops() # did something go wrong? if exception := future.exception(): if isinstance(exception, Exception): @@ -109,18 +112,22 @@ class CollectionOp(Generic[ResultWithChanges]): self._success(result) finally: # update undo status - status = aqt.mw.col.undo_status() - aqt.mw._update_undo_actions_for_status_and_save(status) + status = mw.col.undo_status() + mw._update_undo_actions_for_status_and_save(status) # fire change hooks self._fire_change_hooks_after_op_performed(result, initiator) - aqt.mw.taskman.with_progress(wrapped_op, wrapped_done) + mw.taskman.with_progress(wrapped_op, wrapped_done) def _fire_change_hooks_after_op_performed( self, result: ResultWithChanges, handler: Optional[object], ) -> None: + from aqt import mw + + assert mw + if isinstance(result, OpChanges): changes = result else: @@ -131,5 +138,5 @@ class CollectionOp(Generic[ResultWithChanges]): print(changes) aqt.gui_hooks.operation_did_execute(changes, handler) # fire legacy hook so old code notices changes - if aqt.mw.col.op_made_changes(changes): + if mw.col.op_made_changes(changes): aqt.gui_hooks.state_did_reset() diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index 7d86f3cb7..1eb130b8c 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -31,6 +31,9 @@ def undo(*, parent: QWidget) -> None: def _legacy_undo(*, parent: QWidget) -> None: from aqt import mw + assert mw + assert mw.col + reviewing = mw.state == "review" just_refresh_reviewer = False diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index dae7f8287..de5be2f6b 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Sequence +from typing import Optional, Sequence from anki.collection import OpChanges, OpChangesWithCount from anki.decks import DeckId @@ -34,3 +34,31 @@ def remove_notes( return CollectionOp(parent, lambda col: col.remove_notes(note_ids)).success( lambda out: tooltip(tr.browsing_cards_deleted(count=out.count)), ) + + +def find_and_replace( + *, + parent: QWidget, + note_ids: Sequence[NoteId], + search: str, + replacement: str, + regex: bool, + field_name: Optional[str], + match_case: bool, +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.find_and_replace( + note_ids=note_ids, + search=search, + replacement=replacement, + regex=regex, + field_name=field_name, + match_case=match_case, + ), + ).success( + lambda out: tooltip( + tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), + parent=parent, + ) + ) diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index a9a71c948..384aa0602 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -28,6 +28,7 @@ def set_due_date_dialog( card_ids: Sequence[CardId], config_key: Optional[Config.String.Key.V], ) -> Optional[CollectionOp[OpChanges]]: + assert aqt.mw if not card_ids: return None @@ -76,7 +77,9 @@ def reposition_new_cards_dialog( ) -> Optional[CollectionOp[OpChangesWithCount]]: from aqt import mw + assert mw assert mw.col.db + row = mw.col.db.first( f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0" ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index cd33e2f17..33ee3b9b5 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -90,3 +90,29 @@ def set_tag_collapsed( return CollectionOp( parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed) ) + + +def find_and_replace_tag( + *, + parent: QWidget, + note_ids: Sequence[int], + search: str, + replacement: str, + regex: bool, + match_case: bool, +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.tags.find_and_replace( + note_ids=note_ids, + search=search, + replacement=replacement, + regex=regex, + match_case=match_case, + ), + ).success( + lambda out: tooltip( + tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)), + parent=parent, + ), + ) diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index 3189024f3..a1759ed1c 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -4,7 +4,7 @@ """ Helper for running tasks on background threads. -See mw.query_op() and mw.perform_op() for slightly higher-level routines. +See mw.query_op() and CollectionOp() for higher-level routines. """ from __future__ import annotations diff --git a/qt/mypy.ini b/qt/mypy.ini index d14f07fba..72f33a3a4 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -9,17 +9,7 @@ check_untyped_defs = true disallow_untyped_defs = True strict_equality = true -[mypy-aqt.scheduling_ops] -no_strict_optional = false -[mypy-aqt.note_ops] -no_strict_optional = false -[mypy-aqt.card_ops] -no_strict_optional = false -[mypy-aqt.deck_ops] -no_strict_optional = false -[mypy-aqt.find_and_replace] -no_strict_optional = false -[mypy-aqt.tag_ops] +[mypy-aqt.operations.*] no_strict_optional = false [mypy-aqt.winpaths] diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index f4b50570f..5b4f2b4ab 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -451,7 +451,7 @@ hooks = [ Hook( name="state_did_reset", legacy_hook="reset", - doc="""Legacy 'reset' hook. Called by mw.reset() and mw.perform_op() to redraw the UI. + doc="""Legacy 'reset' hook. Called by mw.reset() and CollectionOp() to redraw the UI. New code should use `operation_did_execute` instead. """, @@ -476,7 +476,7 @@ hooks = [ ), Hook( name="backend_will_block", - doc="""Called before one or more operations are executed with mw.perform_op(). + doc="""Called before one or more DB tasks are run in the background. Subscribers can use this to set a flag to avoid DB queries until the operation completes, as doing so will freeze the UI until the long-running operation @@ -485,7 +485,7 @@ hooks = [ ), Hook( name="backend_did_block", - doc="""Called after one or more operations are executed with mw.perform_op(). + doc="""Called after one or more DB tasks finish running in the background. Called regardless of the success of individual operations, and only called when there are no outstanding ops. """, diff --git a/rslib/i18n/build/extract.rs b/rslib/i18n/build/extract.rs index 5c78ed2e0..dc09ed095 100644 --- a/rslib/i18n/build/extract.rs +++ b/rslib/i18n/build/extract.rs @@ -163,7 +163,7 @@ impl From for Variable { let kind = match name.as_str() { "cards" | "notes" | "count" | "amount" | "reviews" | "total" | "selected" | "kilobytes" | "daysStart" | "daysEnd" | "days" | "secs-per-card" | "remaining" - | "hourStart" | "hourEnd" | "correct" | "decks" => VariableKind::Int, + | "hourStart" | "hourEnd" | "correct" | "decks" | "changed" => VariableKind::Int, "average-seconds" | "cards-per-minute" => VariableKind::Float, "val" | "found" | "expected" | "part" | "percent" | "day" | "number" | "up" | "down" | "seconds" | "megs" => VariableKind::Any,