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,
)
@deprecated(info="please use col.update_card()")
def flush(self) -> None:
hooks.card_will_flush(self)
if self.id != 0:

View file

@ -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()

View file

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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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(

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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()

View file

@ -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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".format(
)
def after_backup_started(created: bool) -> None:
# Legacy checkpoint may have expired.
self.update_undo_actions()
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
) -> None:
mw.update_undo_actions()
mw.autosave()
if isinstance(result, OpChanges):
changes = result

View file

@ -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]:

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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,

View file

@ -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"],