diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 8d06cbe65..fc002eadc 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -125,6 +125,7 @@ class Card(DeprecatedNamesMixin): desired_retention=self.desired_retention, ) + @deprecated(info="please use col.update_card()") def flush(self) -> None: hooks.card_will_flush(self) if self.id != 0: diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 5b79969f1..6c0d6e258 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, Generator, Iterable, Literal, Optional, Sequence, Union, cast +from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast from anki import ( ankiweb_pb2, @@ -81,7 +81,6 @@ from anki.scheduler.dummy import DummyScheduler from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.sync import SyncAuth, SyncOutput, SyncStatus from anki.tags import TagManager -from anki.types import assert_exhaustive from anki.utils import ( from_json_bytes, ids2str, @@ -97,14 +96,6 @@ anki.latex.setup_hook() SearchJoiner = Literal["AND", "OR"] -@dataclass -class LegacyCheckpoint: - name: str - - -LegacyUndoResult = Optional[LegacyCheckpoint] - - @dataclass class DeckIdLimit: deck_id: DeckId @@ -229,7 +220,6 @@ class Collection(DeprecatedNamesMixin): def upgrade_to_v2_scheduler(self) -> None: self._backend.upgrade_scheduler() - self.clear_python_undo() self._load_scheduler() def v3_scheduler(self) -> bool: @@ -259,47 +249,20 @@ class Collection(DeprecatedNamesMixin): def mod(self) -> int: return self.db.scalar("select mod from col") - def modified_by_backend(self) -> bool: - # Until we can move away from long-running transactions, the Python - # code needs to know if the transaction should be committed, so we need - # to check if the backend updated the modification time. - return self.db.last_begin_at != self.mod - - def save(self, name: str | None = None, trx: bool = True) -> None: - "Flush, commit DB, and take out another write lock if trx=True." - # commit needed? - if self.db.modified_in_python or self.modified_by_backend(): - self.db.modified_in_python = False - self.db.commit() - if trx: - self.db.begin() - elif not trx: - # if no changes were pending but calling code expects to be - # outside of a transaction, we need to roll back - self.db.rollback() - - self._save_checkpoint(name) + @deprecated(info="saving is automatic") + def save(self, **args: Any) -> None: + pass + @deprecated(info="saving is automatic") def autosave(self) -> None: - """Save any pending changes. - If a checkpoint was taken in the last 5 minutes, don't save.""" - if not self._have_outstanding_checkpoint(): - # if there's no active checkpoint, we can save immediately - self.save() - elif time.time() - self._last_checkpoint_at > 300: - self.save() + pass def close( self, - save: bool = True, downgrade: bool = False, ) -> None: "Disconnect from DB." if self.db: - if save: - self.save(trx=False) - else: - self.db.rollback() self._clear_caches() self._backend.close_collection( downgrade_to_schema11=downgrade, @@ -309,15 +272,9 @@ class Collection(DeprecatedNamesMixin): def close_for_full_sync(self) -> None: # save and cleanup, but backend will take care of collection close if self.db: - self.save(trx=False) self._clear_caches() self.db = None - def rollback(self) -> None: - self._clear_caches() - self.db.rollback() - self.db.begin() - def _clear_caches(self) -> None: self.models._clear_cache() @@ -325,9 +282,6 @@ class Collection(DeprecatedNamesMixin): if self.db: raise Exception("reopen() called with open db") - self._last_checkpoint_at = time.time() - self._undo: _UndoInfo = None - (media_dir, media_db) = media_paths_from_col_path(self.path) # connect @@ -338,13 +292,11 @@ class Collection(DeprecatedNamesMixin): media_db_path=media_db, ) self.db = DBProxy(weakref.proxy(self._backend)) - self.db.begin() if after_full_sync: self._load_scheduler() def set_schema_modified(self) -> None: self.db.execute("update col set scm=?", int_time(1000)) - self.save() def mod_schema(self, check: bool) -> None: "Mark schema modified. GUI catches this and will ask user if required." @@ -363,12 +315,6 @@ class Collection(DeprecatedNamesMixin): else: return -1 - def legacy_checkpoint_pending(self) -> bool: - return ( - self._have_outstanding_checkpoint() - and time.time() - self._last_checkpoint_at < 300 - ) - # Import/export ########################################################################## @@ -385,19 +331,15 @@ class Collection(DeprecatedNamesMixin): Returns true if backup created. This may be false in the force=True case, if no changes have been made to the collection. - Commits any outstanding changes, which clears any active legacy checkpoint. - Throws on failure of current backup, or the previous backup if it was not awaited. """ # ensure any pending transaction from legacy code/add-ons has been committed - self.save(trx=False) created = self._backend.create_backup( backup_folder=backup_folder, force=force, wait_for_completion=wait_for_completion, ) - self.db.begin() return created def await_backup_completion(self) -> None: @@ -539,30 +481,26 @@ class Collection(DeprecatedNamesMixin): return Card(self, id) def update_cards(self, cards: Sequence[Card]) -> OpChanges: - """Save card changes to database, and add an undo entry. - Unlike card.flush(), this will invalidate any current checkpoint.""" + """Save card changes to database, and add an undo entry.""" return self._backend.update_cards( cards=[c._to_backend_card() for c in cards], skip_undo_entry=False ) def update_card(self, card: Card) -> OpChanges: - """Save card changes to database, and add an undo entry. - Unlike card.flush(), this will invalidate any current checkpoint.""" + """Save card changes to database, and add an undo entry.""" return self.update_cards([card]) def get_note(self, id: NoteId) -> Note: return Note(self, id=id) def update_notes(self, notes: Sequence[Note]) -> OpChanges: - """Save note changes to database, and add an undo entry. - Unlike note.flush(), this will invalidate any current checkpoint.""" + """Save note changes to database, and add an undo entry.""" return self._backend.update_notes( notes=[n._to_backend_note() for n in notes], skip_undo_entry=False ) def update_note(self, note: Note) -> OpChanges: - """Save note changes to database, and add an undo entry. - Unlike note.flush(), this will invalidate any current checkpoint.""" + """Save note changes to database, and add an undo entry.""" return self.update_notes([note]) # Utils @@ -577,10 +515,9 @@ class Collection(DeprecatedNamesMixin): self.conf[type] = id + 1 return id + @deprecated(info="no longer required") def reset(self) -> None: - "Rebuild the queue and reload data after DB modified." - self.autosave() - self.sched.reset() + pass # Notes ########################################################################## @@ -1061,18 +998,7 @@ class Collection(DeprecatedNamesMixin): def undo_status(self) -> UndoStatus: "Return the undo status." - # check backend first - if status := self._check_backend_undo_status(): - return status - - if not self._undo: - return UndoStatus() - - if isinstance(self._undo, LegacyCheckpoint): - return UndoStatus(undo=self._undo.name) - else: - assert_exhaustive(self._undo) - assert False + return self._check_backend_undo_status() or UndoStatus() def add_custom_undo_entry(self, name: str) -> int: """Add an empty undo entry with the given name. @@ -1096,18 +1022,9 @@ class Collection(DeprecatedNamesMixin): """ return self._backend.merge_undo_entries(target) - def clear_python_undo(self) -> None: - """Clear the Python undo state. - The backend will automatically clear backend undo state when - any SQL DML is executed, or an operation that doesn't support undo - is run.""" - self._undo = None - def undo(self) -> OpChangesAfterUndo: - """Returns result of backend undo operation, or throws UndoEmpty. - If UndoEmpty is received, caller should try undo_legacy().""" + """Returns result of backend undo operation, or throws UndoEmpty.""" out = self._backend.undo() - self.clear_python_undo() if out.changes.notetype: self.models._clear_cache() return out @@ -1115,21 +1032,10 @@ class Collection(DeprecatedNamesMixin): def redo(self) -> OpChangesAfterUndo: """Returns result of backend redo operation, or throws UndoEmpty.""" out = self._backend.redo() - self.clear_python_undo() if out.changes.notetype: self.models._clear_cache() return out - def undo_legacy(self) -> LegacyUndoResult: - "Returns None if the legacy undo queue is empty." - if isinstance(self._undo, LegacyCheckpoint): - return self._undo_checkpoint() - elif self._undo is None: - return None - else: - assert_exhaustive(self._undo) - assert False - def op_made_changes(self, changes: OpChanges) -> bool: for field in changes.DESCRIPTOR.fields: if field.name != "kind": @@ -1142,30 +1048,10 @@ class Collection(DeprecatedNamesMixin): If backend has undo available, clear the Python undo state.""" status = self._backend.get_undo_status() if status.undo or status.redo: - self.clear_python_undo() return status else: return None - def _have_outstanding_checkpoint(self) -> bool: - self._check_backend_undo_status() - return isinstance(self._undo, LegacyCheckpoint) - - def _undo_checkpoint(self) -> LegacyCheckpoint: - assert isinstance(self._undo, LegacyCheckpoint) - self.rollback() - undo = self._undo - self.clear_python_undo() - return undo - - def _save_checkpoint(self, name: str | None) -> None: - "Call via .save(). If name not provided, clear any existing checkpoint." - self._last_checkpoint_at = time.time() - if name: - self._undo = LegacyCheckpoint(name=name) - else: - self.clear_python_undo() - # DB maintenance ########################################################################## @@ -1175,7 +1061,6 @@ class Collection(DeprecatedNamesMixin): Returns tuple of (error: str, ok: bool). 'ok' will be true if no problems were found. """ - self.save(trx=False) try: problems = list(self._backend.check_database()) ok = not problems @@ -1183,19 +1068,11 @@ class Collection(DeprecatedNamesMixin): except DBError as err: problems = [str(err)] ok = False - finally: - try: - self.db.begin() - except: - # may fail if the DB is very corrupt - pass return ("\n".join(problems), ok) def optimize(self) -> None: - self.save(trx=False) self.db.execute("vacuum") self.db.execute("analyze") - self.db.begin() ########################################################################## @@ -1372,7 +1249,6 @@ class Collection(DeprecatedNamesMixin): Collection.register_deprecated_aliases( - clearUndo=Collection.clear_python_undo, findReplace=Collection.find_and_replace, remCards=Collection.remove_cards_and_orphaned_notes, ) @@ -1381,8 +1257,6 @@ Collection.register_deprecated_aliases( # legacy name _Collection = Collection -_UndoInfo = Union[LegacyCheckpoint, None] - def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit: message = import_export_pb2.ExportLimit() diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 2d0bddaed..331598aa4 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -5,10 +5,11 @@ from __future__ import annotations import re from re import Match -from typing import TYPE_CHECKING, Any, Iterable, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence, Union if TYPE_CHECKING: import anki._backend + from anki.collection import Collection # DBValue is actually Union[str, int, float, None], but if defined # that way, every call site needs to do a type check prior to using @@ -25,21 +26,29 @@ class DBProxy: def __init__(self, backend: anki._backend.RustBackend) -> None: self._backend = backend - self.modified_in_python = False - self.last_begin_at = 0 # Transactions ############### - def begin(self) -> None: - self.last_begin_at = self.scalar("select mod from col") - self._backend.db_begin() + def transact(self, op: Callable[[], None]) -> None: + """Run the provided operation inside a transaction. - def commit(self) -> None: - self._backend.db_commit() + Please note that all backend methods automatically wrap changes in a transaction, + so there is no need to use this when calling methods like update_cards(), unless + you are making other changes at the same time and want to ensure they are applied + completely or not at all. - def rollback(self) -> None: - self._backend.db_rollback() + If the operation throws an exception, the changes will be automatically rolled + back. + """ + + try: + self._backend.db_begin() + op() + self._backend.db_commit() + except BaseException as e: + self._backend.db_rollback() + raise e # Querying ################ @@ -51,11 +60,6 @@ class DBProxy: first_row_only: bool = False, **kwargs: ValueForDB, ) -> list[Row]: - # mark modified? - cananoized = sql.strip().lower() - for stmt in "insert", "update", "delete": - if cananoized.startswith(stmt): - self.modified_in_python = True sql, args2 = emulate_named_args(sql, args, kwargs) # fetch rows return self._backend.db_query(sql, args2, first_row_only) @@ -93,7 +97,6 @@ class DBProxy: ################ def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None: - self.modified_in_python = True if isinstance(args, list): list_args = args else: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 0ea73e1d8..462806028 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -424,7 +424,6 @@ class DeckManager(DeprecatedNamesMixin): # make sure arg is an int; legacy callers may be passing in a string did = DeckId(did) self.set_current(did) - self.col.reset() selected = get_current_id diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 90b1170b5..1e75adfae 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -396,7 +396,6 @@ class AnkiPackageExporter(AnkiExporter): n = c.newNote() n.fields[0] = "This file requires a newer version of Anki." c.addNote(n) - c.save() c.close(downgrade=True) zip.write(path, "collection.anki2") diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 51e439967..512584967 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -52,7 +52,7 @@ class Anki2Importer(Importer): try: self._import() finally: - self.src.close(save=False, downgrade=False) + self.src.close(downgrade=False) def _prepareFiles(self) -> None: self.source_needs_upgrade = False diff --git a/pylib/anki/media.py b/pylib/anki/media.py index f97c0ddb8..49d2a9d1f 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -179,9 +179,6 @@ class MediaManager(DeprecatedNamesMixin): def check(self) -> CheckMediaResponse: output = self.col._backend.check_media() - # files may have been renamed on disk, so an undo at this point could - # break file references - self.col.save() return output def render_all_latex( diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 1ca6f60ef..e0147ac3a 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -12,7 +12,7 @@ import anki.collection import anki.decks import anki.template from anki import hooks, notes_pb2 -from anki._legacy import DeprecatedNamesMixin +from anki._legacy import DeprecatedNamesMixin, deprecated from anki.consts import MODEL_STD from anki.models import NotetypeDict, NotetypeId, TemplateDict from anki.utils import join_fields @@ -78,9 +78,9 @@ class Note(DeprecatedNamesMixin): fields=self.fields, ) + @deprecated(info="please use col.update_note()") def flush(self) -> None: - """This preserves any current checkpoint. - For an undo entry, use col.update_note() instead.""" + """For an undo entry, use col.update_note() instead.""" if self.id == 0: raise Exception("can't flush a new note") self.col._backend.update_notes( diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index e343de155..25ee20431 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -17,6 +17,7 @@ from __future__ import annotations from typing import Literal, Optional, Sequence from anki import frontend_pb2, scheduler_pb2 +from anki._legacy import deprecated from anki.cards import Card from anki.collection import OpChanges from anki.consts import * @@ -103,6 +104,7 @@ class Scheduler(SchedulerBaseWithLegacy): # Fetching the next card (legacy API) ########################################################################## + @deprecated(info="no longer required") def reset(self) -> None: # backend automatically resets queues as operations are performed pass diff --git a/pylib/tests/test_cards.py b/pylib/tests/test_cards.py index b5f3160ad..214bafa77 100644 --- a/pylib/tests/test_cards.py +++ b/pylib/tests/test_cards.py @@ -13,7 +13,6 @@ def test_delete(): note["Back"] = "2" col.addNote(note) cid = note.cards()[0].id - col.reset() col.sched.answerCard(col.sched.getCard(), 2) col.remove_cards_and_orphaned_notes([cid]) assert col.card_count() == 0 diff --git a/pylib/tests/test_decks.py b/pylib/tests/test_decks.py index d0fb90b82..42da44e89 100644 --- a/pylib/tests/test_decks.py +++ b/pylib/tests/test_decks.py @@ -22,13 +22,11 @@ def test_basic(): assert col.decks.id("new deck") == parentId # we start with the default col selected assert col.decks.selected() == 1 - col.reset() # we can select a different col col.decks.select(parentId) assert col.decks.selected() == parentId # let's create a child childId = col.decks.id("new deck::child") - col.sched.reset() # it should have been added to the active list assert col.decks.selected() == parentId # we can select the child individually too diff --git a/pylib/tests/test_exporting.py b/pylib/tests/test_exporting.py index afc8a7c78..7946f2e45 100644 --- a/pylib/tests/test_exporting.py +++ b/pylib/tests/test_exporting.py @@ -108,7 +108,6 @@ def test_export_anki_due(): note["Front"] = "foo" col.addNote(note) col.crt -= 86400 * 10 - col.sched.reset() c = col.sched.getCard() col.sched.answerCard(c, 3) col.sched.answerCard(c, 3) @@ -130,7 +129,6 @@ def test_export_anki_due(): imp = Anki2Importer(col2, newname) imp.run() c = col2.getCard(c.id) - col2.sched.reset() assert c.due - col2.sched.today == 1 diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index 5c45c05fb..d9c2c1f87 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -46,7 +46,6 @@ def test_find_cards(): note["Front"] = "test" note["Back"] = "foo bar" col.addNote(note) - col.save() latestCardIds = [c.id for c in note.cards()] # tag searches assert len(col.find_cards("tag:*")) == 5 @@ -164,7 +163,6 @@ def test_find_cards(): col.db.execute( "update cards set did = ? where id = ?", col.decks.id("Default::Child"), id ) - col.save() assert len(col.find_cards("deck:default")) == 7 assert len(col.find_cards("deck:default::child")) == 1 assert len(col.find_cards("deck:default -deck:default::*")) == 6 diff --git a/pylib/tests/test_schedv3.py b/pylib/tests/test_schedv3.py index 6a7ead2ba..996f0450d 100644 --- a/pylib/tests/test_schedv3.py +++ b/pylib/tests/test_schedv3.py @@ -29,20 +29,17 @@ def test_clock(): def test_basics(): col = getEmptyCol() - col.reset() assert not col.sched.getCard() def test_new(): col = getEmptyCol() - col.reset() assert col.sched.newCount == 0 # add a note note = col.newNote() note["Front"] = "one" note["Back"] = "two" col.addNote(note) - col.reset() assert col.sched.newCount == 1 # fetch it c = col.sched.getCard() @@ -92,7 +89,6 @@ def test_newLimits(): # give the child deck a different configuration c2 = col.decks.add_config_returning_id("new conf") col.decks.set_config_id_for_deck_dict(col.decks.get(deck2), c2) - col.reset() # both confs have defaulted to a limit of 20 assert col.sched.newCount == 20 # first card we get comes from parent @@ -102,13 +98,11 @@ def test_newLimits(): conf1 = col.decks.config_dict_for_deck_id(1) conf1["new"]["perDay"] = 10 col.decks.save(conf1) - col.reset() assert col.sched.newCount == 10 # if we limit child to 4, we should get 9 conf2 = col.decks.config_dict_for_deck_id(deck2) conf2["new"]["perDay"] = 4 col.decks.save(conf2) - col.reset() assert col.sched.newCount == 9 @@ -117,7 +111,6 @@ def test_newBoxes(): note = col.newNote() note["Front"] = "one" col.addNote(note) - col.reset() c = col.sched.getCard() conf = col.sched._cardConf(c) conf["new"]["delays"] = [1, 2, 3, 4, 5] @@ -138,7 +131,6 @@ def test_learn(): col.addNote(note) # set as a new card and rebuild queues col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") - col.reset() # sched.getCard should return it, since it's due in the past c = col.sched.getCard() assert c @@ -182,7 +174,6 @@ def test_learn(): c.type = CARD_TYPE_NEW c.queue = QUEUE_TYPE_LRN c.flush() - col.sched.reset() col.sched.answerCard(c, 4) assert c.type == CARD_TYPE_REV assert c.queue == QUEUE_TYPE_REV @@ -203,7 +194,6 @@ def test_relearn(): c.flush() # fail the card - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 1) assert c.queue == QUEUE_TYPE_LRN @@ -234,7 +224,6 @@ def test_relearn_no_steps(): col.decks.save(conf) # fail the card - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 1) assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV @@ -251,7 +240,6 @@ def test_learn_collapsed(): col.addNote(note) # set as a new card and rebuild queues col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") - col.reset() # should get '1' first c = col.sched.getCard() assert c.question().endswith("1") @@ -276,7 +264,6 @@ def test_learn_day(): note = col.newNote() note["Front"] = "two" col.addNote(note) - col.sched.reset() c = col.sched.getCard() conf = col.sched._cardConf(c) conf["new"]["delays"] = [1, 10, 1440, 2880] @@ -300,7 +287,6 @@ def test_learn_day(): # for testing, move it back a day c.due -= 1 c.flush() - col.reset() assert col.sched.counts() == (0, 1, 0) c = col.sched.getCard() # nextIvl should work @@ -309,13 +295,11 @@ def test_learn_day(): col.sched.answerCard(c, 1) assert c.queue == QUEUE_TYPE_LRN col.undo() - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 3) # simulate the passing of another two days c.due -= 2 c.flush() - col.reset() # the last pass should graduate it into a review card assert ni(c, 3) == 86400 col.sched.answerCard(c, 3) @@ -324,7 +308,6 @@ def test_learn_day(): # correctly c.due = 0 c.flush() - col.reset() assert col.sched.counts() == (0, 0, 1) conf = col.sched._cardConf(c) conf["lapse"]["delays"] = [1440] @@ -359,7 +342,6 @@ def test_reviews(): ################################################## c = copy.copy(cardcopy) c.flush() - col.reset() col.sched.answerCard(c, 2) assert c.queue == QUEUE_TYPE_REV # the new interval should be (100) * 1.2 = 120 @@ -477,7 +459,6 @@ def test_button_spacing(): c.ivl = 1 c.start_timer() c.flush() - col.reset() ni = col.sched.nextIvlStr wo = without_unicode_isolation assert wo(ni(c, 2)) == "2d" @@ -491,47 +472,12 @@ def test_button_spacing(): assert wo(ni(c, 2)) == "1d" -def test_overdue_lapse(): - # disabled in commit 3069729776990980f34c25be66410e947e9d51a2 - return - col = getEmptyCol() # pylint: disable=unreachable - # add a note - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # simulate a review that was lapsed and is now due for its normal review - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_LRN - c.due = -1 - c.odue = -1 - c.factor = STARTING_FACTOR - c.left = 2002 - c.ivl = 0 - c.flush() - # checkpoint - col.save() - col.sched.reset() - assert col.sched.counts() == (0, 2, 0) - c = col.sched.getCard() - col.sched.answerCard(c, 3) - # it should be due tomorrow - assert c.due == col.sched.today + 1 - # revert to before - col.rollback() - # with the default settings, the overdue card should be removed from the - # learning queue - col.sched.reset() - assert col.sched.counts() == (0, 0, 1) - - def test_nextIvl(): col = getEmptyCol() note = col.newNote() note["Front"] = "one" note["Back"] = "two" col.addNote(note) - col.reset() conf = col.decks.config_dict_for_deck_id(1) conf["new"]["delays"] = [0.5, 3, 10] conf["lapse"]["delays"] = [1, 5, 9] @@ -611,7 +557,6 @@ def test_bury(): c2.load() assert c2.queue == QUEUE_TYPE_SIBLING_BURIED - col.reset() assert not col.sched.getCard() col.sched.unbury_deck(deck_id=col.decks.get_current_id(), mode=UnburyDeck.USER_ONLY) @@ -629,8 +574,6 @@ def test_bury(): col.sched.bury_cards([c.id, c2.id]) col.sched.unbury_deck(deck_id=col.decks.get_current_id()) - col.reset() - assert col.sched.counts() == (2, 0, 0) @@ -641,14 +584,11 @@ def test_suspend(): col.addNote(note) c = note.cards()[0] # suspending - col.reset() assert col.sched.getCard() col.sched.suspend_cards([c.id]) - col.reset() assert not col.sched.getCard() # unsuspending col.sched.unsuspend_cards([c.id]) - col.reset() assert col.sched.getCard() # should cope with rev cards being relearnt c.due = 0 @@ -656,7 +596,6 @@ def test_suspend(): c.type = CARD_TYPE_REV c.queue = QUEUE_TYPE_REV c.flush() - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 1) assert c.due >= time.time() @@ -699,12 +638,10 @@ def test_filt_reviewing_early_normal(): c.factor = STARTING_FACTOR c.start_timer() c.flush() - col.reset() assert col.sched.counts() == (0, 0, 0) # create a dynamic deck and refresh it did = col.decks.new_filtered("Cram") col.sched.rebuild_filtered_deck(did) - col.reset() # should appear as normal in the deck list assert sorted(col.sched.deck_due_tree().children)[0].review_count == 1 # and should appear in the counts @@ -731,7 +668,6 @@ def test_filt_reviewing_early_normal(): c.due = col.sched.today + 75 c.flush() col.sched.rebuild_filtered_deck(did) - col.reset() c = col.sched.getCard() assert col.sched.nextIvl(c, 2) == 100 * 1.2 / 2 * 86400 @@ -763,7 +699,6 @@ def test_filt_keep_lrn_state(): # create a dynamic deck and refresh it did = col.decks.new_filtered("Cram") col.sched.rebuild_filtered_deck(did) - col.reset() # card should still be in learning state c.load() @@ -800,7 +735,6 @@ def test_preview(): cram["resched"] = False col.decks.save(cram) col.sched.rebuild_filtered_deck(did) - col.reset() # grab the first card c = col.sched.getCard() @@ -860,7 +794,6 @@ def test_ordcycle(): conf = col.decks.get_config(1) conf["new"]["bury"] = False col.decks.save(conf) - col.reset() # ordinals should arrive in order for i in range(3): @@ -879,7 +812,6 @@ def test_counts_idx_new(): note["Front"] = "two" note["Back"] = "two" col.addNote(note) - col.reset() assert col.sched.counts() == (2, 0, 0) c = col.sched.getCard() # getCard does not decrement counts @@ -903,7 +835,6 @@ def test_repCounts(): note = col.newNote() note["Front"] = "two" col.addNote(note) - col.reset() # lrnReps should be accurate on pass/fail assert col.sched.counts() == (2, 0, 0) col.sched.answerCard(col.sched.getCard(), 1) @@ -924,7 +855,6 @@ def test_repCounts(): note = col.newNote() note["Front"] = "four" col.addNote(note) - col.reset() # initial pass and immediate graduate should be correct too assert col.sched.counts() == (2, 0, 0) col.sched.answerCard(col.sched.getCard(), 3) @@ -945,7 +875,6 @@ def test_repCounts(): note = col.newNote() note["Front"] = "six" col.addNote(note) - col.reset() assert col.sched.counts() == (1, 0, 1) col.sched.answerCard(col.sched.getCard(), 1) assert col.sched.counts() == (1, 1, 0) @@ -964,7 +893,6 @@ def test_timing(): c.due = 0 c.flush() # fail the first one - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 1) # the next card should be another review @@ -973,7 +901,6 @@ def test_timing(): # if the failed card becomes due, it should show first c.due = int_time() - 1 c.flush() - col.reset() c = col.sched.getCard() assert c.queue == QUEUE_TYPE_LRN @@ -988,7 +915,6 @@ def test_collapse(): note = col.newNote() note["Front"] = "two" col.addNote(note) - col.reset() # first note c = col.sched.getCard() col.sched.answerCard(c, 1) @@ -1032,7 +958,6 @@ def test_deckDue(): note["Front"] = "three" note.note_type()["did"] = col.decks.id("foo::baz") col.addNote(note) - col.reset() assert len(col.decks.all_names_and_ids()) == 5 tree = col.sched.deck_due_tree().children assert tree[0].name == "Default" @@ -1078,7 +1003,6 @@ def test_deckFlow(): note["Front"] = "three" default1 = note.note_type()["did"] = col.decks.id("Default::1") col.addNote(note) - col.reset() assert col.sched.counts() == (3, 0, 0) # should get top level one first, then ::1, then ::2 for i in "one", "three", "two": @@ -1142,10 +1066,8 @@ def test_forget(): c.ivl = 100 c.due = 0 c.flush() - col.reset() assert col.sched.counts() == (0, 0, 1) col.sched.forgetCards([c.id]) - col.reset() assert col.sched.counts() == (1, 0, 0) @@ -1183,7 +1105,6 @@ def test_norelearn(): c.ivl = 100 c.start_timer() c.flush() - col.reset() col.sched.answerCard(c, 1) col.sched._cardConf(c)["lapse"]["delays"] = [] col.sched.answerCard(c, 1) @@ -1235,7 +1156,6 @@ def test_negativeDueFilter(): did = col.decks.new_filtered("Cram") col.sched.rebuild_filtered_deck(did) col.sched.empty_filtered_deck(did) - col.reset() c.load() assert c.due == -5 @@ -1250,7 +1170,6 @@ def test_initial_repeat(): note["Back"] = "two" col.addNote(note) - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 2) # should be due in ~ 5.5 mins diff --git a/pylib/tests/test_stats.py b/pylib/tests/test_stats.py index 1fd9b27de..7b657eab6 100644 --- a/pylib/tests/test_stats.py +++ b/pylib/tests/test_stats.py @@ -17,7 +17,6 @@ def test_stats(): # card stats card_stats = col.card_stats_data(c.id) assert card_stats.note_id == note.id - col.reset() c = col.sched.getCard() col.sched.answerCard(c, 3) col.sched.answerCard(c, 2) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 56b6fcaba..b28813196 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -38,7 +38,6 @@ class DeckConf(QDialog): self.form = aqt.forms.dconf.Ui_Dialog() self.form.setupUi(self) gui_hooks.deck_conf_did_setup_ui_form(self) - self.mw.checkpoint(tr.actions_options()) self.setupCombos() self.setupConfs() qconnect( diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index b7e5f49a3..8ba173f91 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -193,7 +193,6 @@ class ImportDialog(QDialog): ) self.mw.col.models.save(self.importer.model, updateReqs=False) self.mw.progress.start() - self.mw.checkpoint(tr.actions_import()) def on_done(future: Future) -> None: self.mw.progress.finish() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 57b124d15..e4cb3b30e 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -28,6 +28,7 @@ import aqt.toolbar import aqt.webview from anki import hooks from anki._backend import RustBackend as _RustBackend +from anki._legacy import deprecated from anki.collection import Collection, Config, OpChanges, UndoStatus from anki.decks import DeckDict, DeckId from anki.hooks import runHook @@ -918,9 +919,7 @@ title="{}" {}>{}""".format( signal.signal(signal.SIGTERM, self.onUnixSignal) def onUnixSignal(self, signum: Any, frame: Any) -> None: - # schedule a rollback & quit def quit() -> None: - self.col.db.rollback() self.close() self.progress.single_shot(100, quit) @@ -1021,7 +1020,6 @@ title="{}" {}>{}""".format( "Caller should ensure auth available." def on_collection_sync_finished() -> None: - self.col.clear_python_undo() self.col.models._clear_cache() gui_hooks.sync_did_finish() self.reset() @@ -1189,15 +1187,14 @@ title="{}" {}>{}""".format( self.form.actionRedo.setVisible(info.show_redo) gui_hooks.undo_state_did_change(info) + @deprecated(info="checkpoints are no longer supported") def checkpoint(self, name: str) -> None: - self.col.save(name) - self.update_undo_actions() + pass + @deprecated(info="saving is automatic") def autosave(self) -> None: - self.col.autosave() - self.update_undo_actions() + pass - maybeEnableUndo = update_undo_actions onUndo = undo # Other menu operations @@ -1213,7 +1210,6 @@ title="{}" {}>{}""".format( aqt.dialogs.open("EditCurrent", self) def onOverview(self) -> None: - self.col.reset() self.moveToState("overview") def onStats(self) -> None: @@ -1461,10 +1457,6 @@ title="{}" {}>{}""".format( ) def _create_backup_with_progress(self, user_initiated: bool) -> None: - # if there's a legacy undo op, try again later - if not user_initiated and self.col.legacy_checkpoint_pending(): - return - # The initial copy will display a progress window if it takes too long def backup(col: Collection) -> bool: return col.create_backup( @@ -1483,7 +1475,6 @@ title="{}" {}>{}""".format( ) def after_backup_started(created: bool) -> None: - # Legacy checkpoint may have expired. self.update_undo_actions() if user_initiated and not created: diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index 577184a92..865c8a03f 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -148,7 +148,6 @@ def on_op_finished( mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None ) -> None: mw.update_undo_actions() - mw.autosave() if isinstance(result, OpChanges): changes = result diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index 6984bf42b..d36984163 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -3,13 +3,12 @@ from __future__ import annotations -from anki.collection import LegacyCheckpoint, OpChanges, OpChangesAfterUndo, Preferences +from anki.collection import OpChanges, OpChangesAfterUndo, Preferences from anki.errors import UndoEmpty -from anki.types import assert_exhaustive from aqt import gui_hooks from aqt.operations import CollectionOp from aqt.qt import QWidget -from aqt.utils import showInfo, showWarning, tooltip, tr +from aqt.utils import showWarning, tooltip, tr def undo(*, parent: QWidget) -> None: @@ -20,10 +19,7 @@ def undo(*, parent: QWidget) -> None: tooltip(tr.undo_action_undone(action=out.operation), parent=parent) def on_failure(exc: Exception) -> None: - if isinstance(exc, UndoEmpty): - # backend has no undo, but there may be a checkpoint waiting - _legacy_undo(parent=parent) - else: + if not isinstance(exc, UndoEmpty): showWarning(str(exc), parent=parent) CollectionOp(parent, lambda col: col.undo()).success(on_success).failure( @@ -40,35 +36,6 @@ def redo(*, parent: QWidget) -> None: CollectionOp(parent, lambda col: col.redo()).success(on_success).run_in_background() -def _legacy_undo(*, parent: QWidget) -> None: - from aqt import mw - - assert mw - assert mw.col - - result = mw.col.undo_legacy() - - if result is None: - # should not happen - showInfo(tr.actions_nothing_to_undo(), parent=parent) - mw.update_undo_actions() - return - - elif isinstance(result, LegacyCheckpoint): - name = result.name - - else: - assert_exhaustive(result) - assert False - - # full queue+gui reset required - mw.reset() - - tooltip(tr.undo_action_undone(action=name), parent=parent) - gui_hooks.state_did_revert(name) - mw.update_undo_actions() - - def set_preferences( *, parent: QWidget, preferences: Preferences ) -> CollectionOp[OpChanges]: diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 6bfb58f79..44e1c2e67 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -64,7 +64,6 @@ class Overview: def refresh(self) -> None: def success(_counts: tuple) -> None: self._refresh_needed = False - self.mw.col.reset() self._renderPage() self._renderBottom() self.mw.web.setFocus() diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 084dc0b4f..7a698e65e 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -325,7 +325,7 @@ class ProfileManager: continue try: c = Collection(path) - c.close(save=False, downgrade=True) + c.close(downgrade=True) except Exception as e: print(e) problem_profiles.append(name) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 0aea5ce06..f027da5cc 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -178,7 +178,6 @@ class Reviewer: def refresh_if_needed(self) -> None: if self._refresh_needed is RefreshNeeded.QUEUES: - self.mw.col.reset() self.nextCard() self.mw.fade_in_webview() self._refresh_needed = None @@ -444,7 +443,6 @@ class Reviewer: def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None: gui_hooks.reviewer_did_answer_card(self, self.card, ease) self._answeredIds.append(self.card.id) - self.mw.autosave() if not self.check_timebox(): self.nextCard() diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 9a17091f8..051baa483 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -98,7 +98,6 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: timer.start(150) def on_future_done(fut: Future[SyncOutput]) -> None: - mw.col.db.begin() # scheduler version may have changed mw.col._load_scheduler() timer.stop() @@ -121,7 +120,6 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: else: full_sync(mw, out, on_done) - mw.col.save(trx=False) mw.taskman.with_progress( lambda: mw.col.sync_collection(auth, mw.pm.media_syncing_enabled()), on_future_done, diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 13660c029..bb7cf10dc 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -602,12 +602,6 @@ hooks = [ ), # UI state/refreshing ################### - Hook( - name="state_did_revert", - args=["action: str"], - legacy_hook="revertedState", - doc="Legacy hook, called after undoing.", - ), Hook( name="state_did_undo", args=["changes: OpChangesAfterUndo"],