Drop support for checkpoints (#2742)

* Drop support for checkpoints

* Deprecate .flush()

* Remove .begin/.commit

* Remove rollback() and deprecate save/autosave/reset()

There's no need to commit anymore, as the Rust code is handling
transactions for us.

* Add safer transact() method

This will ensure add-on authors can't accidentally leave a transaction
open, leading to data loss.

---------

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
This commit is contained in:
Abdo 2023-10-17 05:43:34 +03:00 committed by GitHub
parent 4c17a6964e
commit b23f17df27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 49 additions and 319 deletions

View file

@ -125,6 +125,7 @@ class Card(DeprecatedNamesMixin):
desired_retention=self.desired_retention, desired_retention=self.desired_retention,
) )
@deprecated(info="please use col.update_card()")
def flush(self) -> None: def flush(self) -> None:
hooks.card_will_flush(self) hooks.card_will_flush(self)
if self.id != 0: if self.id != 0:

View file

@ -3,7 +3,7 @@
from __future__ import annotations 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 ( from anki import (
ankiweb_pb2, ankiweb_pb2,
@ -81,7 +81,6 @@ from anki.scheduler.dummy import DummyScheduler
from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.sync import SyncAuth, SyncOutput, SyncStatus from anki.sync import SyncAuth, SyncOutput, SyncStatus
from anki.tags import TagManager from anki.tags import TagManager
from anki.types import assert_exhaustive
from anki.utils import ( from anki.utils import (
from_json_bytes, from_json_bytes,
ids2str, ids2str,
@ -97,14 +96,6 @@ anki.latex.setup_hook()
SearchJoiner = Literal["AND", "OR"] SearchJoiner = Literal["AND", "OR"]
@dataclass
class LegacyCheckpoint:
name: str
LegacyUndoResult = Optional[LegacyCheckpoint]
@dataclass @dataclass
class DeckIdLimit: class DeckIdLimit:
deck_id: DeckId deck_id: DeckId
@ -229,7 +220,6 @@ class Collection(DeprecatedNamesMixin):
def upgrade_to_v2_scheduler(self) -> None: def upgrade_to_v2_scheduler(self) -> None:
self._backend.upgrade_scheduler() self._backend.upgrade_scheduler()
self.clear_python_undo()
self._load_scheduler() self._load_scheduler()
def v3_scheduler(self) -> bool: def v3_scheduler(self) -> bool:
@ -259,47 +249,20 @@ class Collection(DeprecatedNamesMixin):
def mod(self) -> int: def mod(self) -> int:
return self.db.scalar("select mod from col") return self.db.scalar("select mod from col")
def modified_by_backend(self) -> bool: @deprecated(info="saving is automatic")
# Until we can move away from long-running transactions, the Python def save(self, **args: Any) -> None:
# code needs to know if the transaction should be committed, so we need pass
# 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 autosave(self) -> None: def autosave(self) -> None:
"""Save any pending changes. pass
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()
def close( def close(
self, self,
save: bool = True,
downgrade: bool = False, downgrade: bool = False,
) -> None: ) -> None:
"Disconnect from DB." "Disconnect from DB."
if self.db: if self.db:
if save:
self.save(trx=False)
else:
self.db.rollback()
self._clear_caches() self._clear_caches()
self._backend.close_collection( self._backend.close_collection(
downgrade_to_schema11=downgrade, downgrade_to_schema11=downgrade,
@ -309,15 +272,9 @@ class Collection(DeprecatedNamesMixin):
def close_for_full_sync(self) -> None: def close_for_full_sync(self) -> None:
# save and cleanup, but backend will take care of collection close # save and cleanup, but backend will take care of collection close
if self.db: if self.db:
self.save(trx=False)
self._clear_caches() self._clear_caches()
self.db = None self.db = None
def rollback(self) -> None:
self._clear_caches()
self.db.rollback()
self.db.begin()
def _clear_caches(self) -> None: def _clear_caches(self) -> None:
self.models._clear_cache() self.models._clear_cache()
@ -325,9 +282,6 @@ class Collection(DeprecatedNamesMixin):
if self.db: if self.db:
raise Exception("reopen() called with open 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) (media_dir, media_db) = media_paths_from_col_path(self.path)
# connect # connect
@ -338,13 +292,11 @@ class Collection(DeprecatedNamesMixin):
media_db_path=media_db, media_db_path=media_db,
) )
self.db = DBProxy(weakref.proxy(self._backend)) self.db = DBProxy(weakref.proxy(self._backend))
self.db.begin()
if after_full_sync: if after_full_sync:
self._load_scheduler() self._load_scheduler()
def set_schema_modified(self) -> None: def set_schema_modified(self) -> None:
self.db.execute("update col set scm=?", int_time(1000)) self.db.execute("update col set scm=?", int_time(1000))
self.save()
def mod_schema(self, check: bool) -> None: def mod_schema(self, check: bool) -> None:
"Mark schema modified. GUI catches this and will ask user if required." "Mark schema modified. GUI catches this and will ask user if required."
@ -363,12 +315,6 @@ class Collection(DeprecatedNamesMixin):
else: else:
return -1 return -1
def legacy_checkpoint_pending(self) -> bool:
return (
self._have_outstanding_checkpoint()
and time.time() - self._last_checkpoint_at < 300
)
# Import/export # Import/export
########################################################################## ##########################################################################
@ -385,19 +331,15 @@ class Collection(DeprecatedNamesMixin):
Returns true if backup created. This may be false in the force=True case, Returns true if backup created. This may be false in the force=True case,
if no changes have been made to the collection. 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 Throws on failure of current backup, or the previous backup if it was not
awaited. awaited.
""" """
# ensure any pending transaction from legacy code/add-ons has been committed # ensure any pending transaction from legacy code/add-ons has been committed
self.save(trx=False)
created = self._backend.create_backup( created = self._backend.create_backup(
backup_folder=backup_folder, backup_folder=backup_folder,
force=force, force=force,
wait_for_completion=wait_for_completion, wait_for_completion=wait_for_completion,
) )
self.db.begin()
return created return created
def await_backup_completion(self) -> None: def await_backup_completion(self) -> None:
@ -539,30 +481,26 @@ class Collection(DeprecatedNamesMixin):
return Card(self, id) return Card(self, id)
def update_cards(self, cards: Sequence[Card]) -> OpChanges: def update_cards(self, cards: Sequence[Card]) -> OpChanges:
"""Save card changes to database, and add an undo entry. """Save card changes to database, and add an undo entry."""
Unlike card.flush(), this will invalidate any current checkpoint."""
return self._backend.update_cards( return self._backend.update_cards(
cards=[c._to_backend_card() for c in cards], skip_undo_entry=False cards=[c._to_backend_card() for c in cards], skip_undo_entry=False
) )
def update_card(self, card: Card) -> OpChanges: def update_card(self, card: Card) -> OpChanges:
"""Save card changes to database, and add an undo entry. """Save card changes to database, and add an undo entry."""
Unlike card.flush(), this will invalidate any current checkpoint."""
return self.update_cards([card]) return self.update_cards([card])
def get_note(self, id: NoteId) -> Note: def get_note(self, id: NoteId) -> Note:
return Note(self, id=id) return Note(self, id=id)
def update_notes(self, notes: Sequence[Note]) -> OpChanges: def update_notes(self, notes: Sequence[Note]) -> OpChanges:
"""Save note changes to database, and add an undo entry. """Save note changes to database, and add an undo entry."""
Unlike note.flush(), this will invalidate any current checkpoint."""
return self._backend.update_notes( return self._backend.update_notes(
notes=[n._to_backend_note() for n in notes], skip_undo_entry=False notes=[n._to_backend_note() for n in notes], skip_undo_entry=False
) )
def update_note(self, note: Note) -> OpChanges: def update_note(self, note: Note) -> OpChanges:
"""Save note changes to database, and add an undo entry. """Save note changes to database, and add an undo entry."""
Unlike note.flush(), this will invalidate any current checkpoint."""
return self.update_notes([note]) return self.update_notes([note])
# Utils # Utils
@ -577,10 +515,9 @@ class Collection(DeprecatedNamesMixin):
self.conf[type] = id + 1 self.conf[type] = id + 1
return id return id
@deprecated(info="no longer required")
def reset(self) -> None: def reset(self) -> None:
"Rebuild the queue and reload data after DB modified." pass
self.autosave()
self.sched.reset()
# Notes # Notes
########################################################################## ##########################################################################
@ -1061,18 +998,7 @@ class Collection(DeprecatedNamesMixin):
def undo_status(self) -> UndoStatus: def undo_status(self) -> UndoStatus:
"Return the undo status." "Return the undo status."
# check backend first return self._check_backend_undo_status() or UndoStatus()
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
def add_custom_undo_entry(self, name: str) -> int: def add_custom_undo_entry(self, name: str) -> int:
"""Add an empty undo entry with the given name. """Add an empty undo entry with the given name.
@ -1096,18 +1022,9 @@ class Collection(DeprecatedNamesMixin):
""" """
return self._backend.merge_undo_entries(target) 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: def undo(self) -> OpChangesAfterUndo:
"""Returns result of backend undo operation, or throws UndoEmpty. """Returns result of backend undo operation, or throws UndoEmpty."""
If UndoEmpty is received, caller should try undo_legacy()."""
out = self._backend.undo() out = self._backend.undo()
self.clear_python_undo()
if out.changes.notetype: if out.changes.notetype:
self.models._clear_cache() self.models._clear_cache()
return out return out
@ -1115,21 +1032,10 @@ class Collection(DeprecatedNamesMixin):
def redo(self) -> OpChangesAfterUndo: def redo(self) -> OpChangesAfterUndo:
"""Returns result of backend redo operation, or throws UndoEmpty.""" """Returns result of backend redo operation, or throws UndoEmpty."""
out = self._backend.redo() out = self._backend.redo()
self.clear_python_undo()
if out.changes.notetype: if out.changes.notetype:
self.models._clear_cache() self.models._clear_cache()
return out 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: def op_made_changes(self, changes: OpChanges) -> bool:
for field in changes.DESCRIPTOR.fields: for field in changes.DESCRIPTOR.fields:
if field.name != "kind": if field.name != "kind":
@ -1142,30 +1048,10 @@ class Collection(DeprecatedNamesMixin):
If backend has undo available, clear the Python undo state.""" If backend has undo available, clear the Python undo state."""
status = self._backend.get_undo_status() status = self._backend.get_undo_status()
if status.undo or status.redo: if status.undo or status.redo:
self.clear_python_undo()
return status return status
else: else:
return None 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 # DB maintenance
########################################################################## ##########################################################################
@ -1175,7 +1061,6 @@ class Collection(DeprecatedNamesMixin):
Returns tuple of (error: str, ok: bool). 'ok' will be true if no Returns tuple of (error: str, ok: bool). 'ok' will be true if no
problems were found. problems were found.
""" """
self.save(trx=False)
try: try:
problems = list(self._backend.check_database()) problems = list(self._backend.check_database())
ok = not problems ok = not problems
@ -1183,19 +1068,11 @@ class Collection(DeprecatedNamesMixin):
except DBError as err: except DBError as err:
problems = [str(err)] problems = [str(err)]
ok = False ok = False
finally:
try:
self.db.begin()
except:
# may fail if the DB is very corrupt
pass
return ("\n".join(problems), ok) return ("\n".join(problems), ok)
def optimize(self) -> None: def optimize(self) -> None:
self.save(trx=False)
self.db.execute("vacuum") self.db.execute("vacuum")
self.db.execute("analyze") self.db.execute("analyze")
self.db.begin()
########################################################################## ##########################################################################
@ -1372,7 +1249,6 @@ class Collection(DeprecatedNamesMixin):
Collection.register_deprecated_aliases( Collection.register_deprecated_aliases(
clearUndo=Collection.clear_python_undo,
findReplace=Collection.find_and_replace, findReplace=Collection.find_and_replace,
remCards=Collection.remove_cards_and_orphaned_notes, remCards=Collection.remove_cards_and_orphaned_notes,
) )
@ -1381,8 +1257,6 @@ Collection.register_deprecated_aliases(
# legacy name # legacy name
_Collection = Collection _Collection = Collection
_UndoInfo = Union[LegacyCheckpoint, None]
def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit: def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit:
message = import_export_pb2.ExportLimit() message = import_export_pb2.ExportLimit()

View file

@ -5,10 +5,11 @@ from __future__ import annotations
import re import re
from re import Match 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: if TYPE_CHECKING:
import anki._backend import anki._backend
from anki.collection import Collection
# DBValue is actually Union[str, int, float, None], but if defined # 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 # 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: def __init__(self, backend: anki._backend.RustBackend) -> None:
self._backend = backend self._backend = backend
self.modified_in_python = False
self.last_begin_at = 0
# Transactions # Transactions
############### ###############
def begin(self) -> None: def transact(self, op: Callable[[], None]) -> None:
self.last_begin_at = self.scalar("select mod from col") """Run the provided operation inside a transaction.
self._backend.db_begin()
def commit(self) -> None: Please note that all backend methods automatically wrap changes in a transaction,
self._backend.db_commit() 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: If the operation throws an exception, the changes will be automatically rolled
self._backend.db_rollback() back.
"""
try:
self._backend.db_begin()
op()
self._backend.db_commit()
except BaseException as e:
self._backend.db_rollback()
raise e
# Querying # Querying
################ ################
@ -51,11 +60,6 @@ class DBProxy:
first_row_only: bool = False, first_row_only: bool = False,
**kwargs: ValueForDB, **kwargs: ValueForDB,
) -> list[Row]: ) -> 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) sql, args2 = emulate_named_args(sql, args, kwargs)
# fetch rows # fetch rows
return self._backend.db_query(sql, args2, first_row_only) 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: def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None:
self.modified_in_python = True
if isinstance(args, list): if isinstance(args, list):
list_args = args list_args = args
else: else:

View file

@ -424,7 +424,6 @@ class DeckManager(DeprecatedNamesMixin):
# make sure arg is an int; legacy callers may be passing in a string # make sure arg is an int; legacy callers may be passing in a string
did = DeckId(did) did = DeckId(did)
self.set_current(did) self.set_current(did)
self.col.reset()
selected = get_current_id selected = get_current_id

View file

@ -396,7 +396,6 @@ class AnkiPackageExporter(AnkiExporter):
n = c.newNote() n = c.newNote()
n.fields[0] = "This file requires a newer version of Anki." n.fields[0] = "This file requires a newer version of Anki."
c.addNote(n) c.addNote(n)
c.save()
c.close(downgrade=True) c.close(downgrade=True)
zip.write(path, "collection.anki2") zip.write(path, "collection.anki2")

View file

@ -52,7 +52,7 @@ class Anki2Importer(Importer):
try: try:
self._import() self._import()
finally: finally:
self.src.close(save=False, downgrade=False) self.src.close(downgrade=False)
def _prepareFiles(self) -> None: def _prepareFiles(self) -> None:
self.source_needs_upgrade = False self.source_needs_upgrade = False

View file

@ -179,9 +179,6 @@ class MediaManager(DeprecatedNamesMixin):
def check(self) -> CheckMediaResponse: def check(self) -> CheckMediaResponse:
output = self.col._backend.check_media() 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 return output
def render_all_latex( def render_all_latex(

View file

@ -12,7 +12,7 @@ import anki.collection
import anki.decks import anki.decks
import anki.template import anki.template
from anki import hooks, notes_pb2 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.consts import MODEL_STD
from anki.models import NotetypeDict, NotetypeId, TemplateDict from anki.models import NotetypeDict, NotetypeId, TemplateDict
from anki.utils import join_fields from anki.utils import join_fields
@ -78,9 +78,9 @@ class Note(DeprecatedNamesMixin):
fields=self.fields, fields=self.fields,
) )
@deprecated(info="please use col.update_note()")
def flush(self) -> None: 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: if self.id == 0:
raise Exception("can't flush a new note") raise Exception("can't flush a new note")
self.col._backend.update_notes( self.col._backend.update_notes(

View file

@ -17,6 +17,7 @@ from __future__ import annotations
from typing import Literal, Optional, Sequence from typing import Literal, Optional, Sequence
from anki import frontend_pb2, scheduler_pb2 from anki import frontend_pb2, scheduler_pb2
from anki._legacy import deprecated
from anki.cards import Card from anki.cards import Card
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.consts import * from anki.consts import *
@ -103,6 +104,7 @@ class Scheduler(SchedulerBaseWithLegacy):
# Fetching the next card (legacy API) # Fetching the next card (legacy API)
########################################################################## ##########################################################################
@deprecated(info="no longer required")
def reset(self) -> None: def reset(self) -> None:
# backend automatically resets queues as operations are performed # backend automatically resets queues as operations are performed
pass pass

View file

@ -13,7 +13,6 @@ def test_delete():
note["Back"] = "2" note["Back"] = "2"
col.addNote(note) col.addNote(note)
cid = note.cards()[0].id cid = note.cards()[0].id
col.reset()
col.sched.answerCard(col.sched.getCard(), 2) col.sched.answerCard(col.sched.getCard(), 2)
col.remove_cards_and_orphaned_notes([cid]) col.remove_cards_and_orphaned_notes([cid])
assert col.card_count() == 0 assert col.card_count() == 0

View file

@ -22,13 +22,11 @@ def test_basic():
assert col.decks.id("new deck") == parentId assert col.decks.id("new deck") == parentId
# we start with the default col selected # we start with the default col selected
assert col.decks.selected() == 1 assert col.decks.selected() == 1
col.reset()
# we can select a different col # we can select a different col
col.decks.select(parentId) col.decks.select(parentId)
assert col.decks.selected() == parentId assert col.decks.selected() == parentId
# let's create a child # let's create a child
childId = col.decks.id("new deck::child") childId = col.decks.id("new deck::child")
col.sched.reset()
# it should have been added to the active list # it should have been added to the active list
assert col.decks.selected() == parentId assert col.decks.selected() == parentId
# we can select the child individually too # we can select the child individually too

View file

@ -108,7 +108,6 @@ def test_export_anki_due():
note["Front"] = "foo" note["Front"] = "foo"
col.addNote(note) col.addNote(note)
col.crt -= 86400 * 10 col.crt -= 86400 * 10
col.sched.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 3) col.sched.answerCard(c, 3)
col.sched.answerCard(c, 3) col.sched.answerCard(c, 3)
@ -130,7 +129,6 @@ def test_export_anki_due():
imp = Anki2Importer(col2, newname) imp = Anki2Importer(col2, newname)
imp.run() imp.run()
c = col2.getCard(c.id) c = col2.getCard(c.id)
col2.sched.reset()
assert c.due - col2.sched.today == 1 assert c.due - col2.sched.today == 1

View file

@ -46,7 +46,6 @@ def test_find_cards():
note["Front"] = "test" note["Front"] = "test"
note["Back"] = "foo bar" note["Back"] = "foo bar"
col.addNote(note) col.addNote(note)
col.save()
latestCardIds = [c.id for c in note.cards()] latestCardIds = [c.id for c in note.cards()]
# tag searches # tag searches
assert len(col.find_cards("tag:*")) == 5 assert len(col.find_cards("tag:*")) == 5
@ -164,7 +163,6 @@ def test_find_cards():
col.db.execute( col.db.execute(
"update cards set did = ? where id = ?", col.decks.id("Default::Child"), id "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")) == 7
assert len(col.find_cards("deck:default::child")) == 1 assert len(col.find_cards("deck:default::child")) == 1
assert len(col.find_cards("deck:default -deck:default::*")) == 6 assert len(col.find_cards("deck:default -deck:default::*")) == 6

View file

@ -29,20 +29,17 @@ def test_clock():
def test_basics(): def test_basics():
col = getEmptyCol() col = getEmptyCol()
col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
def test_new(): def test_new():
col = getEmptyCol() col = getEmptyCol()
col.reset()
assert col.sched.newCount == 0 assert col.sched.newCount == 0
# add a note # add a note
note = col.newNote() note = col.newNote()
note["Front"] = "one" note["Front"] = "one"
note["Back"] = "two" note["Back"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
assert col.sched.newCount == 1 assert col.sched.newCount == 1
# fetch it # fetch it
c = col.sched.getCard() c = col.sched.getCard()
@ -92,7 +89,6 @@ def test_newLimits():
# give the child deck a different configuration # give the child deck a different configuration
c2 = col.decks.add_config_returning_id("new conf") c2 = col.decks.add_config_returning_id("new conf")
col.decks.set_config_id_for_deck_dict(col.decks.get(deck2), c2) col.decks.set_config_id_for_deck_dict(col.decks.get(deck2), c2)
col.reset()
# both confs have defaulted to a limit of 20 # both confs have defaulted to a limit of 20
assert col.sched.newCount == 20 assert col.sched.newCount == 20
# first card we get comes from parent # first card we get comes from parent
@ -102,13 +98,11 @@ def test_newLimits():
conf1 = col.decks.config_dict_for_deck_id(1) conf1 = col.decks.config_dict_for_deck_id(1)
conf1["new"]["perDay"] = 10 conf1["new"]["perDay"] = 10
col.decks.save(conf1) col.decks.save(conf1)
col.reset()
assert col.sched.newCount == 10 assert col.sched.newCount == 10
# if we limit child to 4, we should get 9 # if we limit child to 4, we should get 9
conf2 = col.decks.config_dict_for_deck_id(deck2) conf2 = col.decks.config_dict_for_deck_id(deck2)
conf2["new"]["perDay"] = 4 conf2["new"]["perDay"] = 4
col.decks.save(conf2) col.decks.save(conf2)
col.reset()
assert col.sched.newCount == 9 assert col.sched.newCount == 9
@ -117,7 +111,6 @@ def test_newBoxes():
note = col.newNote() note = col.newNote()
note["Front"] = "one" note["Front"] = "one"
col.addNote(note) col.addNote(note)
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
conf = col.sched._cardConf(c) conf = col.sched._cardConf(c)
conf["new"]["delays"] = [1, 2, 3, 4, 5] conf["new"]["delays"] = [1, 2, 3, 4, 5]
@ -138,7 +131,6 @@ def test_learn():
col.addNote(note) col.addNote(note)
# set as a new card and rebuild queues # set as a new card and rebuild queues
col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") 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 # sched.getCard should return it, since it's due in the past
c = col.sched.getCard() c = col.sched.getCard()
assert c assert c
@ -182,7 +174,6 @@ def test_learn():
c.type = CARD_TYPE_NEW c.type = CARD_TYPE_NEW
c.queue = QUEUE_TYPE_LRN c.queue = QUEUE_TYPE_LRN
c.flush() c.flush()
col.sched.reset()
col.sched.answerCard(c, 4) col.sched.answerCard(c, 4)
assert c.type == CARD_TYPE_REV assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV assert c.queue == QUEUE_TYPE_REV
@ -203,7 +194,6 @@ def test_relearn():
c.flush() c.flush()
# fail the card # fail the card
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
@ -234,7 +224,6 @@ def test_relearn_no_steps():
col.decks.save(conf) col.decks.save(conf)
# fail the card # fail the card
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
@ -251,7 +240,6 @@ def test_learn_collapsed():
col.addNote(note) col.addNote(note)
# set as a new card and rebuild queues # set as a new card and rebuild queues
col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}")
col.reset()
# should get '1' first # should get '1' first
c = col.sched.getCard() c = col.sched.getCard()
assert c.question().endswith("1") assert c.question().endswith("1")
@ -276,7 +264,6 @@ def test_learn_day():
note = col.newNote() note = col.newNote()
note["Front"] = "two" note["Front"] = "two"
col.addNote(note) col.addNote(note)
col.sched.reset()
c = col.sched.getCard() c = col.sched.getCard()
conf = col.sched._cardConf(c) conf = col.sched._cardConf(c)
conf["new"]["delays"] = [1, 10, 1440, 2880] conf["new"]["delays"] = [1, 10, 1440, 2880]
@ -300,7 +287,6 @@ def test_learn_day():
# for testing, move it back a day # for testing, move it back a day
c.due -= 1 c.due -= 1
c.flush() c.flush()
col.reset()
assert col.sched.counts() == (0, 1, 0) assert col.sched.counts() == (0, 1, 0)
c = col.sched.getCard() c = col.sched.getCard()
# nextIvl should work # nextIvl should work
@ -309,13 +295,11 @@ def test_learn_day():
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
col.undo() col.undo()
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 3) col.sched.answerCard(c, 3)
# simulate the passing of another two days # simulate the passing of another two days
c.due -= 2 c.due -= 2
c.flush() c.flush()
col.reset()
# the last pass should graduate it into a review card # the last pass should graduate it into a review card
assert ni(c, 3) == 86400 assert ni(c, 3) == 86400
col.sched.answerCard(c, 3) col.sched.answerCard(c, 3)
@ -324,7 +308,6 @@ def test_learn_day():
# correctly # correctly
c.due = 0 c.due = 0
c.flush() c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1) assert col.sched.counts() == (0, 0, 1)
conf = col.sched._cardConf(c) conf = col.sched._cardConf(c)
conf["lapse"]["delays"] = [1440] conf["lapse"]["delays"] = [1440]
@ -359,7 +342,6 @@ def test_reviews():
################################################## ##################################################
c = copy.copy(cardcopy) c = copy.copy(cardcopy)
c.flush() c.flush()
col.reset()
col.sched.answerCard(c, 2) col.sched.answerCard(c, 2)
assert c.queue == QUEUE_TYPE_REV assert c.queue == QUEUE_TYPE_REV
# the new interval should be (100) * 1.2 = 120 # the new interval should be (100) * 1.2 = 120
@ -477,7 +459,6 @@ def test_button_spacing():
c.ivl = 1 c.ivl = 1
c.start_timer() c.start_timer()
c.flush() c.flush()
col.reset()
ni = col.sched.nextIvlStr ni = col.sched.nextIvlStr
wo = without_unicode_isolation wo = without_unicode_isolation
assert wo(ni(c, 2)) == "2d" assert wo(ni(c, 2)) == "2d"
@ -491,47 +472,12 @@ def test_button_spacing():
assert wo(ni(c, 2)) == "1d" 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(): def test_nextIvl():
col = getEmptyCol() col = getEmptyCol()
note = col.newNote() note = col.newNote()
note["Front"] = "one" note["Front"] = "one"
note["Back"] = "two" note["Back"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
conf = col.decks.config_dict_for_deck_id(1) conf = col.decks.config_dict_for_deck_id(1)
conf["new"]["delays"] = [0.5, 3, 10] conf["new"]["delays"] = [0.5, 3, 10]
conf["lapse"]["delays"] = [1, 5, 9] conf["lapse"]["delays"] = [1, 5, 9]
@ -611,7 +557,6 @@ def test_bury():
c2.load() c2.load()
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
col.sched.unbury_deck(deck_id=col.decks.get_current_id(), mode=UnburyDeck.USER_ONLY) 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.bury_cards([c.id, c2.id])
col.sched.unbury_deck(deck_id=col.decks.get_current_id()) col.sched.unbury_deck(deck_id=col.decks.get_current_id())
col.reset()
assert col.sched.counts() == (2, 0, 0) assert col.sched.counts() == (2, 0, 0)
@ -641,14 +584,11 @@ def test_suspend():
col.addNote(note) col.addNote(note)
c = note.cards()[0] c = note.cards()[0]
# suspending # suspending
col.reset()
assert col.sched.getCard() assert col.sched.getCard()
col.sched.suspend_cards([c.id]) col.sched.suspend_cards([c.id])
col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
# unsuspending # unsuspending
col.sched.unsuspend_cards([c.id]) col.sched.unsuspend_cards([c.id])
col.reset()
assert col.sched.getCard() assert col.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
c.due = 0 c.due = 0
@ -656,7 +596,6 @@ def test_suspend():
c.type = CARD_TYPE_REV c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV c.queue = QUEUE_TYPE_REV
c.flush() c.flush()
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
assert c.due >= time.time() assert c.due >= time.time()
@ -699,12 +638,10 @@ def test_filt_reviewing_early_normal():
c.factor = STARTING_FACTOR c.factor = STARTING_FACTOR
c.start_timer() c.start_timer()
c.flush() c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 0) assert col.sched.counts() == (0, 0, 0)
# create a dynamic deck and refresh it # create a dynamic deck and refresh it
did = col.decks.new_filtered("Cram") did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did) col.sched.rebuild_filtered_deck(did)
col.reset()
# should appear as normal in the deck list # should appear as normal in the deck list
assert sorted(col.sched.deck_due_tree().children)[0].review_count == 1 assert sorted(col.sched.deck_due_tree().children)[0].review_count == 1
# and should appear in the counts # and should appear in the counts
@ -731,7 +668,6 @@ def test_filt_reviewing_early_normal():
c.due = col.sched.today + 75 c.due = col.sched.today + 75
c.flush() c.flush()
col.sched.rebuild_filtered_deck(did) col.sched.rebuild_filtered_deck(did)
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
assert col.sched.nextIvl(c, 2) == 100 * 1.2 / 2 * 86400 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 # create a dynamic deck and refresh it
did = col.decks.new_filtered("Cram") did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did) col.sched.rebuild_filtered_deck(did)
col.reset()
# card should still be in learning state # card should still be in learning state
c.load() c.load()
@ -800,7 +735,6 @@ def test_preview():
cram["resched"] = False cram["resched"] = False
col.decks.save(cram) col.decks.save(cram)
col.sched.rebuild_filtered_deck(did) col.sched.rebuild_filtered_deck(did)
col.reset()
# grab the first card # grab the first card
c = col.sched.getCard() c = col.sched.getCard()
@ -860,7 +794,6 @@ def test_ordcycle():
conf = col.decks.get_config(1) conf = col.decks.get_config(1)
conf["new"]["bury"] = False conf["new"]["bury"] = False
col.decks.save(conf) col.decks.save(conf)
col.reset()
# ordinals should arrive in order # ordinals should arrive in order
for i in range(3): for i in range(3):
@ -879,7 +812,6 @@ def test_counts_idx_new():
note["Front"] = "two" note["Front"] = "two"
note["Back"] = "two" note["Back"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
assert col.sched.counts() == (2, 0, 0) assert col.sched.counts() == (2, 0, 0)
c = col.sched.getCard() c = col.sched.getCard()
# getCard does not decrement counts # getCard does not decrement counts
@ -903,7 +835,6 @@ def test_repCounts():
note = col.newNote() note = col.newNote()
note["Front"] = "two" note["Front"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
assert col.sched.counts() == (2, 0, 0) assert col.sched.counts() == (2, 0, 0)
col.sched.answerCard(col.sched.getCard(), 1) col.sched.answerCard(col.sched.getCard(), 1)
@ -924,7 +855,6 @@ def test_repCounts():
note = col.newNote() note = col.newNote()
note["Front"] = "four" note["Front"] = "four"
col.addNote(note) col.addNote(note)
col.reset()
# initial pass and immediate graduate should be correct too # initial pass and immediate graduate should be correct too
assert col.sched.counts() == (2, 0, 0) assert col.sched.counts() == (2, 0, 0)
col.sched.answerCard(col.sched.getCard(), 3) col.sched.answerCard(col.sched.getCard(), 3)
@ -945,7 +875,6 @@ def test_repCounts():
note = col.newNote() note = col.newNote()
note["Front"] = "six" note["Front"] = "six"
col.addNote(note) col.addNote(note)
col.reset()
assert col.sched.counts() == (1, 0, 1) assert col.sched.counts() == (1, 0, 1)
col.sched.answerCard(col.sched.getCard(), 1) col.sched.answerCard(col.sched.getCard(), 1)
assert col.sched.counts() == (1, 1, 0) assert col.sched.counts() == (1, 1, 0)
@ -964,7 +893,6 @@ def test_timing():
c.due = 0 c.due = 0
c.flush() c.flush()
# fail the first one # fail the first one
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
# the next card should be another review # the next card should be another review
@ -973,7 +901,6 @@ def test_timing():
# if the failed card becomes due, it should show first # if the failed card becomes due, it should show first
c.due = int_time() - 1 c.due = int_time() - 1
c.flush() c.flush()
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
@ -988,7 +915,6 @@ def test_collapse():
note = col.newNote() note = col.newNote()
note["Front"] = "two" note["Front"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
# first note # first note
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
@ -1032,7 +958,6 @@ def test_deckDue():
note["Front"] = "three" note["Front"] = "three"
note.note_type()["did"] = col.decks.id("foo::baz") note.note_type()["did"] = col.decks.id("foo::baz")
col.addNote(note) col.addNote(note)
col.reset()
assert len(col.decks.all_names_and_ids()) == 5 assert len(col.decks.all_names_and_ids()) == 5
tree = col.sched.deck_due_tree().children tree = col.sched.deck_due_tree().children
assert tree[0].name == "Default" assert tree[0].name == "Default"
@ -1078,7 +1003,6 @@ def test_deckFlow():
note["Front"] = "three" note["Front"] = "three"
default1 = note.note_type()["did"] = col.decks.id("Default::1") default1 = note.note_type()["did"] = col.decks.id("Default::1")
col.addNote(note) col.addNote(note)
col.reset()
assert col.sched.counts() == (3, 0, 0) assert col.sched.counts() == (3, 0, 0)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
for i in "one", "three", "two": for i in "one", "three", "two":
@ -1142,10 +1066,8 @@ def test_forget():
c.ivl = 100 c.ivl = 100
c.due = 0 c.due = 0
c.flush() c.flush()
col.reset()
assert col.sched.counts() == (0, 0, 1) assert col.sched.counts() == (0, 0, 1)
col.sched.forgetCards([c.id]) col.sched.forgetCards([c.id])
col.reset()
assert col.sched.counts() == (1, 0, 0) assert col.sched.counts() == (1, 0, 0)
@ -1183,7 +1105,6 @@ def test_norelearn():
c.ivl = 100 c.ivl = 100
c.start_timer() c.start_timer()
c.flush() c.flush()
col.reset()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
col.sched._cardConf(c)["lapse"]["delays"] = [] col.sched._cardConf(c)["lapse"]["delays"] = []
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
@ -1235,7 +1156,6 @@ def test_negativeDueFilter():
did = col.decks.new_filtered("Cram") did = col.decks.new_filtered("Cram")
col.sched.rebuild_filtered_deck(did) col.sched.rebuild_filtered_deck(did)
col.sched.empty_filtered_deck(did) col.sched.empty_filtered_deck(did)
col.reset()
c.load() c.load()
assert c.due == -5 assert c.due == -5
@ -1250,7 +1170,6 @@ def test_initial_repeat():
note["Back"] = "two" note["Back"] = "two"
col.addNote(note) col.addNote(note)
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 2) col.sched.answerCard(c, 2)
# should be due in ~ 5.5 mins # should be due in ~ 5.5 mins

View file

@ -17,7 +17,6 @@ def test_stats():
# card stats # card stats
card_stats = col.card_stats_data(c.id) card_stats = col.card_stats_data(c.id)
assert card_stats.note_id == note.id assert card_stats.note_id == note.id
col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 3) col.sched.answerCard(c, 3)
col.sched.answerCard(c, 2) col.sched.answerCard(c, 2)

View file

@ -38,7 +38,6 @@ class DeckConf(QDialog):
self.form = aqt.forms.dconf.Ui_Dialog() self.form = aqt.forms.dconf.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
gui_hooks.deck_conf_did_setup_ui_form(self) gui_hooks.deck_conf_did_setup_ui_form(self)
self.mw.checkpoint(tr.actions_options())
self.setupCombos() self.setupCombos()
self.setupConfs() self.setupConfs()
qconnect( qconnect(

View file

@ -193,7 +193,6 @@ class ImportDialog(QDialog):
) )
self.mw.col.models.save(self.importer.model, updateReqs=False) self.mw.col.models.save(self.importer.model, updateReqs=False)
self.mw.progress.start() self.mw.progress.start()
self.mw.checkpoint(tr.actions_import())
def on_done(future: Future) -> None: def on_done(future: Future) -> None:
self.mw.progress.finish() self.mw.progress.finish()

View file

@ -28,6 +28,7 @@ import aqt.toolbar
import aqt.webview import aqt.webview
from anki import hooks from anki import hooks
from anki._backend import RustBackend as _RustBackend from anki._backend import RustBackend as _RustBackend
from anki._legacy import deprecated
from anki.collection import Collection, Config, OpChanges, UndoStatus from anki.collection import Collection, Config, OpChanges, UndoStatus
from anki.decks import DeckDict, DeckId from anki.decks import DeckDict, DeckId
from anki.hooks import runHook from anki.hooks import runHook
@ -918,9 +919,7 @@ title="{}" {}>{}</button>""".format(
signal.signal(signal.SIGTERM, self.onUnixSignal) signal.signal(signal.SIGTERM, self.onUnixSignal)
def onUnixSignal(self, signum: Any, frame: Any) -> None: def onUnixSignal(self, signum: Any, frame: Any) -> None:
# schedule a rollback & quit
def quit() -> None: def quit() -> None:
self.col.db.rollback()
self.close() self.close()
self.progress.single_shot(100, quit) self.progress.single_shot(100, quit)
@ -1021,7 +1020,6 @@ title="{}" {}>{}</button>""".format(
"Caller should ensure auth available." "Caller should ensure auth available."
def on_collection_sync_finished() -> None: def on_collection_sync_finished() -> None:
self.col.clear_python_undo()
self.col.models._clear_cache() self.col.models._clear_cache()
gui_hooks.sync_did_finish() gui_hooks.sync_did_finish()
self.reset() self.reset()
@ -1189,15 +1187,14 @@ title="{}" {}>{}</button>""".format(
self.form.actionRedo.setVisible(info.show_redo) self.form.actionRedo.setVisible(info.show_redo)
gui_hooks.undo_state_did_change(info) gui_hooks.undo_state_did_change(info)
@deprecated(info="checkpoints are no longer supported")
def checkpoint(self, name: str) -> None: def checkpoint(self, name: str) -> None:
self.col.save(name) pass
self.update_undo_actions()
@deprecated(info="saving is automatic")
def autosave(self) -> None: def autosave(self) -> None:
self.col.autosave() pass
self.update_undo_actions()
maybeEnableUndo = update_undo_actions
onUndo = undo onUndo = undo
# Other menu operations # Other menu operations
@ -1213,7 +1210,6 @@ title="{}" {}>{}</button>""".format(
aqt.dialogs.open("EditCurrent", self) aqt.dialogs.open("EditCurrent", self)
def onOverview(self) -> None: def onOverview(self) -> None:
self.col.reset()
self.moveToState("overview") self.moveToState("overview")
def onStats(self) -> None: def onStats(self) -> None:
@ -1461,10 +1457,6 @@ title="{}" {}>{}</button>""".format(
) )
def _create_backup_with_progress(self, user_initiated: bool) -> None: 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 # The initial copy will display a progress window if it takes too long
def backup(col: Collection) -> bool: def backup(col: Collection) -> bool:
return col.create_backup( return col.create_backup(
@ -1483,7 +1475,6 @@ title="{}" {}>{}</button>""".format(
) )
def after_backup_started(created: bool) -> None: def after_backup_started(created: bool) -> None:
# Legacy checkpoint may have expired.
self.update_undo_actions() self.update_undo_actions()
if user_initiated and not created: if user_initiated and not created:

View file

@ -148,7 +148,6 @@ def on_op_finished(
mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None
) -> None: ) -> None:
mw.update_undo_actions() mw.update_undo_actions()
mw.autosave()
if isinstance(result, OpChanges): if isinstance(result, OpChanges):
changes = result changes = result

View file

@ -3,13 +3,12 @@
from __future__ import annotations 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.errors import UndoEmpty
from anki.types import assert_exhaustive
from aqt import gui_hooks from aqt import gui_hooks
from aqt.operations import CollectionOp from aqt.operations import CollectionOp
from aqt.qt import QWidget 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: def undo(*, parent: QWidget) -> None:
@ -20,10 +19,7 @@ def undo(*, parent: QWidget) -> None:
tooltip(tr.undo_action_undone(action=out.operation), parent=parent) tooltip(tr.undo_action_undone(action=out.operation), parent=parent)
def on_failure(exc: Exception) -> None: def on_failure(exc: Exception) -> None:
if isinstance(exc, UndoEmpty): if not isinstance(exc, UndoEmpty):
# backend has no undo, but there may be a checkpoint waiting
_legacy_undo(parent=parent)
else:
showWarning(str(exc), parent=parent) showWarning(str(exc), parent=parent)
CollectionOp(parent, lambda col: col.undo()).success(on_success).failure( 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() 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( def set_preferences(
*, parent: QWidget, preferences: Preferences *, parent: QWidget, preferences: Preferences
) -> CollectionOp[OpChanges]: ) -> CollectionOp[OpChanges]:

View file

@ -64,7 +64,6 @@ class Overview:
def refresh(self) -> None: def refresh(self) -> None:
def success(_counts: tuple) -> None: def success(_counts: tuple) -> None:
self._refresh_needed = False self._refresh_needed = False
self.mw.col.reset()
self._renderPage() self._renderPage()
self._renderBottom() self._renderBottom()
self.mw.web.setFocus() self.mw.web.setFocus()

View file

@ -325,7 +325,7 @@ class ProfileManager:
continue continue
try: try:
c = Collection(path) c = Collection(path)
c.close(save=False, downgrade=True) c.close(downgrade=True)
except Exception as e: except Exception as e:
print(e) print(e)
problem_profiles.append(name) problem_profiles.append(name)

View file

@ -178,7 +178,6 @@ class Reviewer:
def refresh_if_needed(self) -> None: def refresh_if_needed(self) -> None:
if self._refresh_needed is RefreshNeeded.QUEUES: if self._refresh_needed is RefreshNeeded.QUEUES:
self.mw.col.reset()
self.nextCard() self.nextCard()
self.mw.fade_in_webview() self.mw.fade_in_webview()
self._refresh_needed = None self._refresh_needed = None
@ -444,7 +443,6 @@ class Reviewer:
def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None: def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None:
gui_hooks.reviewer_did_answer_card(self, self.card, ease) gui_hooks.reviewer_did_answer_card(self, self.card, ease)
self._answeredIds.append(self.card.id) self._answeredIds.append(self.card.id)
self.mw.autosave()
if not self.check_timebox(): if not self.check_timebox():
self.nextCard() self.nextCard()

View file

@ -98,7 +98,6 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
timer.start(150) timer.start(150)
def on_future_done(fut: Future[SyncOutput]) -> None: def on_future_done(fut: Future[SyncOutput]) -> None:
mw.col.db.begin()
# scheduler version may have changed # scheduler version may have changed
mw.col._load_scheduler() mw.col._load_scheduler()
timer.stop() timer.stop()
@ -121,7 +120,6 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
else: else:
full_sync(mw, out, on_done) full_sync(mw, out, on_done)
mw.col.save(trx=False)
mw.taskman.with_progress( mw.taskman.with_progress(
lambda: mw.col.sync_collection(auth, mw.pm.media_syncing_enabled()), lambda: mw.col.sync_collection(auth, mw.pm.media_syncing_enabled()),
on_future_done, on_future_done,

View file

@ -602,12 +602,6 @@ hooks = [
), ),
# UI state/refreshing # UI state/refreshing
################### ###################
Hook(
name="state_did_revert",
args=["action: str"],
legacy_hook="revertedState",
doc="Legacy hook, called after undoing.",
),
Hook( Hook(
name="state_did_undo", name="state_did_undo",
args=["changes: OpChangesAfterUndo"], args=["changes: OpChangesAfterUndo"],