From 23083c3eb4545d0a819395b3e26d7c1332e2fd13 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 12:53:07 +0100 Subject: [PATCH 01/16] NF: add types to noteimp.py --- pylib/anki/importing/noteimp.py | 38 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index e3865f8c8..8fe5ae3b7 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -19,6 +19,11 @@ from anki.utils import ( timestampID, ) +type_tagsMapped = Tuple[int, int, str, str, int, str, str] +type_tagsModified = Tuple[int, int, str, str, int, str] +type_tagsElse = Tuple[int, int, str, int, str] +type_udpates = Union[type_tagsMapped, type_tagsModified, type_tagsElse] + # Stores a list of fields, tags and deck ###################################################################### @@ -135,7 +140,7 @@ class NoteImporter(Importer): self._fmap = self.col.models.fieldMap(self.model) self._nextID = timestampID(self.col.db, "notes") # loop through the notes - updates = [] + updates: List[type_udpates] = [] updateLog = [] new = [] self._ids: List[int] = [] @@ -203,9 +208,9 @@ class NoteImporter(Importer): found = False # newly add if not found: - data = self.newData(n) - if data: - new.append(data) + new_data = self.newData(n) + if new_data: + new.append(new_data) # note that we've seen this note once already firsts[fld0] = True self.addNew(new) @@ -235,7 +240,9 @@ class NoteImporter(Importer): self.log.extend(updateLog) self.total = len(self._ids) - def newData(self, n: ForeignNote) -> Optional[list]: + def newData( + self, n: ForeignNote + ) -> Tuple[int, str, int, int, int, str, str, str, int, int, str]: id = self._nextID self._nextID += 1 self._ids.append(id) @@ -243,7 +250,7 @@ class NoteImporter(Importer): # note id for card updates later for ord, c in list(n.cards.items()): self._cards.append((id, ord, c)) - return [ + return ( id, guid64(), self.model["id"], @@ -255,28 +262,33 @@ class NoteImporter(Importer): 0, 0, "", - ] + ) - def addNew(self, rows: List[List[Union[int, str]]]) -> None: + def addNew( + self, + rows: List[Tuple[int, str, int, int, int, str, str, str, int, int, str]], + ) -> None: self.col.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", rows ) - def updateData(self, n: ForeignNote, id: int, sflds: List[str]) -> Optional[list]: + def updateData( + self, n: ForeignNote, id: int, sflds: List[str] + ) -> Optional[type_udpates]: self._ids.append(id) self.processFields(n, sflds) if self._tagsMapped: tags = self.col.tags.join(n.tags) - return [intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags] + return (intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags) elif self.tagModified: tags = self.col.db.scalar("select tags from notes where id = ?", id) tagList = self.col.tags.split(tags) + self.tagModified.split() tags = self.col.tags.join(tagList) - return [intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr] + return (intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr) else: - return [intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr] + return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr) - def addUpdates(self, rows: List[List[Union[int, str]]]) -> None: + def addUpdates(self, rows: List[type_udpates]) -> None: changes = self.col.db.scalar("select total_changes()") if self._tagsMapped: self.col.db.executemany( From 6ac540927a89f721d5d4193281e358aece7554d0 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 25 Mar 2021 13:50:31 +0100 Subject: [PATCH 02/16] NF: NoteID type --- pylib/anki/cards.py | 3 ++- pylib/anki/collection.py | 33 ++++++++++++++++++--------------- pylib/anki/find.py | 5 +++-- pylib/anki/importing/anki2.py | 3 ++- pylib/anki/importing/noteimp.py | 21 +++++++++++---------- pylib/anki/models.py | 11 +++++++---- pylib/anki/notes.py | 9 ++++++--- pylib/anki/scheduler/legacy.py | 3 ++- pylib/anki/tags.py | 9 +++++---- pylib/tools/genhooks.py | 2 +- qt/aqt/addcards.py | 8 ++++---- qt/aqt/browser.py | 7 ++++--- qt/aqt/find_and_replace.py | 7 +++++-- qt/aqt/main.py | 3 ++- qt/aqt/note_ops.py | 4 ++-- qt/aqt/notetypechooser.py | 3 ++- qt/aqt/scheduling_ops.py | 5 +++-- qt/aqt/tag_ops.py | 5 +++-- 18 files changed, 82 insertions(+), 59 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 7d9a4a20a..b71ccaf74 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -32,6 +32,7 @@ class Card: timerStarted: Optional[float] lastIvl: int ord: int + nid: anki.notes.NoteID def __init__( self, col: anki.collection.Collection, id: Optional[int] = None @@ -56,7 +57,7 @@ class Card: self._render_output = None self._note = None self.id = c.id - self.nid = c.note_id + self.nid = anki.notes.NoteID(c.note_id) self.did = c.deck_id self.ord = c.template_idx self.mod = c.mtime_secs diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 41258e67c..67272d50f 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -43,7 +43,7 @@ from anki.errors import AnkiError, DBError from anki.lang import TR, FormatTimeSpan from anki.media import MediaManager, media_paths_from_col_path from anki.models import ModelManager, NoteType -from anki.notes import Note +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.v3 import Scheduler as V3TestScheduler @@ -327,7 +327,7 @@ class Collection: Unlike card.flush(), this will invalidate any current checkpoint.""" self._backend.update_card(card=card._to_backend_card(), skip_undo_entry=False) - def get_note(self, id: int) -> Note: + def get_note(self, id: NoteID) -> Note: return Note(self, id=id) def update_note(self, note: Note) -> OpChanges: @@ -358,7 +358,7 @@ class Collection: # Deletion logging ########################################################################## - def _logRem(self, ids: List[int], type: int) -> None: + def _logRem(self, ids: List[Union[int, NoteID]], type: int) -> None: self.db.executemany( "insert into graves values (%d, ?, %d)" % (self.usn(), type), ([x] for x in ids), @@ -372,10 +372,10 @@ class Collection: def add_note(self, note: Note, deck_id: int) -> OpChanges: out = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id) - note.id = out.note_id + note.id = NoteID(out.note_id) return out.changes - def remove_notes(self, note_ids: Sequence[int]) -> OpChanges: + def remove_notes(self, note_ids: Sequence[NoteID]) -> OpChanges: hooks.notes_will_be_deleted(self, note_ids) return self._backend.remove_notes(note_ids=note_ids, card_ids=[]) @@ -387,7 +387,7 @@ class Collection: hooks.notes_will_be_deleted(self, nids) self._backend.remove_notes(note_ids=[], card_ids=card_ids) - def card_ids_of_note(self, note_id: int) -> Sequence[int]: + def card_ids_of_note(self, note_id: NoteID) -> Sequence[int]: return self._backend.cards_of_note(note_id) def defaults_for_adding( @@ -406,7 +406,7 @@ class Collection: home_deck_of_current_review_card=home_deck, ) - def default_deck_for_notetype(self, notetype_id: int) -> Optional[int]: + def default_deck_for_notetype(self, notetype_id: NoteID) -> Optional[int]: """If 'change deck depending on notetype' is enabled in the preferences, return the last deck used with the provided notetype, if any..""" if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): @@ -432,10 +432,10 @@ class Collection: self.add_note(note, note.model()["did"]) return len(note.cards()) - def remNotes(self, ids: Sequence[int]) -> None: + def remNotes(self, ids: Sequence[NoteID]) -> None: self.remove_notes(ids) - def _remNotes(self, ids: List[int]) -> None: + def _remNotes(self, ids: List[NoteID]) -> None: pass # Cards @@ -470,7 +470,7 @@ class Collection: ########################################################################## def after_note_updates( - self, nids: List[int], mark_modified: bool, generate_cards: bool = True + self, nids: List[NoteID], mark_modified: bool, generate_cards: bool = True ) -> None: self._backend.after_note_updates( nids=nids, generate_cards=generate_cards, mark_notes_modified=mark_modified @@ -478,11 +478,11 @@ class Collection: # legacy - def updateFieldCache(self, nids: List[int]) -> None: + def updateFieldCache(self, nids: List[NoteID]) -> None: self.after_note_updates(nids, mark_modified=False, generate_cards=False) # this also updates field cache - def genCards(self, nids: List[int]) -> List[int]: + def genCards(self, nids: List[NoteID]) -> List[int]: self.after_note_updates(nids, mark_modified=False, generate_cards=True) # previously returned empty cards, no longer does return [] @@ -527,7 +527,7 @@ class Collection: ) return self._backend.search_cards(search=query, order=mode) - def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[int]: + def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[NoteID]: """Return note ids matching the provided search or searches. If more than one search is provided, they will be ANDed together. @@ -538,12 +538,15 @@ class Collection: Eg: col.find_notes(SearchNode(deck="test"), "foo") will return notes that have a card in deck called "test", and have the text "foo". """ - return self._backend.search_notes(self.build_search_string(*terms)) + return [ + NoteID(did) + for did in self._backend.search_notes(self.build_search_string(*terms)) + ] def find_and_replace( self, *, - note_ids: Sequence[int], + note_ids: Sequence[NoteID], search: str, replacement: str, regex: bool = False, diff --git a/pylib/anki/find.py b/pylib/anki/find.py index 14692531a..659797554 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Set from anki.hooks import * +from anki.notes import NoteID if TYPE_CHECKING: from anki.collection import Collection @@ -29,7 +30,7 @@ class Finder: def findReplace( col: Collection, - nids: List[int], + nids: List[NoteID], src: str, dst: str, regex: bool = False, @@ -48,7 +49,7 @@ def findReplace( ).count -def fieldNamesForNotes(col: Collection, nids: List[int]) -> List[str]: +def fieldNamesForNotes(col: Collection, nids: List[NoteID]) -> List[str]: return list(col.field_names_for_note_ids(nids)) diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index b29137396..126a6bf3a 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -10,6 +10,7 @@ from anki.consts import * from anki.decks import DeckManager from anki.importing.base import Importer from anki.lang import TR +from anki.notes import NoteID from anki.utils import intTime, joinFields, splitFields, stripHTMLMedia GUID = 1 @@ -79,7 +80,7 @@ class Anki2Importer(Importer): def _importNotes(self) -> None: # build guid -> (id,mod,mid) hash & map of existing note ids - self._notes: Dict[str, Tuple[int, int, int]] = {} + self._notes: Dict[str, Tuple[NoteID, int, int]] = {} existing = {} for id, guid, mod, mid in self.dst.db.execute( "select id, guid, mod, mid from notes" diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index 8fe5ae3b7..5bb713fbe 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -10,6 +10,7 @@ from anki.config import Config from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR from anki.importing.base import Importer from anki.lang import TR +from anki.notes import NoteID from anki.utils import ( fieldChecksum, guid64, @@ -19,9 +20,9 @@ from anki.utils import ( timestampID, ) -type_tagsMapped = Tuple[int, int, str, str, int, str, str] -type_tagsModified = Tuple[int, int, str, str, int, str] -type_tagsElse = Tuple[int, int, str, int, str] +type_tagsMapped = Tuple[int, int, str, str, NoteID, str, str] +type_tagsModified = Tuple[int, int, str, str, NoteID, str] +type_tagsElse = Tuple[int, int, str, NoteID, str] type_udpates = Union[type_tagsMapped, type_tagsModified, type_tagsElse] # Stores a list of fields, tags and deck @@ -127,7 +128,7 @@ class NoteImporter(Importer): if f == "_tags": self._tagsMapped = True # gather checks for duplicate comparison - csums: Dict[str, List[int]] = {} + csums: Dict[str, List[NoteID]] = {} for csum, id in self.col.db.execute( "select csum, id from notes where mid = ?", self.model["id"] ): @@ -138,12 +139,12 @@ class NoteImporter(Importer): firsts: Dict[str, bool] = {} fld0idx = self.mapping.index(self.model["flds"][0]["name"]) self._fmap = self.col.models.fieldMap(self.model) - self._nextID = timestampID(self.col.db, "notes") + self._nextID = NoteID(timestampID(self.col.db, "notes")) # loop through the notes updates: List[type_udpates] = [] updateLog = [] new = [] - self._ids: List[int] = [] + self._ids: List[NoteID] = [] self._cards: List[Tuple] = [] dupeCount = 0 dupes: List[str] = [] @@ -242,9 +243,9 @@ class NoteImporter(Importer): def newData( self, n: ForeignNote - ) -> Tuple[int, str, int, int, int, str, str, str, int, int, str]: + ) -> Tuple[NoteID, str, int, int, int, str, str, str, int, int, str]: id = self._nextID - self._nextID += 1 + self._nextID = NoteID(self._nextID + 1) self._ids.append(id) self.processFields(n) # note id for card updates later @@ -266,14 +267,14 @@ class NoteImporter(Importer): def addNew( self, - rows: List[Tuple[int, str, int, int, int, str, str, str, int, int, str]], + rows: List[Tuple[NoteID, str, int, int, int, str, str, str, int, int, str]], ) -> None: self.col.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", rows ) def updateData( - self, n: ForeignNote, id: int, sflds: List[str] + self, n: ForeignNote, id: NoteID, sflds: List[str] ) -> Optional[type_udpates]: self._ids.append(id) self.processFields(n, sflds) diff --git a/pylib/anki/models.py b/pylib/anki/models.py index c585c1b61..735b1ec57 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -257,7 +257,7 @@ class ModelManager: # Tools ################################################## - def nids(self, ntid: int) -> List[int]: + def nids(self, ntid: int) -> List[anki.notes.NoteID]: "Note ids for M." if isinstance(ntid, dict): # legacy callers passed in note type @@ -420,7 +420,7 @@ and notes.mid = ? and cards.ord = ?""", def change( self, m: NoteType, - nids: List[int], + nids: List[anki.notes.NoteID], newModel: NoteType, fmap: Optional[Dict[int, Union[None, int]]], cmap: Optional[Dict[int, Union[None, int]]], @@ -434,7 +434,10 @@ and notes.mid = ? and cards.ord = ?""", self.col.after_note_updates(nids, mark_modified=True) def _changeNotes( - self, nids: List[int], newModel: NoteType, map: Dict[int, Union[None, int]] + self, + nids: List[anki.notes.NoteID], + newModel: NoteType, + map: Dict[int, Union[None, int]], ) -> None: d = [] nfields = len(newModel["flds"]) @@ -464,7 +467,7 @@ and notes.mid = ? and cards.ord = ?""", def _changeCards( self, - nids: List[int], + nids: List[anki.notes.NoteID], oldModel: NoteType, newModel: NoteType, map: Dict[int, Union[None, int]], diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index b438937db..b0b477858 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -5,7 +5,7 @@ from __future__ import annotations import copy import pprint -from typing import Any, List, Optional, Sequence, Tuple +from typing import Any, List, NewType, Optional, Sequence, Tuple import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb @@ -16,6 +16,9 @@ from anki.utils import joinFields DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State +# types +NoteID = NewType("NoteID", int) + class Note: # not currently exposed @@ -26,7 +29,7 @@ class Note: self, col: anki.collection.Collection, model: Optional[NoteType] = None, - id: Optional[int] = None, + id: Optional[NoteID] = None, ) -> None: assert not (model and id) self.col = col.weakref() @@ -46,7 +49,7 @@ class Note: self._load_from_backend_note(n) def _load_from_backend_note(self, n: _pb.Note) -> None: - self.id = n.id + self.id = NoteID(n.id) self.guid = n.guid self.mid = n.notetype_id self.mod = n.mtime_secs diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py index db13bd715..f75c30294 100644 --- a/pylib/anki/scheduler/legacy.py +++ b/pylib/anki/scheduler/legacy.py @@ -6,6 +6,7 @@ from typing import List, Optional, Tuple from anki.cards import Card from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN from anki.decks import DeckConfigDict +from anki.notes import NoteID from anki.scheduler.base import SchedulerBase, UnburyCurrentDeck from anki.utils import from_json_bytes, ids2str @@ -18,7 +19,7 @@ class SchedulerBaseWithLegacy(SchedulerBase): ) -> None: self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") - def buryNote(self, nid: int) -> None: + def buryNote(self, nid: NoteID) -> None: note = self.col.get_note(nid) self.bury_cards(note.card_ids()) diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index d139d9f23..fa57d7502 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -19,6 +19,7 @@ import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb import anki.collection from anki.collection import OpChangesWithCount +from anki.notes import NoteID from anki.utils import ids2str # public exports @@ -68,11 +69,11 @@ class TagManager: # Bulk addition/removal from specific notes ############################################################# - def bulk_add(self, note_ids: Sequence[int], tags: str) -> OpChangesWithCount: + def bulk_add(self, note_ids: Sequence[NoteID], tags: str) -> OpChangesWithCount: """Add space-separate tags to provided notes, returning changed count.""" return self.col._backend.add_note_tags(note_ids=note_ids, tags=tags) - def bulk_remove(self, note_ids: Sequence[int], tags: str) -> OpChangesWithCount: + def bulk_remove(self, note_ids: Sequence[NoteID], tags: str) -> OpChangesWithCount: return self.col._backend.remove_note_tags(note_ids=note_ids, tags=tags) # Find&replace @@ -175,12 +176,12 @@ class TagManager: ) -> None: print("tags.register() is deprecated and no longer works") - def bulkAdd(self, ids: List[int], tags: str, add: bool = True) -> None: + def bulkAdd(self, ids: List[NoteID], tags: str, add: bool = True) -> None: "Add tags in bulk. TAGS is space-separated." if add: self.bulk_add(ids, tags) else: self.bulk_remove(ids, tags) - def bulkRem(self, ids: List[int], tags: str) -> None: + def bulkRem(self, ids: List[NoteID], tags: str) -> None: self.bulkAdd(ids, tags, False) diff --git a/pylib/tools/genhooks.py b/pylib/tools/genhooks.py index a4d32c2ec..e47da9cef 100644 --- a/pylib/tools/genhooks.py +++ b/pylib/tools/genhooks.py @@ -23,7 +23,7 @@ hooks = [ Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"), Hook( name="notes_will_be_deleted", - args=["col: anki.collection.Collection", "ids: Sequence[int]"], + args=["col: anki.collection.Collection", "ids: Sequence[anki.notes.NoteID]"], legacy_hook="remNotes", ), Hook(name="media_files_did_export", args=["count: int"]), diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 0b1c7f3c2..fc57e8831 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -8,7 +8,7 @@ import aqt.editor import aqt.forms from anki.collection import OpChanges, SearchNode from anki.consts import MODEL_CLOZE -from anki.notes import DuplicateOrEmptyResult, Note +from anki.notes import DuplicateOrEmptyResult, Note, NoteID from anki.utils import htmlToTextLine, isMac from aqt import AnkiQt, gui_hooks from aqt.note_ops import add_note @@ -47,7 +47,7 @@ class AddCards(QDialog): self.setupEditor() self.setupButtons() self._load_new_note() - self.history: List[int] = [] + self.history: List[NoteID] = [] self._last_added_note: Optional[Note] = None restoreGeom(self, "add") addCloseShortcut(self) @@ -109,7 +109,7 @@ class AddCards(QDialog): def show_notetype_selector(self) -> None: self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype) - def on_notetype_change(self, notetype_id: int) -> None: + def on_notetype_change(self, notetype_id: NoteID) -> None: # need to adjust current deck? if deck_id := self.mw.col.default_deck_for_notetype(notetype_id): self.deck_chooser.selected_deck_id = deck_id @@ -178,7 +178,7 @@ class AddCards(QDialog): gui_hooks.add_cards_will_show_history_menu(self, m) m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) - def editHistory(self, nid: int) -> None: + def editHistory(self, nid: NoteID) -> None: aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) def add_current_note(self) -> None: diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index abba7cd51..1e0cd98fe 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -28,6 +28,7 @@ from anki.consts import * from anki.errors import NotFoundError from anki.lang import without_unicode_isolation from anki.models import NoteType +from anki.notes import NoteID from anki.stats import CardStats from anki.tags import MARKED_TAG from anki.utils import ids2str, isMac, isWin @@ -1054,7 +1055,7 @@ QTableView {{ gridline-color: {grid} }} for idx in self.form.tableView.selectionModel().selectedRows() ] - def selected_notes(self) -> List[int]: + def selected_notes(self) -> List[NoteID]: return self.col.db.list( """ select distinct nid from cards @@ -1073,7 +1074,7 @@ where id in %s""" % ",".join([str(s) for s in self.selected_notes()]) ) - def oneModelNotes(self) -> List[int]: + def oneModelNotes(self) -> List[NoteID]: sf = self.selected_notes() if not sf: return [] @@ -1603,7 +1604,7 @@ where id in %s""" class ChangeModel(QDialog): - def __init__(self, browser: Browser, nids: List[int]) -> None: + def __init__(self, browser: Browser, nids: List[NoteID]) -> None: QDialog.__init__(self, browser) self.browser = browser self.nids = nids diff --git a/qt/aqt/find_and_replace.py b/qt/aqt/find_and_replace.py index 52d4c0b5d..1c7fc3207 100644 --- a/qt/aqt/find_and_replace.py +++ b/qt/aqt/find_and_replace.py @@ -7,6 +7,7 @@ from typing import List, Optional, Sequence import aqt from anki.lang import TR +from anki.notes import NoteID from aqt import AnkiQt, QWidget from aqt.qt import QDialog, Qt from aqt.utils import ( @@ -31,7 +32,7 @@ def find_and_replace( *, mw: AnkiQt, parent: QWidget, - note_ids: Sequence[int], + note_ids: Sequence[NoteID], search: str, replacement: str, regex: bool, @@ -82,7 +83,9 @@ def find_and_replace_tag( class FindAndReplaceDialog(QDialog): COMBO_NAME = "BrowserFindAndReplace" - def __init__(self, parent: QWidget, *, mw: AnkiQt, note_ids: Sequence[int]) -> None: + def __init__( + self, parent: QWidget, *, mw: AnkiQt, note_ids: Sequence[NoteID] + ) -> None: super().__init__(parent) self.mw = mw self.note_ids = note_ids diff --git a/qt/aqt/main.py b/qt/aqt/main.py index f9e97fd74..e8e8fcd59 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -54,6 +54,7 @@ from anki.collection import ( ) from anki.decks import DeckDict from anki.hooks import runHook +from anki.notes import NoteID from anki.sound import AVTag, SoundOrVideoTag from anki.types import assert_exhaustive from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields @@ -1534,7 +1535,7 @@ title="%s" %s>%s""" % ( # Log note deletion ########################################################################## - def onRemNotes(self, col: Collection, nids: Sequence[int]) -> None: + def onRemNotes(self, col: Collection, nids: Sequence[NoteID]) -> None: path = os.path.join(self.pm.profileFolder(), "deleted.txt") existed = os.path.exists(path) with open(path, "ab") as f: diff --git a/qt/aqt/note_ops.py b/qt/aqt/note_ops.py index 582a380b4..042490988 100644 --- a/qt/aqt/note_ops.py +++ b/qt/aqt/note_ops.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Callable, Sequence -from anki.notes import Note +from anki.notes import Note, NoteID from aqt import AnkiQt from aqt.main import PerformOpOptionalSuccessCallback @@ -30,7 +30,7 @@ def update_note(*, mw: AnkiQt, note: Note, after_hooks: Callable[[], None]) -> N def remove_notes( *, mw: AnkiQt, - note_ids: Sequence[int], + note_ids: Sequence[NoteID], success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success) diff --git a/qt/aqt/notetypechooser.py b/qt/aqt/notetypechooser.py index dfddd5bd1..870a696e4 100644 --- a/qt/aqt/notetypechooser.py +++ b/qt/aqt/notetypechooser.py @@ -2,6 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from typing import List, Optional +from anki.notes import NoteID from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr @@ -29,7 +30,7 @@ class NoteTypeChooser(QHBoxLayout): widget: QWidget, starting_notetype_id: int, on_button_activated: Optional[Callable[[], None]] = None, - on_notetype_changed: Optional[Callable[[int], None]] = None, + on_notetype_changed: Optional[Callable[[NoteID], None]] = None, show_prefix_label: bool = True, ) -> None: QHBoxLayout.__init__(self) diff --git a/qt/aqt/scheduling_ops.py b/qt/aqt/scheduling_ops.py index 93828ffc0..43c594c30 100644 --- a/qt/aqt/scheduling_ops.py +++ b/qt/aqt/scheduling_ops.py @@ -9,6 +9,7 @@ import aqt from anki.collection import CARD_TYPE_NEW, Config from anki.decks import DeckID from anki.lang import TR +from anki.notes import NoteID from anki.scheduler import FilteredDeckForUpdate from aqt import AnkiQt from aqt.main import PerformOpOptionalSuccessCallback @@ -143,7 +144,7 @@ def suspend_cards( def suspend_note( *, mw: AnkiQt, - note_id: int, + note_id: NoteID, success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.taskman.run_in_background( @@ -168,7 +169,7 @@ def bury_cards( def bury_note( *, mw: AnkiQt, - note_id: int, + note_id: NoteID, success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.taskman.run_in_background( diff --git a/qt/aqt/tag_ops.py b/qt/aqt/tag_ops.py index f5f68abf4..d99ab8d65 100644 --- a/qt/aqt/tag_ops.py +++ b/qt/aqt/tag_ops.py @@ -7,6 +7,7 @@ from typing import Callable, Sequence from anki.collection import OpChangesWithCount from anki.lang import TR +from anki.notes import NoteID from aqt import AnkiQt, QWidget from aqt.main import PerformOpOptionalSuccessCallback from aqt.utils import showInfo, tooltip, tr @@ -15,7 +16,7 @@ from aqt.utils import showInfo, tooltip, tr def add_tags( *, mw: AnkiQt, - note_ids: Sequence[int], + note_ids: Sequence[NoteID], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, ) -> None: @@ -27,7 +28,7 @@ def add_tags( def remove_tags_for_notes( *, mw: AnkiQt, - note_ids: Sequence[int], + note_ids: Sequence[NoteID], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, ) -> None: From 986efeed199ff215d1f3be8d6e290e30cc29af37 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 10:36:52 +0100 Subject: [PATCH 03/16] NF: CardID type --- pylib/anki/cards.py | 10 +++++++--- pylib/anki/collection.py | 28 +++++++++++++++------------- pylib/anki/decks.py | 7 ++++--- pylib/anki/exporting.py | 3 ++- pylib/anki/importing/anki2.py | 3 ++- pylib/anki/notes.py | 3 ++- pylib/anki/scheduler/base.py | 19 ++++++++++--------- pylib/anki/scheduler/legacy.py | 6 +++--- pylib/anki/scheduler/v2.py | 12 ++++++------ qt/aqt/browser.py | 16 ++++++++-------- qt/aqt/card_ops.py | 6 ++++-- qt/aqt/emptycards.py | 9 +++++---- qt/aqt/exporting.py | 3 ++- qt/aqt/reviewer.py | 4 ++-- qt/aqt/scheduling_ops.py | 15 ++++++++------- 15 files changed, 80 insertions(+), 64 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index b71ccaf74..844c4f048 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -5,7 +5,7 @@ from __future__ import annotations import pprint import time -from typing import List, Optional +from typing import List, NewType, Optional import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb @@ -26,6 +26,9 @@ from anki.sound import AVTag # - rev queue: integer day # - lrn queue: integer timestamp +# types +CardID = NewType("CardID", int) + class Card: _note: Optional[Note] @@ -33,9 +36,10 @@ class Card: lastIvl: int ord: int nid: anki.notes.NoteID + id: CardID def __init__( - self, col: anki.collection.Collection, id: Optional[int] = None + self, col: anki.collection.Collection, id: Optional[CardID] = None ) -> None: self.col = col.weakref() self.timerStarted = None @@ -56,7 +60,7 @@ class Card: def _load_from_backend_card(self, c: _pb.Card) -> None: self._render_output = None self._note = None - self.id = c.id + self.id = CardID(c.id) self.nid = anki.notes.NoteID(c.note_id) self.did = c.deck_id self.ord = c.template_idx diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 67272d50f..fa0c8ddfe 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -34,7 +34,7 @@ from dataclasses import dataclass, field import anki.latex from anki import hooks from anki._backend import RustBackend -from anki.cards import Card +from anki.cards import Card, CardID from anki.config import Config, ConfigManager from anki.consts import * from anki.dbproxy import DBProxy @@ -319,7 +319,7 @@ class Collection: # Object creation helpers ########################################################################## - def get_card(self, id: int) -> Card: + def get_card(self, id: CardID) -> Card: return Card(self, id) def update_card(self, card: Card) -> None: @@ -379,7 +379,7 @@ class Collection: hooks.notes_will_be_deleted(self, note_ids) return self._backend.remove_notes(note_ids=note_ids, card_ids=[]) - def remove_notes_by_card(self, card_ids: List[int]) -> None: + def remove_notes_by_card(self, card_ids: List[CardID]) -> None: if hooks.notes_will_be_deleted.count(): nids = self.db.list( f"select nid from cards where id in {ids2str(card_ids)}" @@ -387,8 +387,8 @@ class Collection: hooks.notes_will_be_deleted(self, nids) self._backend.remove_notes(note_ids=[], card_ids=card_ids) - def card_ids_of_note(self, note_id: NoteID) -> Sequence[int]: - return self._backend.cards_of_note(note_id) + def card_ids_of_note(self, note_id: NoteID) -> Sequence[CardID]: + return [CardID(id) for id in self._backend.cards_of_note(note_id)] def defaults_for_adding( self, *, current_review_card: Optional[Card] @@ -447,11 +447,11 @@ class Collection: def cardCount(self) -> Any: return self.db.scalar("select count() from cards") - def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]) -> None: + def remove_cards_and_orphaned_notes(self, card_ids: Sequence[CardID]) -> None: "You probably want .remove_notes_by_card() instead." self._backend.remove_cards(card_ids=card_ids) - def set_deck(self, card_ids: Sequence[int], deck_id: int) -> OpChanges: + def set_deck(self, card_ids: Sequence[CardID], deck_id: int) -> OpChanges: return self._backend.set_deck(card_ids=card_ids, deck_id=deck_id) def get_empty_cards(self) -> EmptyCardsReport: @@ -459,10 +459,10 @@ class Collection: # legacy - def remCards(self, ids: List[int], notes: bool = True) -> None: + def remCards(self, ids: List[CardID], notes: bool = True) -> None: self.remove_cards_and_orphaned_notes(ids) - def emptyCids(self) -> List[int]: + def emptyCids(self) -> List[CardID]: print("emptyCids() will go away") return [] @@ -495,7 +495,7 @@ class Collection: query: str, order: Union[bool, str, BuiltinSort.Kind.V] = False, reverse: bool = False, - ) -> Sequence[int]: + ) -> Sequence[CardID]: """Return card ids matching the provided search. To programmatically construct a search string, see .build_search_string(). @@ -525,7 +525,9 @@ class Collection: mode = _pb.SortOrder( builtin=_pb.SortOrder.Builtin(kind=order, reverse=reverse) ) - return self._backend.search_cards(search=query, order=mode) + return [ + CardID(id) for id in self._backend.search_cards(search=query, order=mode) + ] def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[NoteID]: """Return note ids matching the provided search or searches. @@ -740,7 +742,7 @@ class Collection: return CollectionStats(self) - def card_stats(self, card_id: int, include_revlog: bool) -> str: + def card_stats(self, card_id: CardID, include_revlog: bool) -> str: import anki.stats as st if include_revlog: @@ -1033,7 +1035,7 @@ table.review-log {{ {revlog_style} }} ########################################################################## - def set_user_flag_for_cards(self, flag: int, cids: Sequence[int]) -> OpChanges: + def set_user_flag_for_cards(self, flag: int, cids: Sequence[CardID]) -> OpChanges: return self._backend.set_flag(card_ids=cids, flag=flag) def set_wants_abort(self) -> None: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index c3a1ec56b..3c38df99c 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Iterable, List, NewType, Optional, Sequence, Tuple import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb +from anki.cards import CardID from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithID from anki.consts import * from anki.errors import NotFoundError @@ -406,7 +407,7 @@ class DeckManager: return deck["name"] return None - def setDeck(self, cids: List[int], did: int) -> None: + def setDeck(self, cids: List[CardID], did: int) -> None: self.col.db.execute( f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", did, @@ -414,7 +415,7 @@ class DeckManager: intTime(), ) - def cids(self, did: int, children: bool = False) -> List[int]: + def cids(self, did: int, children: bool = False) -> List[CardID]: if not children: return self.col.db.list("select id from cards where did=?", did) dids = [did] @@ -422,7 +423,7 @@ class DeckManager: dids.append(id) return self.col.db.list(f"select id from cards where did in {ids2str(dids)}") - def for_card_ids(self, cids: List[int]) -> List[int]: + def for_card_ids(self, cids: List[CardID]) -> List[int]: return self.col.db.list(f"select did from cards where id in {ids2str(cids)}") # Deck selection diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 6a9d7fc9a..1a559c448 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union from zipfile import ZipFile from anki import hooks +from anki.cards import CardID from anki.collection import Collection from anki.lang import TR from anki.utils import ids2str, namedtmp, splitFields, stripHTML @@ -28,7 +29,7 @@ class Exporter: self, col: Collection, did: Optional[int] = None, - cids: Optional[List[int]] = None, + cids: Optional[List[CardID]] = None, ) -> None: self.col = col.weakref() self.did = did diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 126a6bf3a..15eb3b606 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -5,6 +5,7 @@ import os import unicodedata from typing import Any, Dict, List, Optional, Tuple +from anki.cards import CardID from anki.collection import Collection from anki.consts import * from anki.decks import DeckManager @@ -306,7 +307,7 @@ class Anki2Importer(Importer): if self.source_needs_upgrade: self.src.upgrade_to_v2_scheduler() # build map of (guid, ord) -> cid and used id cache - self._cards: Dict[Tuple[str, int], int] = {} + self._cards: Dict[Tuple[str, int], CardID] = {} existing = {} for guid, ord, cid in self.dst.db.execute( "select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id" diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index b0b477858..1f2895e08 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -24,6 +24,7 @@ class Note: # not currently exposed flags = 0 data = "" + id: NoteID def __init__( self, @@ -122,7 +123,7 @@ class Note: def cards(self) -> List[anki.cards.Card]: return [self.col.getCard(id) for id in self.card_ids()] - def card_ids(self) -> Sequence[int]: + def card_ids(self) -> Sequence[anki.cards.CardID]: return self.col.card_ids_of_note(self.id) def model(self) -> Optional[NoteType]: diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 81e7c3954..e61bae378 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -13,6 +13,7 @@ SchedTimingToday = _pb.SchedTimingTodayOut from typing import List, Optional, Sequence +from anki.cards import CardID from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV from anki.decks import DeckConfigDict, DeckID, DeckTreeNode from anki.notes import Note @@ -107,10 +108,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # Suspending & burying ########################################################################## - def unsuspend_cards(self, ids: Sequence[int]) -> OpChanges: + def unsuspend_cards(self, ids: Sequence[CardID]) -> OpChanges: return self.col._backend.restore_buried_and_suspended_cards(ids) - def unbury_cards(self, ids: List[int]) -> OpChanges: + def unbury_cards(self, ids: List[CardID]) -> OpChanges: return self.col._backend.restore_buried_and_suspended_cards(ids) def unbury_cards_in_current_deck( @@ -119,12 +120,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l ) -> None: self.col._backend.unbury_cards_in_current_deck(mode) - def suspend_cards(self, ids: Sequence[int]) -> OpChanges: + def suspend_cards(self, ids: Sequence[CardID]) -> OpChanges: return self.col._backend.bury_or_suspend_cards( card_ids=ids, mode=BuryOrSuspend.SUSPEND ) - def bury_cards(self, ids: Sequence[int], manual: bool = True) -> OpChanges: + def bury_cards(self, ids: Sequence[CardID], manual: bool = True) -> OpChanges: if manual: mode = BuryOrSuspend.BURY_USER else: @@ -137,13 +138,13 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # Resetting/rescheduling ########################################################################## - def schedule_cards_as_new(self, card_ids: List[int]) -> OpChanges: + def schedule_cards_as_new(self, card_ids: List[CardID]) -> OpChanges: "Put cards at the end of the new queue." return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True) def set_due_date( self, - card_ids: List[int], + card_ids: List[CardID], days: str, config_key: Optional[Config.String.Key.V] = None, ) -> OpChanges: @@ -162,7 +163,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l config_key=key, # type: ignore ) - def resetCards(self, ids: List[int]) -> None: + def resetCards(self, ids: List[CardID]) -> None: "Completely reset cards for export." sids = ids2str(ids) assert self.col.db @@ -184,7 +185,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l def reposition_new_cards( self, - card_ids: Sequence[int], + card_ids: Sequence[CardID], starting_from: int, step_size: int, randomize: bool, @@ -223,7 +224,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # legacy def sortCards( self, - cids: List[int], + cids: List[CardID], start: int = 1, step: int = 1, shuffle: bool = False, diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py index f75c30294..6eb2e1023 100644 --- a/pylib/anki/scheduler/legacy.py +++ b/pylib/anki/scheduler/legacy.py @@ -3,7 +3,7 @@ from typing import List, Optional, Tuple -from anki.cards import Card +from anki.cards import Card, CardID from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN from anki.decks import DeckConfigDict from anki.notes import NoteID @@ -15,7 +15,7 @@ class SchedulerBaseWithLegacy(SchedulerBase): "Legacy aliases and helpers. These will go away in the future." def reschedCards( - self, card_ids: List[int], min_interval: int, max_interval: int + self, card_ids: List[CardID], min_interval: int, max_interval: int ) -> None: self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") @@ -80,7 +80,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe self.col.usn(), ) - def remFromDyn(self, cids: List[int]) -> None: + def remFromDyn(self, cids: List[CardID]) -> None: self.emptyDyn(None, f"id in {ids2str(cids)} and odid") # used by v2 scheduler and some add-ons diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index b0ec0467c..a6d1aa8c5 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -11,7 +11,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki import hooks -from anki.cards import Card +from anki.cards import Card, CardID from anki.consts import * from anki.decks import DeckConfigDict, DeckDict from anki.lang import FormatTimeSpan @@ -144,7 +144,7 @@ class Scheduler(SchedulerBaseWithLegacy): def _resetNew(self) -> None: self._newDids = self.col.decks.active()[:] - self._newQueue: List[int] = [] + self._newQueue: List[CardID] = [] self._updateNewCardRatio() def _fillNew(self, recursing: bool = False) -> bool: @@ -301,8 +301,8 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW} def _resetLrn(self) -> None: self._updateLrnCutoff(force=True) self._resetLrnCount() - self._lrnQueue: List[Tuple[int, int]] = [] - self._lrnDayQueue: List[int] = [] + self._lrnQueue: List[Tuple[int, CardID]] = [] + self._lrnDayQueue: List[CardID] = [] self._lrnDids = self.col.decks.active()[:] # sub-day learning @@ -397,7 +397,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", return hooks.scheduler_review_limit_for_single_deck(lim, d) def _resetRev(self) -> None: - self._revQueue: List[int] = [] + self._revQueue: List[CardID] = [] def _fillRev(self, recursing: bool = False) -> bool: "True if a review card can be fetched." @@ -1072,7 +1072,7 @@ limit ?""" ########################################################################## def _burySiblings(self, card: Card) -> None: - toBury: List[int] = [] + toBury: List[CardID] = [] nconf = self._newConf(card) buryNew = nconf.get("bury", True) rconf = self._revConf(card) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 1e0cd98fe..b000976fe 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -22,7 +22,7 @@ from typing import ( import aqt import aqt.forms -from anki.cards import Card +from anki.cards import Card, CardID from anki.collection import BrowserRow, Collection, Config, OpChanges, SearchNode from anki.consts import * from anki.errors import NotFoundError @@ -97,7 +97,7 @@ class SearchContext: browser: Browser order: Union[bool, str] = True # if set, provided card ids will be used instead of the regular search - card_ids: Optional[Sequence[int]] = None + card_ids: Optional[Sequence[CardID]] = None # Data model @@ -170,13 +170,13 @@ class DataModel(QAbstractTableModel): self.activeCols: List[str] = self.col.get_config( "activeCols", ["noteFld", "template", "cardDue", "deck"] ) - self.cards: Sequence[int] = [] + self.cards: Sequence[CardID] = [] self._rows: Dict[int, CellRow] = {} self._last_refresh = 0.0 # serve stale content to avoid hitting the DB? self.block_updates = False - def get_id(self, index: QModelIndex) -> int: + def get_id(self, index: QModelIndex) -> CardID: return self.cards[index.row()] def get_cell(self, index: QModelIndex) -> Cell: @@ -198,7 +198,7 @@ class DataModel(QAbstractTableModel): self._rows[cid] = self._fetch_row_from_backend(cid) return self._rows[cid] - def _fetch_row_from_backend(self, cid: int) -> CellRow: + def _fetch_row_from_backend(self, cid: CardID) -> CellRow: try: row = CellRow(*self.col.browser_row_for_card(cid)) except NotFoundError: @@ -1049,7 +1049,7 @@ QTableView {{ gridline-color: {grid} }} # Menu helpers ###################################################################### - def selected_cards(self) -> List[int]: + def selected_cards(self) -> List[CardID]: return [ self.model.cards[idx.row()] for idx in self.form.tableView.selectionModel().selectedRows() @@ -1068,7 +1068,7 @@ where id in %s""" ) ) - def selectedNotesAsCards(self) -> List[int]: + def selectedNotesAsCards(self) -> List[CardID]: return self.col.db.list( "select id from cards where nid in (%s)" % ",".join([str(s) for s in self.selected_notes()]) @@ -1590,7 +1590,7 @@ where id in %s""" def onCardList(self) -> None: self.form.tableView.setFocus() - def focusCid(self, cid: int) -> None: + def focusCid(self, cid: CardID) -> None: try: row = list(self.model.cards).index(cid) except ValueError: diff --git a/qt/aqt/card_ops.py b/qt/aqt/card_ops.py index d7553527a..a4c22e6f5 100644 --- a/qt/aqt/card_ops.py +++ b/qt/aqt/card_ops.py @@ -5,12 +5,14 @@ from __future__ import annotations from typing import Sequence +from anki.cards import CardID +from anki.decks import DeckID from aqt import AnkiQt -def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[int], deck_id: int) -> None: +def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardID], deck_id: DeckID) -> None: mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id)) -def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[int], flag: int) -> None: +def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[CardID], flag: int) -> None: mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids)) diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index eb8099cbf..9f8ff67f8 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -5,9 +5,10 @@ from __future__ import annotations import re from concurrent.futures import Future -from typing import Any +from typing import Any, List import aqt +from anki.cards import CardID from anki.collection import EmptyCardsReport from aqt import gui_hooks from aqt.qt import QDialog, QDialogButtonBox, qconnect @@ -88,14 +89,14 @@ class EmptyCardsDialog(QDialog): self.mw.taskman.run_in_background(delete, on_done) def _delete_cards(self, keep_notes: bool) -> int: - to_delete = [] + to_delete: List[CardID] = [] note: EmptyCardsReport.NoteWithEmptyCards for note in self.report.notes: if keep_notes and note.will_delete_note: # leave first card - to_delete.extend(note.card_ids[1:]) + to_delete.extend([CardID(id) for id in note.card_ids[1:]]) else: - to_delete.extend(note.card_ids) + to_delete.extend([CardID(id) for id in note.card_ids]) self.mw.col.remove_cards_and_orphaned_notes(to_delete) return len(to_delete) diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index e1990f789..bb5ec145c 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -11,6 +11,7 @@ from typing import List, Optional import aqt from anki import hooks +from anki.cards import CardID from anki.exporting import Exporter, exporters from aqt.qt import * from aqt.utils import ( @@ -29,7 +30,7 @@ class ExportDialog(QDialog): self, mw: aqt.main.AnkiQt, did: Optional[int] = None, - cids: Optional[List[int]] = None, + cids: Optional[List[CardID]] = None, ): QDialog.__init__(self, mw, Qt.Window) self.mw = mw diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 169b4d216..e36062c0a 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -13,7 +13,7 @@ from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union from PyQt5.QtCore import Qt from anki import hooks -from anki.cards import Card +from anki.cards import Card, CardID from anki.collection import Config, OpChanges from anki.tags import MARKED_TAG from anki.utils import stripHTML @@ -74,7 +74,7 @@ class Reviewer: self.card: Optional[Card] = None self.cardQueue: List[Card] = [] self.hadCardQueue = False - self._answeredIds: List[int] = [] + self._answeredIds: List[CardID] = [] self._recordedAudio: Optional[str] = None self.typeCorrect: str = None # web init happens before this is set self.state: Optional[str] = None diff --git a/qt/aqt/scheduling_ops.py b/qt/aqt/scheduling_ops.py index 43c594c30..c0c345d2a 100644 --- a/qt/aqt/scheduling_ops.py +++ b/qt/aqt/scheduling_ops.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import List, Optional, Sequence import aqt +from anki.cards import CardID from anki.collection import CARD_TYPE_NEW, Config from anki.decks import DeckID from anki.lang import TR @@ -21,7 +22,7 @@ def set_due_date_dialog( *, mw: aqt.AnkiQt, parent: QWidget, - card_ids: List[int], + card_ids: List[CardID], config_key: Optional[Config.String.Key.V], ) -> None: if not card_ids: @@ -54,7 +55,7 @@ def set_due_date_dialog( ) -def forget_cards(*, mw: aqt.AnkiQt, parent: QWidget, card_ids: List[int]) -> None: +def forget_cards(*, mw: aqt.AnkiQt, parent: QWidget, card_ids: List[CardID]) -> None: if not card_ids: return @@ -67,7 +68,7 @@ def forget_cards(*, mw: aqt.AnkiQt, parent: QWidget, card_ids: List[int]) -> Non def reposition_new_cards_dialog( - *, mw: AnkiQt, parent: QWidget, card_ids: Sequence[int] + *, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardID] ) -> None: assert mw.col.db row = mw.col.db.first( @@ -112,7 +113,7 @@ def reposition_new_cards( *, mw: AnkiQt, parent: QWidget, - card_ids: Sequence[int], + card_ids: Sequence[CardID], starting_from: int, step_size: int, randomize: bool, @@ -135,7 +136,7 @@ def reposition_new_cards( def suspend_cards( *, mw: AnkiQt, - card_ids: Sequence[int], + card_ids: Sequence[CardID], success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success) @@ -153,14 +154,14 @@ def suspend_note( ) -def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[int]) -> None: +def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[CardID]) -> None: mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids)) def bury_cards( *, mw: AnkiQt, - card_ids: Sequence[int], + card_ids: Sequence[CardID], success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success) From 3b6802530d7635dd6adb9341b159b7a66befeae5 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 25 Mar 2021 13:57:25 +0100 Subject: [PATCH 04/16] NF: currentDeckID factorize odid or did --- pylib/anki/cards.py | 11 +++++++---- pylib/anki/collection.py | 2 +- pylib/anki/template.py | 2 +- qt/aqt/reviewer.py | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 844c4f048..703cf13fb 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -163,21 +163,24 @@ class Card: def startTimer(self) -> None: self.timerStarted = time.time() + def currentDeckID(self) -> int: + return anki.decks.DeckID(self.odid or self.did) + def timeLimit(self) -> int: "Time limit for answering in milliseconds." - conf = self.col.decks.confForDid(self.odid or self.did) + conf = self.col.decks.confForDid(self.currentDeckID()) return conf["maxTaken"] * 1000 def shouldShowTimer(self) -> bool: - conf = self.col.decks.confForDid(self.odid or self.did) + conf = self.col.decks.confForDid(self.currentDeckID()) return conf["timer"] def replay_question_audio_on_answer_side(self) -> bool: - conf = self.col.decks.confForDid(self.odid or self.did) + conf = self.col.decks.confForDid(self.currentDeckID()) return conf.get("replayq", True) def autoplay(self) -> bool: - return self.col.decks.confForDid(self.odid or self.did)["autoplay"] + return self.col.decks.confForDid(self.currentDeckID())["autoplay"] def timeTaken(self) -> int: "Time taken to answer card, in integer MS." diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index fa0c8ddfe..bf9d31519 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -398,7 +398,7 @@ class Collection: or current notetype. """ if card := current_review_card: - home_deck = card.odid or card.did + home_deck = card.currentDeckID() else: home_deck = 0 diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 131633b9d..ed3ce506d 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -172,7 +172,7 @@ class TemplateRenderContext: # add (most) special fields fields["Tags"] = self._note.stringTags().strip() fields["Type"] = self._note_type["name"] - fields["Deck"] = self._col.decks.name(self._card.odid or self._card.did) + fields["Deck"] = self._col.decks.name(self._card.currentDeckID()) fields["Subdeck"] = DeckManager.basename(fields["Deck"]) if self._template: fields["Card"] = self._template["name"] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index e36062c0a..a93336c40 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -834,7 +834,7 @@ time = %(time)d; qconnect(a.triggered, func) def onOptions(self) -> None: - self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did)) + self.mw.onDeckConf(self.mw.col.decks.get(self.card.currentDeckID())) def set_flag_on_current_card(self, desired_flag: int) -> None: # need to toggle off? From b8f715ffeaf35e5796c4303aad57481160a45438 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 25 Mar 2021 14:00:37 +0100 Subject: [PATCH 05/16] NF: default_deck_id as a constant Otherwise it's not clear what this 1 represents --- pylib/anki/decks.py | 5 +++-- pylib/anki/notes.py | 2 +- qt/aqt/deckchooser.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 3c38df99c..24439c728 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -31,6 +31,7 @@ DeckDict = Dict[str, Any] DeckConfigDict = Dict[str, Any] DeckID = NewType("DeckID", int) +default_deck_id = 1 class DecksDictProxy: @@ -248,7 +249,7 @@ class DeckManager: def get(self, did: Union[int, str], default: bool = True) -> Optional[DeckDict]: if not did: if default: - return self.get_legacy(1) + return self.get_legacy(default_deck_id) else: return None id = int(did) @@ -256,7 +257,7 @@ class DeckManager: if deck: return deck elif default: - return self.get_legacy(1) + return self.get_legacy(default_deck_id) else: return None diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 1f2895e08..27893275f 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -97,7 +97,7 @@ class Note: ) -> anki.cards.Card: card = anki.cards.Card(self.col) card.ord = ord - card.did = 1 + card.did = anki.decks.default_deck_id model = custom_note_type or self.model() template = copy.copy( diff --git a/qt/aqt/deckchooser.py b/qt/aqt/deckchooser.py index 70056aaa0..a6fc08da8 100644 --- a/qt/aqt/deckchooser.py +++ b/qt/aqt/deckchooser.py @@ -3,6 +3,7 @@ from typing import Optional +from anki.decks import default_deck_id from aqt import AnkiQt from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr @@ -70,7 +71,7 @@ class DeckChooser(QHBoxLayout): def _ensure_selected_deck_valid(self) -> None: if not self.mw.col.decks.get(self._selected_deck_id, default=False): - self.selected_deck_id = 1 + self.selected_deck_id = default_deck_id def _update_button_label(self) -> None: self.deck.setText(self.selected_deck_name().replace("&", "&&")) From 08e13013b237c9334ab8753e8e0a488f05be5446 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Thu, 25 Mar 2021 14:02:46 +0100 Subject: [PATCH 06/16] NF: default_deck_conf_id as constant So that the 1 is clearer --- pylib/anki/decks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 24439c728..7bc8dd37f 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -32,6 +32,7 @@ DeckConfigDict = Dict[str, Any] DeckID = NewType("DeckID", int) default_deck_id = 1 +default_deck_conf_id = 1 class DecksDictProxy: @@ -319,7 +320,7 @@ class DeckManager: conf = self.get_config(dcid) if not conf: # fall back on default - conf = self.get_config(1) + conf = self.get_config(default_deck_conf_id) conf["dyn"] = False return conf # dynamic decks have embedded conf @@ -579,7 +580,7 @@ class DeckManager: def new_filtered(self, name: str) -> int: "Return a new dynamic deck and set it as the current deck." - did = self.id(name, type=1) + did = self.id(name, type=default_deck_conf_id) self.select(did) return did From 6ac1e6477ed3b117eb9ff5b47da4671bde5a8941 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 10:53:47 +0100 Subject: [PATCH 07/16] NF: DeckID type --- pylib/anki/cards.py | 8 ++-- pylib/anki/collection.py | 14 +++--- pylib/anki/decks.py | 79 ++++++++++++++++++---------------- pylib/anki/exporting.py | 5 ++- pylib/anki/importing/anki2.py | 6 +-- pylib/anki/scheduler/base.py | 10 ++--- pylib/anki/scheduler/legacy.py | 8 ++-- pylib/anki/scheduler/v1.py | 12 +++--- pylib/anki/scheduler/v2.py | 12 +++--- pylib/anki/tags.py | 3 +- qt/aqt/addcards.py | 3 +- qt/aqt/deckbrowser.py | 12 +++--- qt/aqt/deckchooser.py | 14 +++--- qt/aqt/exporting.py | 5 ++- qt/aqt/main.py | 6 +-- qt/aqt/note_ops.py | 3 +- qt/aqt/sidebar.py | 4 +- qt/aqt/studydeck.py | 3 +- 18 files changed, 112 insertions(+), 95 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 703cf13fb..4e04bf453 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -37,6 +37,8 @@ class Card: ord: int nid: anki.notes.NoteID id: CardID + did: anki.decks.DeckID + odid: anki.decks.DeckID def __init__( self, col: anki.collection.Collection, id: Optional[CardID] = None @@ -62,7 +64,7 @@ class Card: self._note = None self.id = CardID(c.id) self.nid = anki.notes.NoteID(c.note_id) - self.did = c.deck_id + self.did = anki.decks.DeckID(c.deck_id) self.ord = c.template_idx self.mod = c.mtime_secs self.usn = c.usn @@ -75,7 +77,7 @@ class Card: self.lapses = c.lapses self.left = c.remaining_steps self.odue = c.original_due - self.odid = c.original_deck_id + self.odid = anki.decks.DeckID(c.original_deck_id) self.flags = c.flags self.data = c.data @@ -163,7 +165,7 @@ class Card: def startTimer(self) -> None: self.timerStarted = time.time() - def currentDeckID(self) -> int: + def currentDeckID(self) -> anki.decks.DeckID: return anki.decks.DeckID(self.odid or self.did) def timeLimit(self) -> int: diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index bf9d31519..761d66e65 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -38,7 +38,7 @@ from anki.cards import Card, CardID from anki.config import Config, ConfigManager from anki.consts import * from anki.dbproxy import DBProxy -from anki.decks import DeckManager +from anki.decks import DeckID, DeckManager from anki.errors import AnkiError, DBError from anki.lang import TR, FormatTimeSpan from anki.media import MediaManager, media_paths_from_col_path @@ -370,7 +370,7 @@ class Collection: def new_note(self, notetype: NoteType) -> Note: return Note(self, notetype) - def add_note(self, note: Note, deck_id: int) -> OpChanges: + def add_note(self, note: Note, deck_id: DeckID) -> OpChanges: out = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id) note.id = NoteID(out.note_id) return out.changes @@ -400,21 +400,23 @@ class Collection: if card := current_review_card: home_deck = card.currentDeckID() else: - home_deck = 0 + home_deck = DeckID(0) return self._backend.defaults_for_adding( home_deck_of_current_review_card=home_deck, ) - def default_deck_for_notetype(self, notetype_id: NoteID) -> Optional[int]: + def default_deck_for_notetype(self, notetype_id: NoteID) -> Optional[DeckID]: """If 'change deck depending on notetype' is enabled in the preferences, return the last deck used with the provided notetype, if any..""" if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): return None return ( - self._backend.default_deck_for_notetype( - ntid=notetype_id, + DeckID( + self._backend.default_deck_for_notetype( + ntid=notetype_id, + ) ) or None ) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 7bc8dd37f..7dde55020 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -31,7 +31,7 @@ DeckDict = Dict[str, Any] DeckConfigDict = Dict[str, Any] DeckID = NewType("DeckID", int) -default_deck_id = 1 +default_deck_id = DeckID(1) default_deck_conf_id = 1 @@ -45,7 +45,7 @@ class DecksDictProxy: def __getitem__(self, item: Any) -> Any: self._warn() - return self._col.decks.get(int(item)) + return self._col.decks.get(DeckID(int(item))) def __setitem__(self, key: Any, val: Any) -> None: self._warn() @@ -124,7 +124,7 @@ class DeckManager: name: str, create: bool = True, type: int = 0, - ) -> Optional[int]: + ) -> Optional[DeckID]: "Add a deck with NAME. Reuse deck if already exists. Return id as int." id = self.id_for_name(name) if id: @@ -135,17 +135,17 @@ class DeckManager: deck = self.new_deck_legacy(bool(type)) deck["name"] = name out = self.add_deck_legacy(deck) - return out.id + return DeckID(out.id) @legacy_func(sub="remove") - def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None: + def rem(self, did: DeckID, cardsToo: bool = True, childrenToo: bool = True) -> None: "Remove the deck. If cardsToo, delete any cards inside." if isinstance(did, str): did = int(did) assert cardsToo and childrenToo self.remove([did]) - def remove(self, dids: Sequence[int]) -> OpChangesWithCount: + def remove(self, dids: Sequence[DeckID]) -> OpChangesWithCount: return self.col._backend.remove_decks(dids) def all_names_and_ids( @@ -156,20 +156,20 @@ class DeckManager: skip_empty_default=skip_empty_default, include_filtered=include_filtered ) - def id_for_name(self, name: str) -> Optional[int]: + def id_for_name(self, name: str) -> Optional[DeckID]: try: - return self.col._backend.get_deck_id_by_name(name) + return DeckID(self.col._backend.get_deck_id_by_name(name)) except NotFoundError: return None - def get_legacy(self, did: int) -> Optional[DeckDict]: + def get_legacy(self, did: DeckID) -> Optional[DeckDict]: try: return from_json_bytes(self.col._backend.get_deck_legacy(did)) except NotFoundError: return None - def have(self, id: int) -> bool: - return not self.get_legacy(int(id)) + def have(self, id: DeckID) -> bool: + return not self.get_legacy(id) def get_all_legacy(self) -> List[DeckDict]: return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values()) @@ -191,7 +191,7 @@ class DeckManager: @classmethod def find_deck_in_tree( - cls, node: DeckTreeNode, deck_id: int + cls, node: DeckTreeNode, deck_id: DeckID ) -> Optional[DeckTreeNode]: if node.deck_id == deck_id: return node @@ -218,12 +218,12 @@ class DeckManager: ) ] - def collapse(self, did: int) -> None: + def collapse(self, did: DeckID) -> None: deck = self.get(did) deck["collapsed"] = not deck["collapsed"] self.save(deck) - def collapseBrowser(self, did: int) -> None: + def collapseBrowser(self, did: DeckID) -> None: deck = self.get(did) collapsed = deck.get("browserCollapsed", False) deck["browserCollapsed"] = not collapsed @@ -233,7 +233,7 @@ class DeckManager: return len(self.all_names_and_ids()) def card_count( - self, dids: Union[int, Iterable[int]], include_subdecks: bool + self, dids: Union[DeckID, Iterable[DeckID]], include_subdecks: bool ) -> Any: if isinstance(dids, int): dids = {dids} @@ -247,13 +247,13 @@ class DeckManager: ) return count - def get(self, did: Union[int, str], default: bool = True) -> Optional[DeckDict]: + def get(self, did: Union[DeckID, str], default: bool = True) -> Optional[DeckDict]: if not did: if default: return self.get_legacy(default_deck_id) else: return None - id = int(did) + id = DeckID(int(did)) deck = self.get_legacy(id) if deck: return deck @@ -297,7 +297,9 @@ class DeckManager: # legacy def renameForDragAndDrop( - self, draggedDeckDid: Union[int, str], ontoDeckDid: Optional[Union[int, str]] + self, + draggedDeckDid: Union[DeckID, str], + ontoDeckDid: Optional[Union[DeckID, str]], ) -> None: if not ontoDeckDid: onto = 0 @@ -312,7 +314,7 @@ class DeckManager: "A list of all deck config." return list(from_json_bytes(self.col._backend.all_deck_config_legacy())) - def confForDid(self, did: int) -> DeckConfigDict: + def confForDid(self, did: DeckID) -> DeckConfigDict: deck = self.get(did, default=False) assert deck if "conf" in deck: @@ -370,7 +372,7 @@ class DeckManager: grp["conf"] = id self.save(grp) - def didsForConf(self, conf: DeckConfigDict) -> List[int]: + def didsForConf(self, conf: DeckConfigDict) -> List[DeckID]: dids = [] for deck in self.all(): if "conf" in deck and deck["conf"] == conf["id"]: @@ -397,19 +399,19 @@ class DeckManager: # Deck utils ############################################################# - def name(self, did: int, default: bool = False) -> str: + def name(self, did: DeckID, default: bool = False) -> str: deck = self.get(did, default=default) if deck: return deck["name"] return self.col.tr(TR.DECKS_NO_DECK) - def name_if_exists(self, did: int) -> Optional[str]: + def name_if_exists(self, did: DeckID) -> Optional[str]: deck = self.get(did, default=False) if deck: return deck["name"] return None - def setDeck(self, cids: List[CardID], did: int) -> None: + def setDeck(self, cids: List[CardID], did: DeckID) -> None: self.col.db.execute( f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", did, @@ -417,7 +419,7 @@ class DeckManager: intTime(), ) - def cids(self, did: int, children: bool = False) -> List[CardID]: + def cids(self, did: DeckID, children: bool = False) -> List[CardID]: if not children: return self.col.db.list("select id from cards where did=?", did) dids = [did] @@ -425,13 +427,13 @@ class DeckManager: dids.append(id) return self.col.db.list(f"select id from cards where did in {ids2str(dids)}") - def for_card_ids(self, cids: List[CardID]) -> List[int]: + def for_card_ids(self, cids: List[CardID]) -> List[DeckID]: return self.col.db.list(f"select did from cards where id in {ids2str(cids)}") # Deck selection ############################################################# - def active(self) -> List[int]: + def active(self) -> List[DeckID]: "The currrently active dids." return self.col.get_config("activeDecks", [1]) @@ -442,10 +444,9 @@ class DeckManager: def current(self) -> DeckDict: return self.get(self.selected()) - def select(self, did: int) -> None: + def select(self, did: DeckID) -> None: "Select a new branch." # make sure arg is an int - did = int(did) current = self.selected() active = self.deck_and_child_ids(did) if current != did or active != self.active(): @@ -486,29 +487,31 @@ class DeckManager: def key(cls, deck: DeckDict) -> List[str]: return cls.path(deck["name"]) - def children(self, did: int) -> List[Tuple[str, int]]: + def children(self, did: DeckID) -> List[Tuple[str, DeckID]]: "All children of did, as (name, id)." name = self.get(did)["name"] actv = [] for g in self.all_names_and_ids(): if g.name.startswith(f"{name}::"): - actv.append((g.name, g.id)) + actv.append((g.name, DeckID(g.id))) return actv - def child_ids(self, parent_name: str) -> Iterable[int]: + def child_ids(self, parent_name: str) -> Iterable[DeckID]: prefix = f"{parent_name}::" - return (d.id for d in self.all_names_and_ids() if d.name.startswith(prefix)) + return ( + DeckID(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix) + ) - def deck_and_child_ids(self, deck_id: int) -> List[int]: + def deck_and_child_ids(self, deck_id: DeckID) -> List[DeckID]: parent_name = self.get_legacy(deck_id)["name"] out = [deck_id] out.extend(self.child_ids(parent_name)) return out - childMapNode = Dict[int, Any] + childMapNode = Dict[DeckID, Any] # Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type - def childDids(self, did: int, childMap: DeckManager.childMapNode) -> List: + def childDids(self, did: DeckID, childMap: DeckManager.childMapNode) -> List: def gather(node: DeckManager.childMapNode, arr: List) -> None: for did, child in node.items(): arr.append(did) @@ -536,7 +539,7 @@ class DeckManager: return childMap def parents( - self, did: int, nameMap: Optional[Dict[str, DeckDict]] = None + self, did: DeckID, nameMap: Optional[Dict[str, DeckDict]] = None ) -> List[DeckDict]: "All parents of did." # get parent and grandparent names @@ -578,14 +581,14 @@ class DeckManager: # Dynamic decks ########################################################################## - def new_filtered(self, name: str) -> int: + def new_filtered(self, name: str) -> DeckID: "Return a new dynamic deck and set it as the current deck." did = self.id(name, type=default_deck_conf_id) self.select(did) return did # 1 for dyn, 0 for standard - def isDyn(self, did: Union[int, str]) -> int: + def isDyn(self, did: Union[DeckID, str]) -> int: return self.get(did)["dyn"] # legacy diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 1a559c448..f169dfb91 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -14,6 +14,7 @@ from zipfile import ZipFile from anki import hooks from anki.cards import CardID from anki.collection import Collection +from anki.decks import DeckID from anki.lang import TR from anki.utils import ids2str, namedtmp, splitFields, stripHTML @@ -28,7 +29,7 @@ class Exporter: def __init__( self, col: Collection, - did: Optional[int] = None, + did: Optional[DeckID] = None, cids: Optional[List[CardID]] = None, ) -> None: self.col = col.weakref() @@ -185,7 +186,7 @@ class AnkiExporter(Exporter): def key(col: Collection) -> str: return col.tr(TR.EXPORTING_ANKI_20_DECK) - def deckIds(self) -> List[int]: + def deckIds(self) -> List[DeckID]: if self.cids: return self.col.decks.for_card_ids(self.cids) elif self.did: diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 15eb3b606..5cd5824d9 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple from anki.cards import CardID from anki.collection import Collection from anki.consts import * -from anki.decks import DeckManager +from anki.decks import DeckID, DeckManager from anki.importing.base import Importer from anki.lang import TR from anki.notes import NoteID @@ -31,7 +31,7 @@ class Anki2Importer(Importer): super().__init__(col, file) # set later, defined here for typechecking - self._decks: Dict[int, int] = {} + self._decks: Dict[DeckID, DeckID] = {} self.source_needs_upgrade = False def run(self, media: None = None) -> None: @@ -256,7 +256,7 @@ class Anki2Importer(Importer): # Decks ###################################################################### - def _did(self, did: int) -> Any: + def _did(self, did: DeckID) -> Any: "Given did in src col, return local id." # already converted? if did in self._decks: diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index e61bae378..6c9e3ea8d 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -91,10 +91,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # Filtered deck handling ########################################################################## - def rebuild_filtered_deck(self, deck_id: int) -> OpChangesWithCount: + def rebuild_filtered_deck(self, deck_id: DeckID) -> OpChangesWithCount: return self.col._backend.rebuild_filtered_deck(deck_id) - def empty_filtered_deck(self, deck_id: int) -> OpChanges: + def empty_filtered_deck(self, deck_id: DeckID) -> OpChanges: return self.col._backend.empty_filtered_deck(deck_id) def get_or_create_filtered_deck(self, deck_id: DeckID) -> FilteredDeckForUpdate: @@ -199,10 +199,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l shift_existing=shift_existing, ) - def randomizeCards(self, did: int) -> None: + def randomizeCards(self, did: DeckID) -> None: self.col._backend.sort_deck(deck_id=did, randomize=True) - def orderCards(self, did: int) -> None: + def orderCards(self, did: DeckID) -> None: self.col._backend.sort_deck(deck_id=did, randomize=False) def resortConf(self, conf: DeckConfigDict) -> None: @@ -213,7 +213,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self.orderCards(did) # for post-import - def maybeRandomizeDeck(self, did: Optional[int] = None) -> None: + def maybeRandomizeDeck(self, did: Optional[DeckID] = None) -> None: if not did: did = self.col.decks.selected() conf = self.col.decks.confForDid(did) diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py index 6eb2e1023..7aafc0192 100644 --- a/pylib/anki/scheduler/legacy.py +++ b/pylib/anki/scheduler/legacy.py @@ -5,7 +5,7 @@ from typing import List, Optional, Tuple from anki.cards import Card, CardID from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN -from anki.decks import DeckConfigDict +from anki.decks import DeckConfigDict, DeckID from anki.notes import NoteID from anki.scheduler.base import SchedulerBase, UnburyCurrentDeck from anki.utils import from_json_bytes, ids2str @@ -49,7 +49,7 @@ class SchedulerBaseWithLegacy(SchedulerBase): print("_nextDueMsg() is obsolete") return "" - def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]: + def rebuildDyn(self, did: Optional[DeckID] = None) -> Optional[int]: did = did or self.col.decks.selected() count = self.rebuild_filtered_deck(did).count or None if not count: @@ -58,7 +58,7 @@ class SchedulerBaseWithLegacy(SchedulerBase): self.col.decks.select(did) return count - def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None: + def emptyDyn(self, did: Optional[DeckID], lim: Optional[str] = None) -> None: if lim is None: self.empty_filtered_deck(did) return @@ -86,7 +86,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe # used by v2 scheduler and some add-ons def update_stats( self, - deck_id: int, + deck_id: DeckID, new_delta: int = 0, review_delta: int = 0, milliseconds_delta: int = 0, diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 6aec92fab..3d1c3c39c 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -12,6 +12,7 @@ import anki from anki import hooks from anki.cards import Card from anki.consts import * +from anki.decks import DeckID from anki.utils import ids2str, intTime from .v2 import QueueConfig @@ -299,7 +300,7 @@ limit %d""" if card.odid: card.did = card.odid card.odue = 0 - card.odid = 0 + card.odid = DeckID(0) # if rescheduling is off, it needs to be set back to a new card if not resched and not lapse: card.queue = card.type = CARD_TYPE_NEW @@ -401,7 +402,7 @@ where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CAR ) ) - def _lrnForDeck(self, did: int) -> int: + def _lrnForDeck(self, did: DeckID) -> int: cnt = ( self.col.db.scalar( f""" @@ -426,7 +427,7 @@ and due <= ? limit ?)""", # Reviews ########################################################################## - def _deckRevLimit(self, did: int) -> int: + def _deckRevLimit(self, did: DeckID) -> int: return self._deckNewLimit(did, self._deckRevLimitSingle) def _resetRev(self) -> None: @@ -541,7 +542,7 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""", card.due = card.odue if card.odid: card.did = card.odid - card.odid = 0 + card.odid = DeckID(0) card.odue = 0 # Interval management @@ -617,7 +618,8 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""", card.due = card.odue if card.odid: card.did = card.odid - card.odue = card.odid = 0 + card.odue = 0 + card.odid = DeckID(0) card.queue = QUEUE_TYPE_SUSPENDED # notify UI hooks.card_did_leech(card) diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index a6d1aa8c5..31fcd0f67 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -13,7 +13,7 @@ import anki._backend.backend_pb2 as _pb from anki import hooks from anki.cards import Card, CardID from anki.consts import * -from anki.decks import DeckConfigDict, DeckDict +from anki.decks import DeckConfigDict, DeckDict, DeckID from anki.lang import FormatTimeSpan from anki.scheduler.legacy import SchedulerBaseWithLegacy from anki.utils import ids2str, intTime @@ -75,7 +75,9 @@ class Scheduler(SchedulerBaseWithLegacy): def _reset_counts(self) -> None: tree = self.deck_due_tree(self.col.decks.selected()) - node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"])) + node = self.col.decks.find_deck_in_tree( + tree, DeckID(int(self.col.conf["curDeck"])) + ) if not node: # current deck points to a missing deck self.newCount = 0 @@ -210,7 +212,7 @@ class Scheduler(SchedulerBaseWithLegacy): return None def _deckNewLimit( - self, did: int, fn: Optional[Callable[[DeckDict], int]] = None + self, did: DeckID, fn: Optional[Callable[[DeckDict], int]] = None ) -> int: if not fn: fn = self._deckNewLimitSingle @@ -225,7 +227,7 @@ class Scheduler(SchedulerBaseWithLegacy): lim = min(rem, lim) return lim - def _newForDeck(self, did: int, lim: int) -> int: + def _newForDeck(self, did: DeckID, lim: int) -> int: "New count for a single deck." if not lim: return 0 @@ -788,7 +790,7 @@ limit ?""" if card.odid: card.did = card.odid card.odue = 0 - card.odid = 0 + card.odid = DeckID(0) def _restorePreviewCard(self, card: Card) -> None: assert card.odid diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index fa57d7502..036fb4efa 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -19,6 +19,7 @@ import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb import anki.collection from anki.collection import OpChangesWithCount +from anki.decks import DeckID from anki.notes import NoteID from anki.utils import ids2str @@ -49,7 +50,7 @@ class TagManager: def clear_unused_tags(self) -> OpChangesWithCount: return self.col._backend.clear_unused_tags() - def byDeck(self, did: int, children: bool = False) -> List[str]: + def byDeck(self, did: DeckID, children: bool = False) -> List[str]: basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" if not children: query = f"{basequery} AND c.did=?" diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index fc57e8831..66f13c037 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -8,6 +8,7 @@ import aqt.editor import aqt.forms from anki.collection import OpChanges, SearchNode from anki.consts import MODEL_CLOZE +from anki.decks import DeckID from anki.notes import DuplicateOrEmptyResult, Note, NoteID from anki.utils import htmlToTextLine, isMac from aqt import AnkiQt, gui_hooks @@ -69,7 +70,7 @@ class AddCards(QDialog): on_notetype_changed=self.on_notetype_change, ) self.deck_chooser = aqt.deckchooser.DeckChooser( - self.mw, self.form.deckArea, starting_deck_id=defaults.deck_id + self.mw, self.form.deckArea, starting_deck_id=DeckID(defaults.deck_id) ) def helpRequested(self) -> None: diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index ddf297189..ecb326d00 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -40,7 +40,7 @@ class DeckBrowserContent: @dataclass class RenderDeckNodeContext: - current_deck_id: int + current_deck_id: DeckID class DeckBrowser: @@ -101,7 +101,7 @@ class DeckBrowser: source, target = arg.split(",") self._handle_drag_and_drop(DeckID(int(source)), DeckID(int(target or 0))) elif cmd == "collapse": - self._collapse(int(arg)) + self._collapse(DeckID(int(arg))) elif cmd == "v2upgrade": self._confirm_upgrade() elif cmd == "v2upgradeinfo": @@ -112,7 +112,7 @@ class DeckBrowser: return False def _selDeck(self, did: str) -> None: - self.mw.col.decks.select(int(did)) + self.mw.col.decks.select(DeckID(int(did))) self.mw.onOverview() # HTML generation @@ -255,13 +255,13 @@ class DeckBrowser: a = m.addAction(tr(TR.ACTIONS_OPTIONS)) qconnect(a.triggered, lambda b, did=did: self._options(DeckID(int(did)))) a = m.addAction(tr(TR.ACTIONS_EXPORT)) - qconnect(a.triggered, lambda b, did=did: self._export(int(did))) + qconnect(a.triggered, lambda b, did=did: self._export(DeckID(int(did)))) a = m.addAction(tr(TR.ACTIONS_DELETE)) qconnect(a.triggered, lambda b, did=did: self._delete(DeckID(int(did)))) gui_hooks.deck_browser_will_show_options_menu(m, int(did)) m.exec_(QCursor.pos()) - def _export(self, did: int) -> None: + def _export(self, did: DeckID) -> None: self.mw.onExport(did=did) def _rename(self, did: DeckID) -> None: @@ -279,7 +279,7 @@ class DeckBrowser: self.mw.col.decks.select(did) self.mw.onDeckConf() - def _collapse(self, did: int) -> None: + def _collapse(self, did: DeckID) -> None: self.mw.col.decks.collapse(did) node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did) if node: diff --git a/qt/aqt/deckchooser.py b/qt/aqt/deckchooser.py index a6fc08da8..81ddc5ba8 100644 --- a/qt/aqt/deckchooser.py +++ b/qt/aqt/deckchooser.py @@ -3,7 +3,7 @@ from typing import Optional -from anki.decks import default_deck_id +from anki.decks import DeckID, default_deck_id from aqt import AnkiQt from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr @@ -15,17 +15,17 @@ class DeckChooser(QHBoxLayout): mw: AnkiQt, widget: QWidget, label: bool = True, - starting_deck_id: Optional[int] = None, + starting_deck_id: Optional[DeckID] = None, ) -> None: QHBoxLayout.__init__(self) self._widget = widget # type: ignore self.mw = mw self._setup_ui(show_label=label) - self._selected_deck_id = 0 + self._selected_deck_id = DeckID(0) # default to current deck if starting id not provided if starting_deck_id is None: - starting_deck_id = self.mw.col.get_config("curDeck", default=1) or 1 + starting_deck_id = DeckID(self.mw.col.get_config("curDeck", default=1) or 1) self.selected_deck_id = starting_deck_id def _setup_ui(self, show_label: bool) -> None: @@ -57,13 +57,13 @@ class DeckChooser(QHBoxLayout): ) @property - def selected_deck_id(self) -> int: + def selected_deck_id(self) -> DeckID: self._ensure_selected_deck_valid() return self._selected_deck_id @selected_deck_id.setter - def selected_deck_id(self, id: int) -> None: + def selected_deck_id(self, id: DeckID) -> None: if id != self._selected_deck_id: self._selected_deck_id = id self._ensure_selected_deck_valid() @@ -104,7 +104,7 @@ class DeckChooser(QHBoxLayout): onDeckChange = choose_deck deckName = selected_deck_name - def selectedId(self) -> int: + def selectedId(self) -> DeckID: return self.selected_deck_id def cleanup(self) -> None: diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index bb5ec145c..45fc0a1b5 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -12,6 +12,7 @@ from typing import List, Optional import aqt from anki import hooks from anki.cards import CardID +from anki.decks import DeckID from anki.exporting import Exporter, exporters from aqt.qt import * from aqt.utils import ( @@ -29,7 +30,7 @@ class ExportDialog(QDialog): def __init__( self, mw: aqt.main.AnkiQt, - did: Optional[int] = None, + did: Optional[DeckID] = None, cids: Optional[List[CardID]] = None, ): QDialog.__init__(self, mw, Qt.Window) @@ -43,7 +44,7 @@ class ExportDialog(QDialog): self.setup(did) self.exec_() - def setup(self, did: Optional[int]) -> None: + def setup(self, did: Optional[DeckID]) -> None: self.exporters = exporters(self.col) # if a deck specified, start with .apkg type selected idx = 0 diff --git a/qt/aqt/main.py b/qt/aqt/main.py index e8e8fcd59..ca67ac452 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -52,7 +52,7 @@ from anki.collection import ( UndoResult, UndoStatus, ) -from anki.decks import DeckDict +from anki.decks import DeckDict, DeckID from anki.hooks import runHook from anki.notes import NoteID from anki.sound import AVTag, SoundOrVideoTag @@ -518,7 +518,7 @@ class AnkiQt(QMainWindow): except Exception as e: if "FileTooNew" in str(e): showWarning( - "This profile requires a newer version of Anki to open. Did you forget to use the Downgrade button prior to switching Anki versions?" + "This profile requires a newer version of Anki to open. DeckID you forget to use the Downgrade button prior to switching Anki versions?" ) else: showWarning( @@ -1382,7 +1382,7 @@ title="%s" %s>%s""" % ( aqt.importing.onImport(self) - def onExport(self, did: Optional[int] = None) -> None: + def onExport(self, did: Optional[DeckID] = None) -> None: import aqt.exporting aqt.exporting.ExportDialog(self, did=did) diff --git a/qt/aqt/note_ops.py b/qt/aqt/note_ops.py index 042490988..6df6a028e 100644 --- a/qt/aqt/note_ops.py +++ b/qt/aqt/note_ops.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Callable, Sequence +from anki.decks import DeckID from anki.notes import Note, NoteID from aqt import AnkiQt from aqt.main import PerformOpOptionalSuccessCallback @@ -14,7 +15,7 @@ def add_note( *, mw: AnkiQt, note: Note, - target_deck_id: int, + target_deck_id: DeckID, success: PerformOpOptionalSuccessCallback = None, ) -> None: mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 1827da0e0..e773b28fe 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -976,7 +976,7 @@ class SidebarTreeView(QTreeView): for node in nodes: def toggle_expand() -> Callable[[bool], None]: - did = node.deck_id # pylint: disable=cell-var-from-loop + did = DeckID(node.deck_id) # pylint: disable=cell-var-from-loop return lambda _: self.mw.col.decks.collapseBrowser(did) item = SidebarItem( @@ -1158,7 +1158,7 @@ class SidebarTreeView(QTreeView): ########################### def rename_deck(self, item: SidebarItem, new_name: str) -> None: - deck = self.mw.col.decks.get(item.id) + deck = self.mw.col.decks.get(DeckID(item.id)) if not new_name: return new_name = item.name_prefix + new_name diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 6e18a3e8a..1156d662c 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -5,6 +5,7 @@ from typing import List, Optional import aqt from anki.collection import OpChangesWithID +from anki.decks import DeckID from aqt import gui_hooks from aqt.deck_ops import add_deck_dialog from aqt.qt import * @@ -167,7 +168,7 @@ class StudyDeck(QDialog): default = self.names[self.form.list.currentRow()] def success(out: OpChangesWithID) -> None: - deck = self.mw.col.decks.get(out.id) + deck = self.mw.col.decks.get(DeckID(out.id)) self.name = deck["name"] # make sure we clean up reset hook when manually exiting From b54410200eecb98d5eadb1e7bda10bc936e5939c Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 12:25:06 +0100 Subject: [PATCH 08/16] NF: DeckConfID --- pylib/anki/decks.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 7dde55020..261cb1da3 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -31,8 +31,11 @@ DeckDict = Dict[str, Any] DeckConfigDict = Dict[str, Any] DeckID = NewType("DeckID", int) +DeckConfID = NewType("DeckConfID", int) + default_deck_id = DeckID(1) -default_deck_conf_id = 1 +default_deck_conf_id = DeckConfID(1) + class DecksDictProxy: @@ -123,7 +126,7 @@ class DeckManager: self, name: str, create: bool = True, - type: int = 0, + type: DeckConfID = DeckConfID(0), ) -> Optional[DeckID]: "Add a deck with NAME. Reuse deck if already exists. Return id as int." id = self.id_for_name(name) @@ -318,7 +321,7 @@ class DeckManager: deck = self.get(did, default=False) assert deck if "conf" in deck: - dcid = int(deck["conf"]) # may be a string + dcid = DeckConfID(int(deck["conf"])) # may be a string conf = self.get_config(dcid) if not conf: # fall back on default @@ -328,7 +331,7 @@ class DeckManager: # dynamic decks have embedded conf return deck - def get_config(self, conf_id: int) -> Optional[DeckConfigDict]: + def get_config(self, conf_id: DeckConfID) -> Optional[DeckConfigDict]: try: return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id)) except NotFoundError: @@ -353,10 +356,10 @@ class DeckManager: def add_config_returning_id( self, name: str, clone_from: Optional[DeckConfigDict] = None - ) -> int: + ) -> DeckConfID: return self.add_config(name, clone_from)["id"] - def remove_config(self, id: int) -> None: + def remove_config(self, id: DeckConfID) -> None: "Remove a configuration and update all decks using it." self.col.modSchema(check=True) for g in self.all(): @@ -368,7 +371,7 @@ class DeckManager: self.save(g) self.col._backend.remove_deck_config(id) - def setConf(self, grp: DeckConfigDict, id: int) -> None: + def setConf(self, grp: DeckConfigDict, id: DeckConfID) -> None: grp["conf"] = id self.save(grp) From 7ea862931cd342ac01e424371b468115b0b1ca76 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 12:41:24 +0100 Subject: [PATCH 09/16] NF: NoteTypeID type --- pylib/anki/collection.py | 6 +++--- pylib/anki/importing/anki2.py | 11 ++++++----- pylib/anki/importing/noteimp.py | 7 +++++-- pylib/anki/media.py | 3 ++- pylib/anki/models.py | 35 +++++++++++++++++---------------- pylib/anki/notes.py | 5 +++-- qt/aqt/addcards.py | 5 +++-- qt/aqt/models.py | 4 ++-- qt/aqt/notetypechooser.py | 18 ++++++++++------- qt/aqt/sidebar.py | 8 ++++++-- 10 files changed, 59 insertions(+), 43 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 761d66e65..b0798c27b 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -42,7 +42,7 @@ from anki.decks import DeckID, DeckManager from anki.errors import AnkiError, DBError from anki.lang import TR, FormatTimeSpan from anki.media import MediaManager, media_paths_from_col_path -from anki.models import ModelManager, NoteType +from anki.models import ModelManager, NoteType, NoteTypeID from anki.notes import Note, NoteID from anki.scheduler.v1 import Scheduler as V1Scheduler from anki.scheduler.v2 import Scheduler as V2Scheduler @@ -406,7 +406,7 @@ class Collection: home_deck_of_current_review_card=home_deck, ) - def default_deck_for_notetype(self, notetype_id: NoteID) -> Optional[DeckID]: + def default_deck_for_notetype(self, notetype_id: NoteTypeID) -> Optional[DeckID]: """If 'change deck depending on notetype' is enabled in the preferences, return the last deck used with the provided notetype, if any..""" if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): @@ -578,7 +578,7 @@ class Collection: dupes = [] fields: Dict[int, int] = {} - def ordForMid(mid: int) -> int: + def ordForMid(mid: NoteTypeID) -> int: if mid not in fields: model = self.models.get(mid) for c, f in enumerate(model["flds"]): diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 5cd5824d9..bf92198d4 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -11,6 +11,7 @@ from anki.consts import * from anki.decks import DeckID, DeckManager from anki.importing.base import Importer from anki.lang import TR +from anki.models import NoteTypeID from anki.notes import NoteID from anki.utils import intTime, joinFields, splitFields, stripHTMLMedia @@ -81,7 +82,7 @@ class Anki2Importer(Importer): def _importNotes(self) -> None: # build guid -> (id,mod,mid) hash & map of existing note ids - self._notes: Dict[str, Tuple[NoteID, int, int]] = {} + self._notes: Dict[str, Tuple[NoteID, int, NoteTypeID]] = {} existing = {} for id, guid, mod, mid in self.dst.db.execute( "select id, guid, mod, mid from notes" @@ -217,9 +218,9 @@ class Anki2Importer(Importer): def _prepareModels(self) -> None: "Prepare index of schema hashes." - self._modelMap: Dict[int, int] = {} + self._modelMap: Dict[NoteTypeID, NoteTypeID] = {} - def _mid(self, srcMid: int) -> Any: + def _mid(self, srcMid: NoteTypeID) -> Any: "Return local id for remote MID." # already processed this mid? if srcMid in self._modelMap: @@ -248,7 +249,7 @@ class Anki2Importer(Importer): self.dst.models.update(model) break # as they don't match, try next id - mid += 1 + mid = NoteTypeID(mid + 1) # save map and return new mid self._modelMap[srcMid] = mid return mid @@ -432,7 +433,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", # the user likely used subdirectories pass - def _mungeMedia(self, mid: int, fieldsStr: str) -> str: + def _mungeMedia(self, mid: NoteTypeID, fieldsStr: str) -> str: fields = splitFields(fieldsStr) def repl(match): diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index 5bb713fbe..35bb4fa71 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -10,6 +10,7 @@ from anki.config import Config from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR from anki.importing.base import Importer from anki.lang import TR +from anki.models import NoteTypeID from anki.notes import NoteID from anki.utils import ( fieldChecksum, @@ -243,7 +244,7 @@ class NoteImporter(Importer): def newData( self, n: ForeignNote - ) -> Tuple[NoteID, str, int, int, int, str, str, str, int, int, str]: + ) -> Tuple[NoteID, str, NoteTypeID, int, int, str, str, str, int, int, str]: id = self._nextID self._nextID = NoteID(self._nextID + 1) self._ids.append(id) @@ -267,7 +268,9 @@ class NoteImporter(Importer): def addNew( self, - rows: List[Tuple[NoteID, str, int, int, int, str, str, str, int, int, str]], + rows: List[ + Tuple[NoteID, str, NoteTypeID, int, int, str, str, str, int, int, str] + ], ) -> None: self.col.db.executemany( "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", rows diff --git a/pylib/anki/media.py b/pylib/anki/media.py index 3becaf196..cb37d9e62 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -17,6 +17,7 @@ import anki import anki._backend.backend_pb2 as _pb from anki.consts import * from anki.latex import render_latex, render_latex_returning_errors +from anki.models import NoteTypeID from anki.sound import SoundOrVideoTag from anki.template import av_tags_to_native from anki.utils import intTime @@ -159,7 +160,7 @@ class MediaManager: ########################################################################## def filesInStr( - self, mid: int, string: str, includeRemote: bool = False + self, mid: NoteTypeID, string: str, includeRemote: bool = False ) -> List[str]: l = [] model = self.col.models.get(mid) diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 735b1ec57..1050f9faa 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -8,7 +8,7 @@ import pprint import sys import time import traceback -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb @@ -35,6 +35,7 @@ NoteTypeNameIDUseCount = _pb.NoteTypeNameIDUseCount NoteType = Dict[str, Any] Field = Dict[str, Any] Template = Dict[str, Union[str, int, None]] +NoteTypeID = NewType("NoteTypeID", int) class ModelsDictProxy: @@ -47,7 +48,7 @@ class ModelsDictProxy: def __getitem__(self, item: Any) -> Any: self._warn() - return self._col.models.get(int(item)) + return self._col.models.get(NoteTypeID(int(item))) def __setitem__(self, key: str, val: Any) -> None: self._warn() @@ -114,16 +115,16 @@ class ModelManager: # need to cache responses from the backend. Please do not # access the cache directly! - _cache: Dict[int, NoteType] = {} + _cache: Dict[NoteTypeID, NoteType] = {} def _update_cache(self, nt: NoteType) -> None: self._cache[nt["id"]] = nt - def _remove_from_cache(self, ntid: int) -> None: + def _remove_from_cache(self, ntid: NoteTypeID) -> None: if ntid in self._cache: del self._cache[ntid] - def _get_cached(self, ntid: int) -> Optional[NoteType]: + def _get_cached(self, ntid: NoteTypeID) -> Optional[NoteType]: return self._cache.get(ntid) def _clear_cache(self) -> None: @@ -143,11 +144,11 @@ class ModelManager: def allNames(self) -> List[str]: return [n.name for n in self.all_names_and_ids()] - def ids(self) -> List[int]: - return [n.id for n in self.all_names_and_ids()] + def ids(self) -> List[NoteTypeID]: + return [NoteTypeID(n.id) for n in self.all_names_and_ids()] # only used by importing code - def have(self, id: int) -> bool: + def have(self, id: NoteTypeID) -> bool: if isinstance(id, str): id = int(id) return any(True for e in self.all_names_and_ids() if e.id == id) @@ -162,7 +163,7 @@ class ModelManager: m = self.get(self.col.conf["curModel"]) if m: return m - return self.get(self.all_names_and_ids()[0].id) + return self.get(NoteTypeID(self.all_names_and_ids()[0].id)) def setCurrent(self, m: NoteType) -> None: self.col.conf["curModel"] = m["id"] @@ -170,13 +171,13 @@ class ModelManager: # Retrieving and creating models ############################################################# - def id_for_name(self, name: str) -> Optional[int]: + def id_for_name(self, name: str) -> Optional[NoteTypeID]: try: - return self.col._backend.get_notetype_id_by_name(name) + return NoteTypeID(self.col._backend.get_notetype_id_by_name(name)) except NotFoundError: return None - def get(self, id: int) -> Optional[NoteType]: + def get(self, id: NoteTypeID) -> Optional[NoteType]: "Get model with ID, or None." # deal with various legacy input types if id is None: @@ -195,7 +196,7 @@ class ModelManager: def all(self) -> List[NoteType]: "Get all models." - return [self.get(nt.id) for nt in self.all_names_and_ids()] + return [self.get(NoteTypeID(nt.id)) for nt in self.all_names_and_ids()] def byName(self, name: str) -> Optional[NoteType]: "Get model with NAME." @@ -222,10 +223,10 @@ class ModelManager: def remove_all_notetypes(self) -> None: for nt in self.all_names_and_ids(): - self._remove_from_cache(nt.id) + self._remove_from_cache(NoteTypeID(nt.id)) self.col._backend.remove_notetype(nt.id) - def remove(self, id: int) -> None: + def remove(self, id: NoteTypeID) -> None: "Modifies schema." self._remove_from_cache(id) self.col._backend.remove_notetype(id) @@ -257,7 +258,7 @@ class ModelManager: # Tools ################################################## - def nids(self, ntid: int) -> List[anki.notes.NoteID]: + def nids(self, ntid: NoteTypeID) -> List[anki.notes.NoteID]: "Note ids for M." if isinstance(ntid, dict): # legacy callers passed in note type @@ -403,7 +404,7 @@ class ModelManager: self.reposition_template(m, template, idx) self.save(m) - def template_use_count(self, ntid: int, ord: int) -> int: + def template_use_count(self, ntid: NoteTypeID, ord: int) -> int: return self.col.db.scalar( """ select count() from cards, notes where cards.nid = notes.id diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 27893275f..00de732f8 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -11,7 +11,7 @@ import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki import hooks from anki.consts import MODEL_STD -from anki.models import NoteType, Template +from anki.models import NoteType, NoteTypeID, Template from anki.utils import joinFields DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State @@ -25,6 +25,7 @@ class Note: flags = 0 data = "" id: NoteID + mid: NoteTypeID def __init__( self, @@ -52,7 +53,7 @@ class Note: def _load_from_backend_note(self, n: _pb.Note) -> None: self.id = NoteID(n.id) self.guid = n.guid - self.mid = n.notetype_id + self.mid = NoteTypeID(n.notetype_id) self.mod = n.mtime_secs self.usn = n.usn self.tags = list(n.tags) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 66f13c037..c0a878130 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -9,6 +9,7 @@ import aqt.forms from anki.collection import OpChanges, SearchNode from anki.consts import MODEL_CLOZE from anki.decks import DeckID +from anki.models import NoteTypeID from anki.notes import DuplicateOrEmptyResult, Note, NoteID from anki.utils import htmlToTextLine, isMac from aqt import AnkiQt, gui_hooks @@ -65,7 +66,7 @@ class AddCards(QDialog): self.notetype_chooser = NoteTypeChooser( mw=self.mw, widget=self.form.modelArea, - starting_notetype_id=defaults.notetype_id, + starting_notetype_id=NoteTypeID(defaults.notetype_id), on_button_activated=self.show_notetype_selector, on_notetype_changed=self.on_notetype_change, ) @@ -110,7 +111,7 @@ class AddCards(QDialog): def show_notetype_selector(self) -> None: self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype) - def on_notetype_change(self, notetype_id: NoteID) -> None: + def on_notetype_change(self, notetype_id: NoteTypeID) -> None: # need to adjust current deck? if deck_id := self.mw.col.default_deck_for_notetype(notetype_id): self.deck_chooser.selected_deck_id = deck_id diff --git a/qt/aqt/models.py b/qt/aqt/models.py index a893178cc..84fcf197a 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -8,7 +8,7 @@ from typing import Any, List, Optional, Sequence import aqt.clayout from anki import stdmodels from anki.lang import without_unicode_isolation -from anki.models import NoteType, NoteTypeNameIDUseCount +from anki.models import NoteType, NoteTypeID, NoteTypeNameIDUseCount from anki.notes import Note from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -33,7 +33,7 @@ class Models(QDialog): mw: AnkiQt, parent: Optional[QWidget] = None, fromMain: bool = False, - selected_notetype_id: Optional[int] = None, + selected_notetype_id: Optional[NoteTypeID] = None, ): self.mw = mw parent = parent or mw diff --git a/qt/aqt/notetypechooser.py b/qt/aqt/notetypechooser.py index 870a696e4..96205f36c 100644 --- a/qt/aqt/notetypechooser.py +++ b/qt/aqt/notetypechooser.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from typing import List, Optional -from anki.notes import NoteID +from anki.models import NoteTypeID from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr @@ -23,14 +23,16 @@ class NoteTypeChooser(QHBoxLayout): deleted. """ + _selected_notetype_id: NoteTypeID + def __init__( self, *, mw: AnkiQt, widget: QWidget, - starting_notetype_id: int, + starting_notetype_id: NoteTypeID, on_button_activated: Optional[Callable[[], None]] = None, - on_notetype_changed: Optional[Callable[[NoteID], None]] = None, + on_notetype_changed: Optional[Callable[[NoteTypeID], None]] = None, show_prefix_label: bool = True, ) -> None: QHBoxLayout.__init__(self) @@ -42,7 +44,7 @@ class NoteTypeChooser(QHBoxLayout): self.on_button_activated = self.choose_notetype self._setup_ui(show_label=show_prefix_label) gui_hooks.state_did_reset.append(self.reset_state) - self._selected_notetype_id = 0 + self._selected_notetype_id = NoteTypeID(0) # triggers UI update; avoid firing changed hook on startup self.on_notetype_changed = None self.selected_notetype_id = starting_notetype_id @@ -119,7 +121,7 @@ class NoteTypeChooser(QHBoxLayout): self.selected_notetype_id = id @property - def selected_notetype_id(self) -> int: + def selected_notetype_id(self) -> NoteTypeID: # theoretically this should not be necessary, as we're listening to # resets self._ensure_selected_notetype_valid() @@ -127,7 +129,7 @@ class NoteTypeChooser(QHBoxLayout): return self._selected_notetype_id @selected_notetype_id.setter - def selected_notetype_id(self, id: int) -> None: + def selected_notetype_id(self, id: NoteTypeID) -> None: if id != self._selected_notetype_id: self._selected_notetype_id = id self._ensure_selected_notetype_valid() @@ -140,7 +142,9 @@ class NoteTypeChooser(QHBoxLayout): def _ensure_selected_notetype_valid(self) -> None: if not self.mw.col.models.get(self._selected_notetype_id): - self.selected_notetype_id = self.mw.col.models.all_names_and_ids()[0].id + self.selected_notetype_id = NoteTypeID( + self.mw.col.models.all_names_and_ids()[0].id + ) def _update_button_label(self) -> None: self.button.setText(self.selected_notetype_name().replace("&", "&&")) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index e773b28fe..d8be37abb 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -9,6 +9,7 @@ from typing import Dict, Iterable, List, Optional, Tuple, cast import aqt from anki.collection import Config, OpChanges, SearchJoiner, SearchNode from anki.decks import DeckID, DeckTreeNode +from anki.models import NoteTypeID from anki.notes import Note from anki.tags import TagTreeNode from anki.types import assert_exhaustive @@ -1291,11 +1292,14 @@ class SidebarTreeView(QTreeView): def manage_notetype(self, item: SidebarItem) -> None: Models( - self.mw, parent=self.browser, fromMain=True, selected_notetype_id=item.id + self.mw, + parent=self.browser, + fromMain=True, + selected_notetype_id=NoteTypeID(item.id), ) def manage_template(self, item: SidebarItem) -> None: - note = Note(self.col, self.col.models.get(item._parent_item.id)) + note = Note(self.col, self.col.models.get(NoteTypeID(item._parent_item.id))) CardLayout(self.mw, note, ord=item.id, parent=self, fill_empty=True) # Helpers From 365de5f232111ba0b458e7867ee08347b16b4652 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 13:14:57 +0100 Subject: [PATCH 10/16] NF: CardQueue type --- pylib/anki/cards.py | 3 ++- pylib/anki/consts.py | 19 ++++++++++--------- pylib/anki/scheduler/v1.py | 5 +++-- pylib/anki/scheduler/v2.py | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 4e04bf453..3ccfa49a0 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -39,6 +39,7 @@ class Card: id: CardID did: anki.decks.DeckID odid: anki.decks.DeckID + queue: CardQueue def __init__( self, col: anki.collection.Collection, id: Optional[CardID] = None @@ -69,7 +70,7 @@ class Card: self.mod = c.mtime_secs self.usn = c.usn self.type = c.ctype - self.queue = c.queue + self.queue = CardQueue(c.queue) self.due = c.due self.ivl = c.interval self.factor = c.ease_factor diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index 1e472bdf4..92818889f 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Any, Dict, NewType, Optional import anki from anki.lang import TR @@ -18,14 +18,15 @@ NEW_CARDS_RANDOM = 0 NEW_CARDS_DUE = 1 # Queue types -QUEUE_TYPE_MANUALLY_BURIED = -3 -QUEUE_TYPE_SIBLING_BURIED = -2 -QUEUE_TYPE_SUSPENDED = -1 -QUEUE_TYPE_NEW = 0 -QUEUE_TYPE_LRN = 1 -QUEUE_TYPE_REV = 2 -QUEUE_TYPE_DAY_LEARN_RELEARN = 3 -QUEUE_TYPE_PREVIEW = 4 +CardQueue = NewType("CardQueue", int) +QUEUE_TYPE_MANUALLY_BURIED = CardQueue(-3) +QUEUE_TYPE_SIBLING_BURIED = CardQueue(-2) +QUEUE_TYPE_SUSPENDED = CardQueue(-1) +QUEUE_TYPE_NEW = CardQueue(0) +QUEUE_TYPE_LRN = CardQueue(1) +QUEUE_TYPE_REV = CardQueue(2) +QUEUE_TYPE_DAY_LEARN_RELEARN = CardQueue(3) +QUEUE_TYPE_PREVIEW = CardQueue(4) # Card types CARD_TYPE_NEW = 0 diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 3d1c3c39c..04d1004a1 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -98,7 +98,7 @@ class Scheduler(V2): if card: idx = self.countIdx(card) if idx == QUEUE_TYPE_LRN: - counts[QUEUE_TYPE_LRN] += card.left // 1000 + counts[int(QUEUE_TYPE_LRN)] += card.left // 1000 else: counts[idx] += 1 @@ -303,7 +303,8 @@ limit %d""" card.odid = DeckID(0) # if rescheduling is off, it needs to be set back to a new card if not resched and not lapse: - card.queue = card.type = CARD_TYPE_NEW + card.queue = QUEUE_TYPE_NEW + card.type = CARD_TYPE_NEW card.due = self.col.nextID("pos") def _startingLeft(self, card: Card) -> int: diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index 31fcd0f67..ae24aa1f9 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -805,7 +805,7 @@ limit ?""" else: card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN else: - card.queue = card.type + card.queue = CardQueue(card.type) # Answering a review card ########################################################################## From be630adab9d3accb81a94950a0ba7e27b2fef078 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Tue, 23 Mar 2021 13:15:16 +0100 Subject: [PATCH 11/16] NF: CardType type --- pylib/anki/cards.py | 3 ++- pylib/anki/consts.py | 9 +++++---- pylib/anki/decks.py | 1 - pylib/anki/scheduler/v2.py | 3 ++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 3ccfa49a0..8bd3464fc 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -40,6 +40,7 @@ class Card: did: anki.decks.DeckID odid: anki.decks.DeckID queue: CardQueue + type: CardType def __init__( self, col: anki.collection.Collection, id: Optional[CardID] = None @@ -69,7 +70,7 @@ class Card: self.ord = c.template_idx self.mod = c.mtime_secs self.usn = c.usn - self.type = c.ctype + self.type = CardType(c.ctype) self.queue = CardQueue(c.queue) self.due = c.due self.ivl = c.interval diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index 92818889f..18cb2aa59 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -29,10 +29,11 @@ QUEUE_TYPE_DAY_LEARN_RELEARN = CardQueue(3) QUEUE_TYPE_PREVIEW = CardQueue(4) # Card types -CARD_TYPE_NEW = 0 -CARD_TYPE_LRN = 1 -CARD_TYPE_REV = 2 -CARD_TYPE_RELEARNING = 3 +CardType = NewType("CardType", int) +CARD_TYPE_NEW = CardType(0) +CARD_TYPE_LRN = CardType(1) +CARD_TYPE_REV = CardType(2) +CARD_TYPE_RELEARNING = CardType(3) # removal types REM_CARD = 0 diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 261cb1da3..1d877ad54 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -37,7 +37,6 @@ default_deck_id = DeckID(1) default_deck_conf_id = DeckConfID(1) - class DecksDictProxy: def __init__(self, col: anki.collection.Collection): self._col = col.weakref() diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index ae24aa1f9..2b644b5da 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -722,7 +722,8 @@ limit ?""" card.ivl = self._graduatingIvl(card, conf, early) card.due = self.today + card.ivl card.factor = conf["initialFactor"] - card.type = card.queue = QUEUE_TYPE_REV + card.type = CARD_TYPE_REV + card.queue = QUEUE_TYPE_REV def _logLrn( self, From a16940a246a8aaa6f4153510c4a18faa177a1c8c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Mar 2021 11:27:22 +1000 Subject: [PATCH 12/16] fix broken string --- qt/aqt/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index ca67ac452..944f7f2d8 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -518,7 +518,7 @@ class AnkiQt(QMainWindow): except Exception as e: if "FileTooNew" in str(e): showWarning( - "This profile requires a newer version of Anki to open. DeckID you forget to use the Downgrade button prior to switching Anki versions?" + "This profile requires a newer version of Anki to open. Did you forget to use the Downgrade button prior to switching Anki versions?" ) else: showWarning( From 8e0f00fbb946451445dd04bb12f2b378d2e4d341 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Mar 2021 11:27:49 +1000 Subject: [PATCH 13/16] fix .select() not handling string arguments --- pylib/anki/decks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 1d877ad54..0f50d310b 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -448,7 +448,8 @@ class DeckManager: def select(self, did: DeckID) -> None: "Select a new branch." - # make sure arg is an int + # make sure arg is an int; legacy callers may be passing in a string + did = DeckID(did) current = self.selected() active = self.deck_and_child_ids(did) if current != did or active != self.active(): From 7d5014fd5fb79e749b7518bbf9f24d0e46a6264e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Mar 2021 11:28:18 +1000 Subject: [PATCH 14/16] fix typo and PEP8 naming in noteimp.py --- pylib/anki/importing/noteimp.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index 35bb4fa71..8ba0c05cd 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -21,10 +21,10 @@ from anki.utils import ( timestampID, ) -type_tagsMapped = Tuple[int, int, str, str, NoteID, str, str] -type_tagsModified = Tuple[int, int, str, str, NoteID, str] -type_tagsElse = Tuple[int, int, str, NoteID, str] -type_udpates = Union[type_tagsMapped, type_tagsModified, type_tagsElse] +TagMappedUpdate = Tuple[int, int, str, str, NoteID, str, str] +TagModifiedUpdate = Tuple[int, int, str, str, NoteID, str] +NoTagUpdate = Tuple[int, int, str, NoteID, str] +Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate] # Stores a list of fields, tags and deck ###################################################################### @@ -142,7 +142,7 @@ class NoteImporter(Importer): self._fmap = self.col.models.fieldMap(self.model) self._nextID = NoteID(timestampID(self.col.db, "notes")) # loop through the notes - updates: List[type_udpates] = [] + updates: List[Updates] = [] updateLog = [] new = [] self._ids: List[NoteID] = [] @@ -278,7 +278,7 @@ class NoteImporter(Importer): def updateData( self, n: ForeignNote, id: NoteID, sflds: List[str] - ) -> Optional[type_udpates]: + ) -> Optional[Updates]: self._ids.append(id) self.processFields(n, sflds) if self._tagsMapped: @@ -292,7 +292,7 @@ class NoteImporter(Importer): else: return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr) - def addUpdates(self, rows: List[type_udpates]) -> None: + def addUpdates(self, rows: List[Updates]) -> None: changes = self.col.db.scalar("select total_changes()") if self._tagsMapped: self.col.db.executemany( From 64bb52600840f21f2fbcf979b892ec3ddec0f406 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Mar 2021 11:28:51 +1000 Subject: [PATCH 15/16] fix incorrect camelCase --- pylib/anki/cards.py | 10 +++++----- pylib/anki/collection.py | 2 +- pylib/anki/template.py | 2 +- qt/aqt/reviewer.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 8bd3464fc..dff8fe32a 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -167,24 +167,24 @@ class Card: def startTimer(self) -> None: self.timerStarted = time.time() - def currentDeckID(self) -> anki.decks.DeckID: + def current_deck_id(self) -> anki.decks.DeckID: return anki.decks.DeckID(self.odid or self.did) def timeLimit(self) -> int: "Time limit for answering in milliseconds." - conf = self.col.decks.confForDid(self.currentDeckID()) + conf = self.col.decks.confForDid(self.current_deck_id()) return conf["maxTaken"] * 1000 def shouldShowTimer(self) -> bool: - conf = self.col.decks.confForDid(self.currentDeckID()) + conf = self.col.decks.confForDid(self.current_deck_id()) return conf["timer"] def replay_question_audio_on_answer_side(self) -> bool: - conf = self.col.decks.confForDid(self.currentDeckID()) + conf = self.col.decks.confForDid(self.current_deck_id()) return conf.get("replayq", True) def autoplay(self) -> bool: - return self.col.decks.confForDid(self.currentDeckID())["autoplay"] + return self.col.decks.confForDid(self.current_deck_id())["autoplay"] def timeTaken(self) -> int: "Time taken to answer card, in integer MS." diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index b0798c27b..d04b2a499 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -398,7 +398,7 @@ class Collection: or current notetype. """ if card := current_review_card: - home_deck = card.currentDeckID() + home_deck = card.current_deck_id() else: home_deck = DeckID(0) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index ed3ce506d..f6a14d08a 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -172,7 +172,7 @@ class TemplateRenderContext: # add (most) special fields fields["Tags"] = self._note.stringTags().strip() fields["Type"] = self._note_type["name"] - fields["Deck"] = self._col.decks.name(self._card.currentDeckID()) + fields["Deck"] = self._col.decks.name(self._card.current_deck_id()) fields["Subdeck"] = DeckManager.basename(fields["Deck"]) if self._template: fields["Card"] = self._template["name"] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a93336c40..9fa4fb79d 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -834,7 +834,7 @@ time = %(time)d; qconnect(a.triggered, func) def onOptions(self) -> None: - self.mw.onDeckConf(self.mw.col.decks.get(self.card.currentDeckID())) + self.mw.onDeckConf(self.mw.col.decks.get(self.card.current_deck_id())) def set_flag_on_current_card(self, desired_flag: int) -> None: # need to toggle off? From bc2c3a57bae4f814db2ded66862a2a2c6184f1b4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Mar 2021 11:29:07 +1000 Subject: [PATCH 16/16] fix incorrect constant naming --- pylib/anki/decks.py | 12 ++++++------ pylib/anki/notes.py | 2 +- qt/aqt/deckchooser.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 0f50d310b..af402b778 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -33,8 +33,8 @@ DeckConfigDict = Dict[str, Any] DeckID = NewType("DeckID", int) DeckConfID = NewType("DeckConfID", int) -default_deck_id = DeckID(1) -default_deck_conf_id = DeckConfID(1) +DEFAULT_DECK_ID = DeckID(1) +DEFAULT_DECK_CONF_ID = DeckConfID(1) class DecksDictProxy: @@ -252,7 +252,7 @@ class DeckManager: def get(self, did: Union[DeckID, str], default: bool = True) -> Optional[DeckDict]: if not did: if default: - return self.get_legacy(default_deck_id) + return self.get_legacy(DEFAULT_DECK_ID) else: return None id = DeckID(int(did)) @@ -260,7 +260,7 @@ class DeckManager: if deck: return deck elif default: - return self.get_legacy(default_deck_id) + return self.get_legacy(DEFAULT_DECK_ID) else: return None @@ -324,7 +324,7 @@ class DeckManager: conf = self.get_config(dcid) if not conf: # fall back on default - conf = self.get_config(default_deck_conf_id) + conf = self.get_config(DEFAULT_DECK_CONF_ID) conf["dyn"] = False return conf # dynamic decks have embedded conf @@ -586,7 +586,7 @@ class DeckManager: def new_filtered(self, name: str) -> DeckID: "Return a new dynamic deck and set it as the current deck." - did = self.id(name, type=default_deck_conf_id) + did = self.id(name, type=DEFAULT_DECK_CONF_ID) self.select(did) return did diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 00de732f8..060458909 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -98,7 +98,7 @@ class Note: ) -> anki.cards.Card: card = anki.cards.Card(self.col) card.ord = ord - card.did = anki.decks.default_deck_id + card.did = anki.decks.DEFAULT_DECK_ID model = custom_note_type or self.model() template = copy.copy( diff --git a/qt/aqt/deckchooser.py b/qt/aqt/deckchooser.py index 81ddc5ba8..7837f38e5 100644 --- a/qt/aqt/deckchooser.py +++ b/qt/aqt/deckchooser.py @@ -3,7 +3,7 @@ from typing import Optional -from anki.decks import DeckID, default_deck_id +from anki.decks import DEFAULT_DECK_ID, DeckID from aqt import AnkiQt from aqt.qt import * from aqt.utils import TR, HelpPage, shortcut, tr @@ -71,7 +71,7 @@ class DeckChooser(QHBoxLayout): def _ensure_selected_deck_valid(self) -> None: if not self.mw.col.decks.get(self._selected_deck_id, default=False): - self.selected_deck_id = default_deck_id + self.selected_deck_id = DEFAULT_DECK_ID def _update_button_label(self) -> None: self.deck.setText(self.selected_deck_name().replace("&", "&&"))