From 558c75493fb605ec3046887749047b4b0beb2cce Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 24 Sep 2023 14:19:25 +1000 Subject: [PATCH] Strip out v1/v2 code --- pylib/anki/collection.py | 14 +- pylib/anki/consts.py | 11 - pylib/anki/scheduler/__init__.py | 11 - pylib/anki/scheduler/base.py | 15 +- pylib/anki/scheduler/dummy.py | 33 + pylib/anki/scheduler/legacy.py | 5 + pylib/anki/scheduler/v1.py | 85 -- pylib/anki/scheduler/v2.py | 1166 ----------------- pylib/anki/scheduler/v3.py | 16 - pylib/tests/test_decks.py | 4 - pylib/tests/test_sched2021.py | 4 - .../{test_schedv2.py => test_schedv3.py} | 79 +- pylib/tests/test_undo.py | 96 -- qt/aqt/forms/preferences.ui | 89 +- qt/aqt/preferences.py | 24 +- qt/aqt/reviewer.py | 28 +- 16 files changed, 85 insertions(+), 1595 deletions(-) create mode 100644 pylib/anki/scheduler/dummy.py delete mode 100644 pylib/anki/scheduler/v1.py delete mode 100644 pylib/anki/scheduler/v2.py delete mode 100644 pylib/tests/test_sched2021.py rename pylib/tests/{test_schedv2.py => test_schedv3.py} (94%) delete mode 100644 pylib/tests/test_undo.py diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 4cd355fdd..25b69fb16 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -75,8 +75,7 @@ from anki.lang import FormatTimeSpan from anki.media import MediaManager, media_paths_from_col_path from anki.models import ModelManager, NotetypeDict, NotetypeId from anki.notes import Note, NoteId -from anki.scheduler.v1 import Scheduler as V1Scheduler -from anki.scheduler.v2 import Scheduler as V2Scheduler +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 @@ -135,7 +134,7 @@ class AddNoteRequest: class Collection(DeprecatedNamesMixin): - sched: V1Scheduler | V2Scheduler | V3Scheduler + sched: V3Scheduler | DummyScheduler @staticmethod def initialize_backend_logging(path: str | None = None) -> None: @@ -213,12 +212,17 @@ class Collection(DeprecatedNamesMixin): def _load_scheduler(self) -> None: ver = self.sched_ver() if ver == 1: - self.sched = V1Scheduler(self) + self.sched = DummyScheduler(self) elif ver == 2: if self.v3_scheduler(): self.sched = V3Scheduler(self) + # enable new timezone if not already enabled + if self.conf.get("creationOffset") is None: + prefs = self._backend.get_preferences() + prefs.scheduling.new_timezone = True + self._backend.set_preferences(prefs) else: - self.sched = V2Scheduler(self) + self.sched = DummyScheduler(self) def upgrade_to_v2_scheduler(self) -> None: self._backend.upgrade_scheduler() diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index 5b6e552fc..ac07ec955 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -118,17 +118,6 @@ def new_card_order_labels(col: anki.collection.Collection | None) -> dict[int, A } -def new_card_scheduling_labels( - col: anki.collection.Collection | None, -) -> dict[int, Any]: - tr = _tr(col) - return { - 0: tr.scheduling_mix_new_cards_and_reviews(), - 1: tr.scheduling_show_new_cards_after_reviews(), - 2: tr.scheduling_show_new_cards_before_reviews(), - } - - _deprecated_names = DeprecatedNamesMixinForModule(globals()) diff --git a/pylib/anki/scheduler/__init__.py b/pylib/anki/scheduler/__init__.py index 5965d2e05..5b7562a4c 100644 --- a/pylib/anki/scheduler/__init__.py +++ b/pylib/anki/scheduler/__init__.py @@ -3,8 +3,6 @@ from __future__ import annotations -import sys - import anki.scheduler.base as _base UnburyDeck = _base.UnburyDeck @@ -12,12 +10,3 @@ CongratsInfo = _base.CongratsInfo BuryOrSuspend = _base.BuryOrSuspend FilteredDeckForUpdate = _base.FilteredDeckForUpdate CustomStudyRequest = _base.CustomStudyRequest - -# add aliases to the legacy pathnames -import anki.scheduler.v1 -import anki.scheduler.v2 - -sys.modules["anki.sched"] = sys.modules["anki.scheduler.v1"] -sys.modules["anki.schedv2"] = sys.modules["anki.scheduler.v2"] -anki.sched = sys.modules["anki.scheduler.v1"] # type: ignore -anki.schedv2 = sys.modules["anki.scheduler.v2"] # type: ignore diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index d99425f31..d84194cee 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -7,6 +7,7 @@ import anki import anki.collection from anki import decks_pb2, scheduler_pb2 from anki._legacy import DeprecatedNamesMixin +from anki.cards import Card from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId from anki.config import Config @@ -25,7 +26,14 @@ from typing import Sequence from anki import config_pb2 from anki.cards import CardId -from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW +from anki.consts import ( + CARD_TYPE_NEW, + NEW_CARDS_RANDOM, + QUEUE_TYPE_DAY_LEARN_RELEARN, + QUEUE_TYPE_LRN, + QUEUE_TYPE_NEW, + QUEUE_TYPE_PREVIEW, +) from anki.decks import DeckConfigDict, DeckId, DeckTreeNode from anki.notes import NoteId from anki.utils import ids2str, int_time @@ -49,6 +57,11 @@ class SchedulerBase(DeprecatedNamesMixin): def day_cutoff(self) -> int: return self._timing_today().next_day_at + def countIdx(self, card: Card) -> int: + if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): + return QUEUE_TYPE_LRN + return card.queue + # Deck list ########################################################################## diff --git a/pylib/anki/scheduler/dummy.py b/pylib/anki/scheduler/dummy.py new file mode 100644 index 000000000..90805728f --- /dev/null +++ b/pylib/anki/scheduler/dummy.py @@ -0,0 +1,33 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +# pylint: disable=invalid-name + +from __future__ import annotations + +from anki.cards import Card +from anki.decks import DeckId +from anki.scheduler.legacy import SchedulerBaseWithLegacy + + +class DummyScheduler(SchedulerBaseWithLegacy): + reps = 0 + + def reset(self) -> None: + pass + + def getCard(self) -> Card | None: + raise Exception("v1 scheduler no longer supported") + + def answerCard(self, card: Card, ease: int) -> None: + raise Exception("v1 scheduler no longer supported") + + def _is_finished(self) -> bool: + return False + + @property + def active_decks(self) -> list[DeckId]: + return [] + + def counts(self) -> list[int]: + return [0, 0, 0] diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py index 8bfee1bd7..a5e90a677 100644 --- a/pylib/anki/scheduler/legacy.py +++ b/pylib/anki/scheduler/legacy.py @@ -3,6 +3,8 @@ # pylint: disable=invalid-name +from __future__ import annotations + from typing import Optional from anki._legacy import deprecated @@ -127,6 +129,9 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self.today, ) + def answerButtons(self, card: Card) -> int: + return 4 + # legacy in v3 but used by unit tests; redefined in v2/v1 def _cardConf(self, card: Card) -> DeckConfigDict: diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py deleted file mode 100644 index a4694da10..000000000 --- a/pylib/anki/scheduler/v1.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# pylint: disable=invalid-name - -from __future__ import annotations - -import anki -import anki.collection -from anki.cards import Card -from anki.consts import * -from anki.decks import DeckId - -from .v2 import QueueConfig -from .v2 import Scheduler as V2 - - -class Scheduler(V2): - version = 1 - name = "std" - haveCustomStudy = True - _spreadRev = True - _burySiblingsOnAnswer = True - - def __init__( # pylint: disable=super-init-not-called - self, col: anki.collection.Collection - ) -> None: - super().__init__(col) - self.queueLimit = 0 - self.reportLimit = 0 - self.dynReportLimit = 0 - self.reps = 0 - self.lrnCount = 0 - self.revCount = 0 - self.newCount = 0 - self._haveQueues = False - - def reset(self) -> None: - pass - - def getCard(self) -> Card | None: - raise Exception("v1 scheduler no longer supported") - - def answerCard(self, card: Card, ease: int) -> None: - raise Exception("v1 scheduler no longer supported") - - def _is_finished(self) -> bool: - return False - - # stubs of v1-specific routines that add-ons may be overriding - - def _graduatingIvl( - self, card: Card, conf: QueueConfig, early: bool, adj: bool = True - ) -> int: - return 0 - - def removeLrn(self, ids: list[int] | None = None) -> None: - pass - - def _lrnForDeck(self, did: DeckId) -> int: - return 0 - - def _deckRevLimit(self, did: DeckId) -> int: - return 0 - - def _nextLapseIvl(self, card: Card, conf: QueueConfig) -> int: - return 0 - - def _rescheduleRev(self, card: Card, ease: int) -> None: # type: ignore[override] - pass - - def _nextRevIvl(self, card: Card, ease: int) -> int: # type: ignore[override] - return 0 - - def _constrainedIvl(self, ivl: float, conf: QueueConfig, prev: int) -> int: # type: ignore[override] - return 0 - - def _adjRevIvl(self, card: Card, idealIvl: int) -> int: - return 0 - - def _dynIvlBoost(self, card: Card) -> int: - return 0 - - def _resched(self, card: Card) -> bool: - return False diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py deleted file mode 100644 index 3167ae3f7..000000000 --- a/pylib/anki/scheduler/v2.py +++ /dev/null @@ -1,1166 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# pylint: disable=invalid-name - -from __future__ import annotations - -import random -import time -from heapq import * -from typing import Any, Callable, cast - -import anki # pylint: disable=unused-import -import anki.collection -from anki import hooks, scheduler_pb2 -from anki._legacy import deprecated -from anki.cards import Card, CardId -from anki.consts import * -from anki.decks import DeckConfigDict, DeckDict, DeckId -from anki.lang import FormatTimeSpan -from anki.scheduler.legacy import SchedulerBaseWithLegacy -from anki.utils import ids2str, int_time - -CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse -SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse - -# legacy type alias -QueueConfig = dict[str, Any] - -# card types: 0=new, 1=lrn, 2=rev, 3=relrn -# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, -# 4=preview, -1=suspended, -2=sibling buried, -3=manually buried - -# revlog types: 0=lrn, 1=rev, 2=relrn, 3=early review -# positive revlog intervals are in days (rev), negative in seconds (lrn) -# odue/odid store original due/did when cards moved to filtered deck - - -class Scheduler(SchedulerBaseWithLegacy): - version = 2 - name = "std2" - haveCustomStudy = True - _burySiblingsOnAnswer = True - revCount: int - - def __init__(self, col: anki.collection.Collection) -> None: - super().__init__(col) - self.queueLimit = 50 - self.reportLimit = 1000 - self.dynReportLimit = 99999 - self.reps = 0 - self._haveQueues = False - self._lrnCutoff = 0 - self._active_decks: list[DeckId] = [] - self._current_deck_id = DeckId(1) - - @property - def active_decks(self) -> list[DeckId]: - "Caller must make sure to make a copy." - return self._active_decks - - def _update_active_decks(self) -> None: - self._active_decks = self.col.decks.deck_and_child_ids(self._current_deck_id) - - # Daily cutoff - ########################################################################## - - def _updateCutoff(self) -> None: - pass - - def _checkDay(self) -> None: - # check if the day has rolled over - if time.time() > self.day_cutoff: - self.reset() - - # Fetching the next card - ########################################################################## - - def reset(self) -> None: - self._current_deck_id = self.col.decks.selected() - self._update_active_decks() - self._reset_counts() - self._resetLrn() - self._resetRev() - self._resetNew() - self._haveQueues = True - - def _reset_counts(self) -> None: - node = self.deck_due_tree(self._current_deck_id) - if not node: - # current deck points to a missing deck - self.newCount = 0 - self.revCount = 0 - self._immediate_learn_count = 0 - else: - self.newCount = node.new_count - self.revCount = node.review_count - self._immediate_learn_count = node.learn_count - - def getCard(self) -> Card | None: - """Pop the next card from the queue. None if finished.""" - self._checkDay() - if not self._haveQueues: - self.reset() - card = self._getCard() - if card: - if not self._burySiblingsOnAnswer: - self._burySiblings(card) - card.start_timer() - return card - return None - - def _getCard(self) -> Card | None: - """Return the next due card, or None.""" - # learning card due? - c = self._getLrnCard() - if c: - return c - - # new first, or time for one? - if self._timeForNewCard(): - c = self._getNewCard() - if c: - return c - - # day learning first and card due? - dayLearnFirst = self.col.conf.get("dayLearnFirst", False) - if dayLearnFirst: - c = self._getLrnDayCard() - if c: - return c - - # card due for review? - c = self._getRevCard() - if c: - return c - - # day learning card due? - if not dayLearnFirst: - c = self._getLrnDayCard() - if c: - return c - - # new cards left? - c = self._getNewCard() - if c: - return c - - # collapse or finish - return self._getLrnCard(collapse=True) - - # Fetching new cards - ########################################################################## - - def _resetNew(self) -> None: - self._newDids = self.col.decks.active()[:] - self._newQueue: list[CardId] = [] - self._updateNewCardRatio() - - def _fillNew(self, recursing: bool = False) -> bool: - if self._newQueue: - return True - if not self.newCount: - return False - while self._newDids: - did = self._newDids[0] - lim = min(self.queueLimit, self._deckNewLimit(did)) - if lim: - # fill the queue with the current did - self._newQueue = self.col.db.list( - f""" - select id from cards where did = ? and queue = {QUEUE_TYPE_NEW} order by due,ord limit ?""", - did, - lim, - ) - if self._newQueue: - self._newQueue.reverse() - return True - # nothing left in the deck; move to next - self._newDids.pop(0) - - # if we didn't get a card but the count is non-zero, - # we need to check again for any cards that were - # removed from the queue but not buried - if recursing: - return False - self._reset_counts() - self._resetNew() - return self._fillNew(recursing=True) - - def _getNewCard(self) -> Card | None: - if self._fillNew(): - self.newCount -= 1 - return self.col.get_card(self._newQueue.pop()) - return None - - def _updateNewCardRatio(self) -> None: - if self.col.conf["newSpread"] == NEW_CARDS_DISTRIBUTE: - if self.newCount: - self.newCardModulus = (self.newCount + self.revCount) // self.newCount - # if there are cards to review, ensure modulo >= 2 - if self.revCount: - self.newCardModulus = max(2, self.newCardModulus) - return - self.newCardModulus = 0 - - def _timeForNewCard(self) -> bool | None: - "True if it's time to display a new card when distributing." - if not self.newCount: - return False - if self.col.conf["newSpread"] == NEW_CARDS_LAST: - return False - elif self.col.conf["newSpread"] == NEW_CARDS_FIRST: - return True - elif self.newCardModulus: - return self.reps != 0 and self.reps % self.newCardModulus == 0 - else: - # shouldn't reach - return None - - def _deckNewLimit( - self, did: DeckId, fn: Callable[[DeckDict], int] | None = None - ) -> int: - if not fn: - fn = self._deckNewLimitSingle - sel = self.col.decks.get(did) - lim = -1 - # for the deck and each of its parents - for g in [sel] + self.col.decks.parents(did): - rem = fn(g) - if lim == -1: - lim = rem - else: - lim = min(rem, lim) - return lim - - def _newForDeck(self, did: DeckId, lim: int) -> int: - "New count for a single deck." - if not lim: - return 0 - lim = min(lim, self.reportLimit) - return self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""", - did, - lim, - ) - - def _deckNewLimitSingle(self, g: DeckConfigDict) -> int: - "Limit for deck without parent limits." - if g["dyn"]: - return self.dynReportLimit - c = self.col.decks.config_dict_for_deck_id(g["id"]) - limit = max(0, c["new"]["perDay"] - self.counts_for_deck_today(g["id"]).new) - return hooks.scheduler_new_limit_for_single_deck(limit, g) - - @deprecated(info="no longer used by Anki; will be removed in the future") - def totalNewForCurrentDeck(self) -> int: - return self.col.db.scalar( - f""" -select count() from cards where id in ( -select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)""" - % self._deck_limit(), - self.reportLimit, - ) - - # Fetching learning cards - ########################################################################## - - # scan for any newly due learning cards every minute - def _updateLrnCutoff(self, force: bool) -> bool: - nextCutoff = int_time() + self.col.conf["collapseTime"] - if nextCutoff - self._lrnCutoff > 60 or force: - self._lrnCutoff = nextCutoff - return True - return False - - def _maybeResetLrn(self, force: bool) -> None: - if self._updateLrnCutoff(force): - self._resetLrn() - - def _resetLrnCount(self) -> None: - # sub-day - self.lrnCount = ( - self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_LRN} -and due < ?""" - % (self._deck_limit()), - self._lrnCutoff, - ) - or 0 - ) - # day - self.lrnCount += self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} -and due <= ?""" - % (self._deck_limit()), - self.today, - ) - # previews - self.lrnCount += self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW} -""" - % (self._deck_limit()) - ) - - def _resetLrn(self) -> None: - self._updateLrnCutoff(force=True) - self._resetLrnCount() - self._lrnQueue: list[tuple[int, CardId]] = [] - self._lrnDayQueue: list[CardId] = [] - self._lrnDids = self.col.decks.active()[:] - - # sub-day learning - def _fillLrn(self) -> bool | list[Any]: - if not self.lrnCount: - return False - if self._lrnQueue: - return True - cutoff = int_time() + self.col.conf["collapseTime"] - self._lrnQueue = self.col.db.all( # type: ignore - f""" -select due, id from cards where -did in %s and queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_PREVIEW}) and due < ? -limit %d""" - % (self._deck_limit(), self.reportLimit), - cutoff, - ) - self._lrnQueue = [cast(tuple[int, CardId], tuple(e)) for e in self._lrnQueue] - # as it arrives sorted by did first, we need to sort it - self._lrnQueue.sort() - return self._lrnQueue - - def _getLrnCard(self, collapse: bool = False) -> Card | None: - self._maybeResetLrn(force=collapse and self.lrnCount == 0) - if self._fillLrn(): - cutoff = time.time() - if collapse: - cutoff += self.col.conf["collapseTime"] - if self._lrnQueue[0][0] < cutoff: - id = heappop(self._lrnQueue)[1] - card = self.col.get_card(id) - self.lrnCount -= 1 - return card - return None - - # daily learning - def _fillLrnDay(self) -> bool | None: - if not self.lrnCount: - return False - if self._lrnDayQueue: - return True - while self._lrnDids: - did = self._lrnDids[0] - # fill the queue with the current did - self._lrnDayQueue = self.col.db.list( - f""" -select id from cards where -did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", - did, - self.today, - self.queueLimit, - ) - if self._lrnDayQueue: - # order - r = random.Random() - r.seed(self.today) - r.shuffle(self._lrnDayQueue) - # is the current did empty? - if len(self._lrnDayQueue) < self.queueLimit: - self._lrnDids.pop(0) - return True - # nothing left in the deck; move to next - self._lrnDids.pop(0) - # shouldn't reach here - return False - - def _getLrnDayCard(self) -> Card | None: - if self._fillLrnDay(): - self.lrnCount -= 1 - return self.col.get_card(self._lrnDayQueue.pop()) - return None - - # Fetching reviews - ########################################################################## - - def _currentRevLimit(self) -> int: - d = self.col.decks.get(self.col.decks.selected(), default=False) - return self._deckRevLimitSingle(d) - - def _deckRevLimitSingle(self, d: dict[str, Any]) -> int: - # invalid deck selected? - if not d: - return 0 - - if d["dyn"]: - return self.dynReportLimit - - c = self.col.decks.config_dict_for_deck_id(d["id"]) - lim = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review) - - return hooks.scheduler_review_limit_for_single_deck(lim, d) - - def _resetRev(self) -> None: - self._revQueue: list[CardId] = [] - - def _fillRev(self, recursing: bool = False) -> bool: - "True if a review card can be fetched." - if self._revQueue: - return True - if not self.revCount: - return False - - lim = min(self.queueLimit, self._currentRevLimit()) - if lim: - self._revQueue = self.col.db.list( - f""" -select id from cards where -did in %s and queue = {QUEUE_TYPE_REV} and due <= ? -order by due, random() -limit ?""" - % self._deck_limit(), - self.today, - lim, - ) - - if self._revQueue: - # preserve order - self._revQueue.reverse() - return True - - if recursing: - return False - self._reset_counts() - self._resetRev() - return self._fillRev(recursing=True) - - def _getRevCard(self) -> Card | None: - if self._fillRev(): - self.revCount -= 1 - return self.col.get_card(self._revQueue.pop()) - return None - - # Answering a card - ########################################################################## - - def answerCard(self, card: Card, ease: int) -> None: - if (not 1 <= ease <= 4) or (not 0 <= card.queue <= 4): - raise Exception("invalid ease or queue") - self.col.save_card_review_undo_info(card) - if self._burySiblingsOnAnswer: - self._burySiblings(card) - - self._answerCard(card, ease) - - card.mod = int_time() - card.usn = self.col.usn() - card.flush() - - def _answerCard(self, card: Card, ease: int) -> None: - self.reps += 1 - - if self._previewingCard(card): - self._answerCardPreview(card, ease) - return - - card.reps += 1 - - new_delta = 0 - review_delta = 0 - - if card.queue == QUEUE_TYPE_NEW: - # came from the new queue, move to learning - card.queue = QUEUE_TYPE_LRN - card.type = CARD_TYPE_LRN - # init reps to graduation - card.left = self._startingLeft(card) - new_delta = +1 - - if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): - self._answerLrnCard(card, ease) - elif card.queue == QUEUE_TYPE_REV: - self._answerRevCard(card, ease) - review_delta = +1 - else: - raise Exception(f"Invalid queue '{card}'") - - self.update_stats( - card.did, - new_delta=new_delta, - review_delta=review_delta, - milliseconds_delta=card.time_taken(), - ) - - # once a card has been answered once, the original due date - # no longer applies - if card.odue: - card.odue = 0 - - def _cardConf(self, card: Card) -> DeckConfigDict: - return self.col.decks.config_dict_for_deck_id(card.did) - - def _deck_limit(self) -> str: - return ids2str(self.col.decks.active()) - - # Answering (re)learning cards - ########################################################################## - - def _newConf(self, card: Card) -> Any: - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf["new"] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.config_dict_for_deck_id(card.odid) - return dict( - # original deck - ints=oconf["new"]["ints"], - initialFactor=oconf["new"]["initialFactor"], - bury=oconf["new"].get("bury", True), - delays=oconf["new"]["delays"], - # overrides - order=NEW_CARDS_DUE, - perDay=self.reportLimit, - ) - - def _lapseConf(self, card: Card) -> Any: - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf["lapse"] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.config_dict_for_deck_id(card.odid) - return dict( - # original deck - minInt=oconf["lapse"]["minInt"], - leechFails=oconf["lapse"]["leechFails"], - leechAction=oconf["lapse"]["leechAction"], - mult=oconf["lapse"]["mult"], - delays=oconf["lapse"]["delays"], - # overrides - resched=conf["resched"], - ) - - def _answerLrnCard(self, card: Card, ease: int) -> None: - conf = self._lrnConf(card) - if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING): - type = REVLOG_RELRN - else: - type = REVLOG_LRN - # lrnCount was decremented once when card was fetched - lastLeft = card.left - - leaving = False - - # immediate graduate? - if ease == BUTTON_FOUR: - self._rescheduleAsRev(card, conf, True) - leaving = True - # next step? - elif ease == BUTTON_THREE: - # graduation time? - if (card.left % 1000) - 1 <= 0: - self._rescheduleAsRev(card, conf, False) - leaving = True - else: - self._moveToNextStep(card, conf) - elif ease == BUTTON_TWO: - self._repeatStep(card, conf) - else: - # back to first step - self._moveToFirstStep(card, conf) - - self._logLrn(card, ease, conf, leaving, type, lastLeft) - - def _updateRevIvlOnFail(self, card: Card, conf: QueueConfig) -> None: - card.lastIvl = card.ivl - card.ivl = self._lapseIvl(card, conf) - - def _moveToFirstStep(self, card: Card, conf: QueueConfig) -> Any: - card.left = self._startingLeft(card) - - # relearning card? - if card.type == CARD_TYPE_RELEARNING: - self._updateRevIvlOnFail(card, conf) - - return self._rescheduleLrnCard(card, conf) - - def _moveToNextStep(self, card: Card, conf: QueueConfig) -> None: - # decrement real left count and recalculate left today - left = (card.left % 1000) - 1 - card.left = self._leftToday(conf["delays"], left) * 1000 + left - - self._rescheduleLrnCard(card, conf) - - def _repeatStep(self, card: Card, conf: QueueConfig) -> None: - delay = self._delayForRepeatingGrade(conf, card.left) - self._rescheduleLrnCard(card, conf, delay=delay) - - def _rescheduleLrnCard( - self, card: Card, conf: QueueConfig, delay: int | None = None - ) -> Any: - # normal delay for the current step? - if delay is None: - delay = self._delayForGrade(conf, card.left) - - card.due = int(time.time() + delay) - # due today? - if card.due < self.day_cutoff: - # add some randomness, up to 5 minutes or 25% - maxExtra = min(300, int(delay * 0.25)) - fuzz = random.randrange(0, max(1, maxExtra)) - card.due = min(self.day_cutoff - 1, card.due + fuzz) - card.queue = QUEUE_TYPE_LRN - if card.due < (int_time() + self.col.conf["collapseTime"]): - self.lrnCount += 1 - # if the queue is not empty and there's nothing else to do, make - # sure we don't put it at the head of the queue and end up showing - # it twice in a row - if self._lrnQueue and not self.revCount and not self.newCount: - smallestDue = self._lrnQueue[0][0] - card.due = max(card.due, smallestDue + 1) - heappush(self._lrnQueue, (card.due, card.id)) - else: - # the card is due in one or more days, so we need to use the - # day learn queue - ahead = ((card.due - self.day_cutoff) // 86400) + 1 - card.due = self.today + ahead - card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN - return delay - - def _delayForGrade(self, conf: QueueConfig, left: int) -> int: - left = left % 1000 - try: - delay = conf["delays"][-left] - except IndexError: - if conf["delays"]: - delay = conf["delays"][0] - else: - # user deleted final step; use dummy value - delay = 1 - return int(delay * 60) - - def _delayForRepeatingGrade(self, conf: QueueConfig, left: int) -> Any: - delay1 = self._delayForGrade(conf, left) - # first step? - if len(conf["delays"]) == left % 1000: - # halfway between last and next to avoid same interval with Again - if len(conf["delays"]) > 1: - delay2 = self._delayForGrade(conf, left - 1) - else: - # no next step, use dummy - delay2 = delay1 * 2 - avg = (delay1 + max(delay1, delay2)) // 2 - return avg - return delay1 - - def _lrnConf(self, card: Card) -> Any: - if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING): - return self._lapseConf(card) - else: - return self._newConf(card) - - def _rescheduleAsRev(self, card: Card, conf: QueueConfig, early: bool) -> None: - lapse = card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING) - - if lapse: - self._rescheduleGraduatingLapse(card, early) - else: - self._rescheduleNew(card, conf, early) - - # if we were dynamic, graduating means moving back to the old deck - if card.odid: - self._removeFromFiltered(card) - - def _rescheduleGraduatingLapse(self, card: Card, early: bool = False) -> None: - if early: - card.ivl += 1 - card.due = self.today + card.ivl - card.queue = QUEUE_TYPE_REV - card.type = CARD_TYPE_REV - - def _startingLeft(self, card: Card) -> int: - if card.type == CARD_TYPE_RELEARNING: - conf = self._lapseConf(card) - else: - conf = self._lrnConf(card) - tot = len(conf["delays"]) - tod = self._leftToday(conf["delays"], tot) - return tot + tod * 1000 - - def _leftToday( - self, - delays: list[int], - left: int, - now: int | None = None, - ) -> int: - "The number of steps that can be completed by the day cutoff." - if not now: - now = int_time() - delays = delays[-left:] - ok = 0 - for idx, delay in enumerate(delays): - now += int(delay * 60) - if now > self.day_cutoff: - break - ok = idx - return ok + 1 - - def _graduatingIvl( - self, card: Card, conf: QueueConfig, early: bool, fuzz: bool = True - ) -> Any: - if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING): - bonus = early and 1 or 0 - return card.ivl + bonus - if not early: - # graduate - ideal = conf["ints"][0] - else: - # early remove - ideal = conf["ints"][1] - if fuzz: - ideal = self._fuzzedIvl(ideal) - return ideal - - def _rescheduleNew(self, card: Card, conf: QueueConfig, early: bool) -> None: - "Reschedule a new card that's graduated for the first time." - card.ivl = self._graduatingIvl(card, conf, early) - card.due = self.today + card.ivl - card.factor = conf["initialFactor"] - card.type = CARD_TYPE_REV - card.queue = QUEUE_TYPE_REV - - def _logLrn( - self, - card: Card, - ease: int, - conf: QueueConfig, - leaving: bool, - type: int, - lastLeft: int, - ) -> None: - lastIvl = -(self._delayForGrade(conf, lastLeft)) - if leaving: - ivl = card.ivl - else: - if ease == BUTTON_TWO: - ivl = -self._delayForRepeatingGrade(conf, card.left) - else: - ivl = -self._delayForGrade(conf, card.left) - - def log() -> None: - self.col.db.execute( - "insert into revlog values (?,?,?,?,?,?,?,?,?)", - int(time.time() * 1000), - card.id, - self.col.usn(), - ease, - saturated_i32(ivl), - saturated_i32(lastIvl), - card.factor, - card.time_taken(), - type, - ) - - try: - log() - except: - # duplicate pk; retry in 10ms - time.sleep(0.01) - log() - - # note: when adding revlog entries in the future, make sure undo - # code deletes the entries - def _answerCardPreview(self, card: Card, ease: int) -> None: - if not 1 <= ease <= 2: - raise Exception("invalid ease") - - if ease == BUTTON_ONE: - # repeat after delay - card.queue = QUEUE_TYPE_PREVIEW - card.due = int_time() + self._previewDelay(card) - self.lrnCount += 1 - else: - # BUTTON_TWO - # restore original card state and remove from filtered deck - self._restorePreviewCard(card) - self._removeFromFiltered(card) - - def _previewingCard(self, card: Card) -> Any: - conf = self._cardConf(card) - return conf["dyn"] and not conf["resched"] - - def _previewDelay(self, card: Card) -> Any: - return self._cardConf(card).get("previewDelay", 10) * 60 - - def _removeFromFiltered(self, card: Card) -> None: - if card.odid: - card.did = card.odid - card.odue = 0 - card.odid = DeckId(0) - - def _restorePreviewCard(self, card: Card) -> None: - if not card.odid: - raise Exception("card should have odid set") - - card.due = card.odue - - # learning and relearning cards may be seconds-based or day-based; - # other types map directly to queues - if card.type in (CARD_TYPE_LRN, CARD_TYPE_RELEARNING): - if card.odue > 1000000000: - card.queue = QUEUE_TYPE_LRN - else: - card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN - else: - card.queue = CardQueue(card.type) - - # Answering a review card - ########################################################################## - - def _revConf(self, card: Card) -> QueueConfig: - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf["rev"] - # dynamic deck - return self.col.decks.config_dict_for_deck_id(card.odid)["rev"] - - def _answerRevCard(self, card: Card, ease: int) -> None: - delay = 0 - early = bool(card.odid and (card.odue > self.today)) - type = early and REVLOG_CRAM or REVLOG_REV - - if ease == BUTTON_ONE: - delay = self._rescheduleLapse(card) - else: - self._rescheduleRev(card, ease, early) - - hooks.schedv2_did_answer_review_card(card, ease, early) - self._logRev(card, ease, delay, type) - - def _rescheduleLapse(self, card: Card) -> Any: - conf = self._lapseConf(card) - - card.lapses += 1 - card.factor = max(1300, card.factor - 200) - - suspended = self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED - - if conf["delays"] and not suspended: - card.type = CARD_TYPE_RELEARNING - delay = self._moveToFirstStep(card, conf) - else: - # no relearning steps - self._updateRevIvlOnFail(card, conf) - self._rescheduleAsRev(card, conf, early=False) - # need to reset the queue after rescheduling - if suspended: - card.queue = QUEUE_TYPE_SUSPENDED - delay = 0 - - return delay - - def _lapseIvl(self, card: Card, conf: QueueConfig) -> Any: - ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"])) - return ivl - - def _rescheduleRev(self, card: Card, ease: int, early: bool) -> None: - # update interval - card.lastIvl = card.ivl - if early: - self._updateEarlyRevIvl(card, ease) - else: - self._updateRevIvl(card, ease) - - # then the rest - card.factor = max(1300, card.factor + [-150, 0, 150][ease - 2]) - card.due = self.today + card.ivl - - # card leaves filtered deck - self._removeFromFiltered(card) - - def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None: - def log() -> None: - self.col.db.execute( - "insert into revlog values (?,?,?,?,?,?,?,?,?)", - int(time.time() * 1000), - card.id, - self.col.usn(), - ease, - saturated_i32(-delay or card.ivl), - saturated_i32(card.lastIvl), - card.factor, - card.time_taken(), - type, - ) - - try: - log() - except: - # duplicate pk; retry in 10ms - time.sleep(0.01) - log() - - def _nextRevIvl(self, card: Card, ease: int, fuzz: bool) -> int: - "Next review interval for CARD, given EASE." - delay = self._daysLate(card) - conf = self._revConf(card) - fct = card.factor / 1000 - hardFactor = conf.get("hardFactor", 1.2) - if hardFactor > 1: - hardMin = card.ivl - else: - hardMin = 0 - ivl2 = self._constrainedIvl(card.ivl * hardFactor, conf, hardMin, fuzz) - if ease == BUTTON_TWO: - return ivl2 - - ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2, fuzz) - if ease == BUTTON_THREE: - return ivl3 - - ivl4 = self._constrainedIvl( - (card.ivl + delay) * fct * conf["ease4"], conf, ivl3, fuzz - ) - return ivl4 - - def _fuzzedIvl(self, ivl: int) -> int: - min, max = self._fuzzIvlRange(ivl) - return random.randint(min, max) - - def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]: - if ivl < 2: - return (1, 1) - elif ivl == 2: - return (2, 3) - elif ivl < 7: - fuzz = int(ivl * 0.25) - elif ivl < 30: - fuzz = max(2, int(ivl * 0.15)) - else: - fuzz = max(4, int(ivl * 0.05)) - # fuzz at least a day - fuzz = max(fuzz, 1) - return (ivl - fuzz, ivl + fuzz) - - def _constrainedIvl( - self, ivl: float, conf: QueueConfig, prev: int, fuzz: bool - ) -> int: - ivl = int(ivl * conf.get("ivlFct", 1)) - if fuzz: - ivl = self._fuzzedIvl(ivl) - ivl = max(ivl, prev + 1, 1) - ivl = min(ivl, conf["maxIvl"]) - return int(ivl) - - def _daysLate(self, card: Card) -> int: - "Number of days later than scheduled." - due = card.odue if card.odid else card.due - return max(0, self.today - due) - - def _updateRevIvl(self, card: Card, ease: int) -> None: - card.ivl = self._nextRevIvl(card, ease, fuzz=True) - - def _updateEarlyRevIvl(self, card: Card, ease: int) -> None: - card.ivl = self._earlyReviewIvl(card, ease) - - # next interval for card when answered early+correctly - def _earlyReviewIvl(self, card: Card, ease: int) -> int: - if ( - not (card.odid and card.type == CARD_TYPE_REV) - or not card.factor - or not ease > 1 - ): - raise Exception("invalid input to earlyReviewIvl") - - elapsed = card.ivl - (card.odue - self.today) - - conf = self._revConf(card) - - easyBonus = 1 - # early 3/4 reviews shouldn't decrease previous interval - minNewIvl = 1 - - if ease == BUTTON_TWO: - factor = conf.get("hardFactor", 1.2) - # hard cards shouldn't have their interval decreased by more than 50% - # of the normal factor - minNewIvl = factor / 2 - elif ease == BUTTON_THREE: - factor = card.factor / 1000 - else: # ease == BUTTON_FOUR: - factor = card.factor / 1000 - ease4 = conf["ease4"] - # 1.3 -> 1.15 - easyBonus = ease4 - (ease4 - 1) / 2 - - ivl = max(elapsed * factor, 1) - - # cap interval decreases - ivl = max(card.ivl * minNewIvl, ivl) * easyBonus - - ivl = self._constrainedIvl(ivl, conf, prev=0, fuzz=False) - - return ivl - - # Daily limits - ########################################################################## - - def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday: - return self.col._backend.counts_for_deck_today(deck_id) - - # Next times - ########################################################################## - - def nextIvl(self, card: Card, ease: int) -> Any: - "Return the next interval for CARD, in seconds." - # preview mode? - if self._previewingCard(card): - if ease == BUTTON_ONE: - return self._previewDelay(card) - return 0 - - # (re)learning? - if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): - return self._nextLrnIvl(card, ease) - elif ease == BUTTON_ONE: - # lapse - conf = self._lapseConf(card) - if conf["delays"]: - return conf["delays"][0] * 60 - return self._lapseIvl(card, conf) * 86400 - else: - # review - early = card.odid and (card.odue > self.today) - if early: - return self._earlyReviewIvl(card, ease) * 86400 - else: - return self._nextRevIvl(card, ease, fuzz=False) * 86400 - - # this isn't easily extracted from the learn code - def _nextLrnIvl(self, card: Card, ease: int) -> Any: - if card.queue == QUEUE_TYPE_NEW: - card.left = self._startingLeft(card) - conf = self._lrnConf(card) - if ease == BUTTON_ONE: - # fail - return self._delayForGrade(conf, len(conf["delays"])) - elif ease == BUTTON_TWO: - return self._delayForRepeatingGrade(conf, card.left) - elif ease == BUTTON_FOUR: - return self._graduatingIvl(card, conf, True, fuzz=False) * 86400 - else: # ease == BUTTON_THREE - left = card.left % 1000 - 1 - if left <= 0: - # graduate - return self._graduatingIvl(card, conf, False, fuzz=False) * 86400 - else: - return self._delayForGrade(conf, left) - - # Leeches - ########################################################################## - - def _checkLeech(self, card: Card, conf: QueueConfig) -> bool: - "Leech handler. True if card was a leech." - lf = conf["leechFails"] - if not lf: - return False - # if over threshold or every half threshold reps after that - if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0: - # add a leech tag - f = card.note() - f.add_tag("leech") - f.flush() - # handle - a = conf["leechAction"] - if a == LEECH_SUSPEND: - card.queue = QUEUE_TYPE_SUSPENDED - # notify UI - hooks.card_did_leech(card) - return True - return False - - # Sibling spacing - ########################################################################## - - def _burySiblings(self, card: Card) -> None: - toBury: list[CardId] = [] - nconf = self._newConf(card) - buryNew = nconf.get("bury", True) - rconf = self._revConf(card) - buryRev = rconf.get("bury", True) - # loop through and remove from queues - for cid, queue in self.col.db.execute( - f""" -select id, queue from cards where nid=? and id!=? -and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", - card.nid, - card.id, - self.today, - ): - if queue == QUEUE_TYPE_REV: - queue_obj = self._revQueue - if buryRev: - toBury.append(cid) - else: - queue_obj = self._newQueue - if buryNew: - toBury.append(cid) - - # even if burying disabled, we still discard to give same-day spacing - try: - queue_obj.remove(cid) - except ValueError: - pass - # then bury - if toBury: - self.bury_cards(toBury, manual=False) - - # Review-related UI helpers - ########################################################################## - - def counts(self, card: Card | None = None) -> tuple[int, int, int]: - counts = [self.newCount, self.lrnCount, self.revCount] - if card: - idx = self.countIdx(card) - counts[idx] += 1 - new, lrn, rev = counts - return (new, lrn, rev) - - def countIdx(self, card: Card) -> int: - if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): - return QUEUE_TYPE_LRN - return card.queue - - def answerButtons(self, card: Card) -> int: - conf = self._cardConf(card) - if card.odid and not conf["resched"]: - return 2 - return 4 - - def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: - "Return the next interval for CARD as a string." - ivl_secs = self.nextIvl(card, ease) - if not ivl_secs: - return self.col.tr.scheduling_end() - s = self.col.format_timespan(ivl_secs, FormatTimeSpan.ANSWER_BUTTONS) - if ivl_secs < self.col.conf["collapseTime"]: - s = f"<{s}" - return s - - def _is_finished(self) -> bool: - "Don't use this, it is a stop-gap until this code is refactored." - return not any((self.newCount, self.revCount, self._immediate_learn_count)) - - -def saturated_i32(number: int) -> int: - """Avoid problems on the backend by ensuring reasonably sized values.""" - I32_MIN = -(2**31) - I32_MAX = 2**31 - 1 - return min(max(number, I32_MIN), I32_MAX) diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 222b99b21..e343de155 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -17,7 +17,6 @@ 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 * @@ -140,14 +139,6 @@ class Scheduler(SchedulerBaseWithLegacy): def reviewCount(self) -> int: return self.counts()[2] - def countIdx(self, card: Card) -> int: - if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW): - return QUEUE_TYPE_LRN - return card.queue - - def answerButtons(self, card: Card) -> int: - return 4 - def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: "Return the next interval for CARD as a string." states = self.col._backend.get_scheduling_states(card.id) @@ -246,10 +237,3 @@ class Scheduler(SchedulerBaseWithLegacy): return self.col.db.list("select id from active_decks") except DBError: return [] - - @deprecated(info="no longer used by Anki; will be removed in the future") - def totalNewForCurrentDeck(self) -> int: - return self.col.db.scalar( - f""" -select count() from cards where queue={QUEUE_TYPE_NEW} and did in (select id from active_decks)""" - ) diff --git a/pylib/tests/test_decks.py b/pylib/tests/test_decks.py index 1e9951444..d0fb90b82 100644 --- a/pylib/tests/test_decks.py +++ b/pylib/tests/test_decks.py @@ -23,21 +23,17 @@ def test_basic(): # we start with the default col selected assert col.decks.selected() == 1 col.reset() - assert col.decks.active() == [1] # we can select a different col col.decks.select(parentId) assert col.decks.selected() == parentId - assert col.decks.active() == [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 - assert col.decks.active() == [parentId, childId] # we can select the child individually too col.decks.select(childId) assert col.decks.selected() == childId - assert col.decks.active() == [childId] # parents with a different case should be handled correctly col.decks.id("ONE") m = col.models.current() diff --git a/pylib/tests/test_sched2021.py b/pylib/tests/test_sched2021.py deleted file mode 100644 index 5d698d2ce..000000000 --- a/pylib/tests/test_sched2021.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -from .test_schedv2 import * diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv3.py similarity index 94% rename from pylib/tests/test_schedv2.py rename to pylib/tests/test_schedv3.py index 931998713..ab540938d 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv3.py @@ -16,20 +16,8 @@ from anki.utils import int_time from tests.shared import getEmptyCol as getEmptyColOrig -# This file is used to exercise both the legacy Python 2.1 scheduler, -# and the experimental new one in Rust. Most tests run on both, but a few -# tests have been implemented separately where the behaviour differs. -def is_2021() -> bool: - return "2021" in os.getenv("PYTEST_CURRENT_TEST") - - def getEmptyCol(): col = getEmptyColOrig() - col.upgrade_to_v2_scheduler() - if is_2021(): - col.set_v3_scheduler(True) - else: - col.set_v3_scheduler(False) return col @@ -39,13 +27,6 @@ def test_clock(): raise Exception("Unit tests will fail around the day rollover.") -def checkRevIvl(col, c, targetIvl): - if is_2021(): - return - min, max = col.sched._fuzzIvlRange(targetIvl) - assert min <= c.ivl <= max - - def test_basics(): col = getEmptyCol() col.reset() @@ -205,7 +186,6 @@ def test_learn(): col.sched.answerCard(c, 4) assert c.type == CARD_TYPE_REV assert c.queue == QUEUE_TYPE_REV - checkRevIvl(col, c, 4) # revlog should have been updated each time assert col.db.scalar("select count() from revlog where type = 0") == 5 @@ -328,10 +308,7 @@ def test_learn_day(): # if we fail it, it should be back in the correct queue col.sched.answerCard(c, 1) assert c.queue == QUEUE_TYPE_LRN - if is_2021(): - col.undo() - else: - col.undo_legacy() + col.undo() col.reset() c = col.sched.getCard() col.sched.answerCard(c, 3) @@ -386,7 +363,6 @@ def test_reviews(): col.sched.answerCard(c, 2) assert c.queue == QUEUE_TYPE_REV # the new interval should be (100) * 1.2 = 120 - checkRevIvl(col, c, 120) assert c.due == col.sched.today + c.ivl # factor should have been decremented assert c.factor == 2350 @@ -399,7 +375,6 @@ def test_reviews(): c.flush() col.sched.answerCard(c, 3) # the new interval should be (100 + 8/2) * 2.5 = 260 - checkRevIvl(col, c, 260) assert c.due == col.sched.today + c.ivl # factor should have been left alone assert c.factor == STARTING_FACTOR @@ -409,7 +384,6 @@ def test_reviews(): c.flush() col.sched.answerCard(c, 4) # the new interval should be (100 + 8) * 2.5 * 1.3 = 351 - checkRevIvl(col, c, 351) assert c.due == col.sched.today + c.ivl # factor should have been increased assert c.factor == 2650 @@ -429,8 +403,6 @@ def test_reviews(): hooks.card_did_leech.append(onLeech) col.sched.answerCard(c, 1) - if not is_2021(): - assert hooked assert c.queue == QUEUE_TYPE_SUSPENDED c.load() assert c.queue == QUEUE_TYPE_SUSPENDED @@ -719,12 +691,6 @@ def test_suspend(): def test_filt_reviewing_early_normal(): - def to_int(val: float) -> int: - if is_2021(): - return round(val) - else: - return int(val) - col = getEmptyCol() note = col.newNote() note["Front"] = "one" @@ -753,13 +719,12 @@ def test_filt_reviewing_early_normal(): c = col.sched.getCard() assert col.sched.answerButtons(c) == 4 assert col.sched.nextIvl(c, 1) == 600 - assert col.sched.nextIvl(c, 2) == to_int(75 * 1.2) * 86400 - assert col.sched.nextIvl(c, 3) == to_int(75 * 2.5) * 86400 - assert col.sched.nextIvl(c, 4) == to_int(75 * 2.5 * 1.15) * 86400 + assert col.sched.nextIvl(c, 2) == round(75 * 1.2) * 86400 + assert col.sched.nextIvl(c, 3) == round(75 * 2.5) * 86400 + assert col.sched.nextIvl(c, 4) == round(75 * 2.5 * 1.15) * 86400 # answer 'good' col.sched.answerCard(c, 3) - checkRevIvl(col, c, int(75 * 2.5)) assert c.due == col.sched.today + c.ivl assert not c.odue # should not be in learning @@ -777,7 +742,7 @@ def test_filt_reviewing_early_normal(): assert col.sched.nextIvl(c, 2) == 100 * 1.2 / 2 * 86400 assert col.sched.nextIvl(c, 3) == 100 * 86400 - assert col.sched.nextIvl(c, 4) == to_int(100 * (1.3 - (1.3 - 1) / 2)) * 86400 + assert col.sched.nextIvl(c, 4) == round(100 * (1.3 - (1.3 - 1) / 2)) * 86400 def test_filt_keep_lrn_state(): @@ -845,11 +810,7 @@ def test_preview(): # grab the first card c = col.sched.getCard() - if is_2021(): - passing_grade = 4 - else: - passing_grade = 2 - + passing_grade = 4 assert col.sched.answerButtons(c) == passing_grade assert col.sched.nextIvl(c, 1) == 600 assert col.sched.nextIvl(c, passing_grade) == 0 @@ -914,35 +875,7 @@ def test_ordcycle(): col.sched.answerCard(c, 4) -def test_counts_idx(): - if is_2021(): - pytest.skip("old sched only") - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - col.reset() - assert col.sched.counts() == (1, 0, 0) - c = col.sched.getCard() - # counter's been decremented but idx indicates 1 - assert col.sched.counts() == (0, 0, 0) - assert col.sched.countIdx(c) == 0 - # answer to move to learn queue - col.sched.answerCard(c, 1) - assert col.sched.counts() == (0, 1, 0) - # fetching again will decrement the count - c = col.sched.getCard() - assert col.sched.counts() == (0, 0, 0) - assert col.sched.countIdx(c) == 1 - # answering should add it back again - col.sched.answerCard(c, 1) - assert col.sched.counts() == (0, 1, 0) - - def test_counts_idx_new(): - if not is_2021(): - pytest.skip("new sched only") col = getEmptyCol() note = col.newNote() note["Front"] = "one" diff --git a/pylib/tests/test_undo.py b/pylib/tests/test_undo.py deleted file mode 100644 index ecc6fb004..000000000 --- a/pylib/tests/test_undo.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -import time - -from anki.consts import * -from tests.shared import getEmptyCol as getEmptyColOrig - - -def getEmptyCol(): - col = getEmptyColOrig() - col.upgrade_to_v2_scheduler() - return col - - -def test_op(): - col = getEmptyCol() - col.set_v3_scheduler(False) - # should have no undo by default - assert not col.undo_status().undo - # let's adjust a study option - col.save("studyopts") - col.conf["abc"] = 5 - # it should be listed as undoable - assert col.undo_status().undo == "studyopts" - # with about 5 minutes until it's clobbered - assert time.time() - col._last_checkpoint_at < 1 - # undoing should restore the old value - col.undo_legacy() - assert not col.undo_status().undo - assert "abc" not in col.conf - # an (auto)save will clear the undo - col.save("foo") - assert col.undo_status().undo == "foo" - col.save() - assert not col.undo_status().undo - # and a review will, too - col.save("add") - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - col.reset() - assert "add" in col.undo_status().undo.lower() - c = col.sched.getCard() - col.sched.answerCard(c, 2) - assert col.undo_status().undo == "Review" - - -def test_review(): - col = getEmptyCol() - col.set_v3_scheduler(False) - col.conf["counts"] = COUNT_REMAINING - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - note = col.newNote() - note["Front"] = "two" - col.addNote(note) - col.reset() - # answer - assert col.sched.counts() == (2, 0, 0) - c = col.sched.getCard() - assert c.queue == QUEUE_TYPE_NEW - col.sched.answerCard(c, 3) - assert c.left % 1000 == 1 - assert col.sched.counts() == (1, 1, 0) - assert c.queue == QUEUE_TYPE_LRN - # undo - assert col.undo_status().undo - col.undo_legacy() - col.reset() - assert col.sched.counts() == (2, 0, 0) - c.load() - assert c.queue == QUEUE_TYPE_NEW - assert c.left % 1000 != 1 - assert not col.undo_status().undo - # we should be able to undo multiple answers too - c = col.sched.getCard() - col.sched.answerCard(c, 3) - c = col.sched.getCard() - col.sched.answerCard(c, 3) - assert col.sched.counts() == (0, 2, 0) - col.undo_legacy() - col.reset() - assert col.sched.counts() == (1, 1, 0) - col.undo_legacy() - col.reset() - assert col.sched.counts() == (2, 0, 0) - # performing a normal op will clear the review queue - c = col.sched.getCard() - col.sched.answerCard(c, 3) - assert col.undo_status().undo == "Review" - col.save("foo") - assert col.undo_status().undo == "foo" - col.undo_legacy() - assert not col.undo_status().undo diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 1bbbc50ec..884c07442 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -6,7 +6,7 @@ 0 0 - 604 + 606 638 @@ -252,64 +252,6 @@ preferences_scheduler - - - - - 0 - 0 - - - - - - - preferences_v3_scheduler - - - - - - - - 0 - 0 - - - - preferences_show_learning_cards_with_larger_steps - - - - - - - - 0 - 0 - - - - preferences_legacy_timezone_handling - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - @@ -392,9 +334,6 @@ - - - @@ -472,17 +411,17 @@ - - - - 0 - 0 - - - - preferences_stop_timer_on_answer - - + + + + 0 + 0 + + + + preferences_stop_timer_on_answer + + @@ -1152,13 +1091,9 @@ bottomBarComboBox reduce_motion minimalist_mode - sched2021 - dayLearnFirst - legacy_timezone dayOffset lrnCutoff timeLimit - newSpread showPlayButtons interrupt_audio showProgress diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index fa3508478..d5728d653 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -3,15 +3,13 @@ import functools import re -from typing import Any, cast import anki.lang import aqt import aqt.forms import aqt.operations from anki.collection import OpChanges -from anki.consts import new_card_scheduling_labels -from aqt import AnkiQt, gui_hooks +from aqt import AnkiQt from aqt.operations.collection import set_preferences from aqt.profiles import VideoDriver from aqt.qt import * @@ -106,19 +104,8 @@ class Preferences(QDialog): scheduling = self.prefs.scheduling - version = scheduling.scheduler_version - form.dayLearnFirst.setVisible(version == 2) - form.legacy_timezone.setVisible(version >= 2) - form.newSpread.setVisible(version < 3) - form.sched2021.setVisible(version >= 2) - form.lrnCutoff.setValue(int(scheduling.learn_ahead_secs / 60.0)) - form.newSpread.addItems(list(new_card_scheduling_labels(self.mw.col).values())) - form.newSpread.setCurrentIndex(scheduling.new_review_mix) - form.dayLearnFirst.setChecked(scheduling.day_learn_first) form.dayOffset.setValue(scheduling.rollover) - form.legacy_timezone.setChecked(not scheduling.new_timezone) - form.sched2021.setChecked(version == 3) reviewing = self.prefs.reviewing form.timeLimit.setValue(int(reviewing.time_limit_secs / 60.0)) @@ -149,11 +136,8 @@ class Preferences(QDialog): form = self.form scheduling = self.prefs.scheduling - scheduling.new_review_mix = cast(Any, form.newSpread.currentIndex()) scheduling.learn_ahead_secs = form.lrnCutoff.value() * 60 - scheduling.day_learn_first = form.dayLearnFirst.isChecked() scheduling.rollover = form.dayOffset.value() - scheduling.new_timezone = not form.legacy_timezone.isChecked() reviewing = self.prefs.reviewing reviewing.show_remaining_due_counts = form.showProgress.isChecked() @@ -179,12 +163,6 @@ class Preferences(QDialog): def after_prefs_update(changes: OpChanges) -> None: self.mw.apply_collection_options() - if scheduling.scheduler_version > 1: - want_v3 = form.sched2021.isChecked() - if self.mw.col.v3_scheduler() != want_v3: - self.mw.col.set_v3_scheduler(want_v3) - gui_hooks.operation_did_execute(OpChanges(study_queues=True), None) - on_done() set_preferences(parent=self, preferences=self.prefs).success( diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index f1b788277..a4a187b31 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -444,8 +444,6 @@ class Reviewer: return if self.state != "answer": return - if self.mw.col.sched.answerButtons(self.card) < ease: - return proceed, ease = gui_hooks.reviewer_will_answer_card( (True, ease), self, self.card ) @@ -753,18 +751,8 @@ timerStopped = false; return "" counts: list[Union[int, str]] - if v3 := self._v3: - idx, counts_ = v3.counts() - counts = cast(list[Union[int, str]], counts_) - else: - # v1/v2 scheduler - if self.hadCardQueue: - # if it's come from the undo queue, don't count it separately - counts = list(self.mw.col.sched.counts()) - else: - counts = list(self.mw.col.sched.counts(self.card)) - idx = self.mw.col.sched.countIdx(self.card) - + idx, counts_ = self._v3.counts() + counts = cast(list[Union[int, str]], counts_) counts[idx] = f"{counts[idx]}" return f""" @@ -774,10 +762,7 @@ timerStopped = false; """ def _defaultEase(self) -> Literal[2, 3]: - if self.mw.col.sched.answerButtons(self.card) == 4: - return 3 - else: - return 2 + return 3 def _answerButtonList(self) -> tuple[tuple[int, str], ...]: button_count = self.mw.col.sched.answerButtons(self.card) @@ -841,12 +826,9 @@ timerStopped = false; buf += "" return buf - def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str: + def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str: if self.mw.col.conf["estTimes"]: - if v3_labels: - txt = v3_labels[i - 1] - else: - txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or "" + txt = v3_labels[i - 1] return f"""{txt}""" else: return ""