Merge branch 'int_type' into main

This commit is contained in:
Damien Elmes 2021-03-26 11:38:34 +10:00
commit 07c6c4044c
35 changed files with 389 additions and 290 deletions

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import pprint import pprint
import time import time
from typing import List, Optional from typing import List, NewType, Optional
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
@ -26,15 +26,24 @@ from anki.sound import AVTag
# - rev queue: integer day # - rev queue: integer day
# - lrn queue: integer timestamp # - lrn queue: integer timestamp
# types
CardID = NewType("CardID", int)
class Card: class Card:
_note: Optional[Note] _note: Optional[Note]
timerStarted: Optional[float] timerStarted: Optional[float]
lastIvl: int lastIvl: int
ord: int ord: int
nid: anki.notes.NoteID
id: CardID
did: anki.decks.DeckID
odid: anki.decks.DeckID
queue: CardQueue
type: CardType
def __init__( def __init__(
self, col: anki.collection.Collection, id: Optional[int] = None self, col: anki.collection.Collection, id: Optional[CardID] = None
) -> None: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.timerStarted = None self.timerStarted = None
@ -55,14 +64,14 @@ class Card:
def _load_from_backend_card(self, c: _pb.Card) -> None: def _load_from_backend_card(self, c: _pb.Card) -> None:
self._render_output = None self._render_output = None
self._note = None self._note = None
self.id = c.id self.id = CardID(c.id)
self.nid = c.note_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.ord = c.template_idx
self.mod = c.mtime_secs self.mod = c.mtime_secs
self.usn = c.usn self.usn = c.usn
self.type = c.ctype self.type = CardType(c.ctype)
self.queue = c.queue self.queue = CardQueue(c.queue)
self.due = c.due self.due = c.due
self.ivl = c.interval self.ivl = c.interval
self.factor = c.ease_factor self.factor = c.ease_factor
@ -70,7 +79,7 @@ class Card:
self.lapses = c.lapses self.lapses = c.lapses
self.left = c.remaining_steps self.left = c.remaining_steps
self.odue = c.original_due 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.flags = c.flags
self.data = c.data self.data = c.data
@ -158,21 +167,24 @@ class Card:
def startTimer(self) -> None: def startTimer(self) -> None:
self.timerStarted = time.time() self.timerStarted = time.time()
def current_deck_id(self) -> anki.decks.DeckID:
return anki.decks.DeckID(self.odid or self.did)
def timeLimit(self) -> int: def timeLimit(self) -> int:
"Time limit for answering in milliseconds." "Time limit for answering in milliseconds."
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.current_deck_id())
return conf["maxTaken"] * 1000 return conf["maxTaken"] * 1000
def shouldShowTimer(self) -> bool: def shouldShowTimer(self) -> bool:
conf = self.col.decks.confForDid(self.odid or self.did) conf = self.col.decks.confForDid(self.current_deck_id())
return conf["timer"] return conf["timer"]
def replay_question_audio_on_answer_side(self) -> bool: 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.current_deck_id())
return conf.get("replayq", True) return conf.get("replayq", True)
def autoplay(self) -> bool: def autoplay(self) -> bool:
return self.col.decks.confForDid(self.odid or self.did)["autoplay"] return self.col.decks.confForDid(self.current_deck_id())["autoplay"]
def timeTaken(self) -> int: def timeTaken(self) -> int:
"Time taken to answer card, in integer MS." "Time taken to answer card, in integer MS."

View file

@ -34,16 +34,16 @@ from dataclasses import dataclass, field
import anki.latex import anki.latex
from anki import hooks from anki import hooks
from anki._backend import RustBackend, Translations from anki._backend import RustBackend, Translations
from anki.cards import Card from anki.cards import Card, CardID
from anki.config import Config, ConfigManager from anki.config import Config, ConfigManager
from anki.consts import * from anki.consts import *
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
from anki.decks import DeckManager from anki.decks import DeckID, DeckManager
from anki.errors import AnkiError, DBError from anki.errors import AnkiError, DBError
from anki.lang import TR, FormatTimeSpan from anki.lang import TR, FormatTimeSpan
from anki.media import MediaManager, media_paths_from_col_path 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 from anki.notes import Note, NoteID
from anki.scheduler.v1 import Scheduler as V1Scheduler from anki.scheduler.v1 import Scheduler as V1Scheduler
from anki.scheduler.v2 import Scheduler as V2Scheduler from anki.scheduler.v2 import Scheduler as V2Scheduler
from anki.scheduler.v3 import Scheduler as V3TestScheduler from anki.scheduler.v3 import Scheduler as V3TestScheduler
@ -317,7 +317,7 @@ class Collection:
# Object creation helpers # Object creation helpers
########################################################################## ##########################################################################
def get_card(self, id: int) -> Card: def get_card(self, id: CardID) -> Card:
return Card(self, id) return Card(self, id)
def update_card(self, card: Card) -> None: def update_card(self, card: Card) -> None:
@ -325,7 +325,7 @@ class Collection:
Unlike card.flush(), this will invalidate any current checkpoint.""" Unlike card.flush(), this will invalidate any current checkpoint."""
self._backend.update_card(card=card._to_backend_card(), skip_undo_entry=False) 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) return Note(self, id=id)
def update_note(self, note: Note) -> OpChanges: def update_note(self, note: Note) -> OpChanges:
@ -356,7 +356,7 @@ class Collection:
# Deletion logging # 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( self.db.executemany(
"insert into graves values (%d, ?, %d)" % (self.usn(), type), "insert into graves values (%d, ?, %d)" % (self.usn(), type),
([x] for x in ids), ([x] for x in ids),
@ -368,16 +368,16 @@ class Collection:
def new_note(self, notetype: NoteType) -> Note: def new_note(self, notetype: NoteType) -> Note:
return Note(self, notetype) 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) 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 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) hooks.notes_will_be_deleted(self, note_ids)
return self._backend.remove_notes(note_ids=note_ids, card_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(): if hooks.notes_will_be_deleted.count():
nids = self.db.list( nids = self.db.list(
f"select nid from cards where id in {ids2str(card_ids)}" f"select nid from cards where id in {ids2str(card_ids)}"
@ -385,8 +385,8 @@ class Collection:
hooks.notes_will_be_deleted(self, nids) hooks.notes_will_be_deleted(self, nids)
self._backend.remove_notes(note_ids=[], card_ids=card_ids) 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[CardID]:
return self._backend.cards_of_note(note_id) return [CardID(id) for id in self._backend.cards_of_note(note_id)]
def defaults_for_adding( def defaults_for_adding(
self, *, current_review_card: Optional[Card] self, *, current_review_card: Optional[Card]
@ -396,23 +396,25 @@ class Collection:
or current notetype. or current notetype.
""" """
if card := current_review_card: if card := current_review_card:
home_deck = card.odid or card.did home_deck = card.current_deck_id()
else: else:
home_deck = 0 home_deck = DeckID(0)
return self._backend.defaults_for_adding( return self._backend.defaults_for_adding(
home_deck_of_current_review_card=home_deck, 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: NoteTypeID) -> Optional[DeckID]:
"""If 'change deck depending on notetype' is enabled in the preferences, """If 'change deck depending on notetype' is enabled in the preferences,
return the last deck used with the provided notetype, if any..""" return the last deck used with the provided notetype, if any.."""
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
return None return None
return ( return (
self._backend.default_deck_for_notetype( DeckID(
ntid=notetype_id, self._backend.default_deck_for_notetype(
ntid=notetype_id,
)
) )
or None or None
) )
@ -430,10 +432,10 @@ class Collection:
self.add_note(note, note.model()["did"]) self.add_note(note, note.model()["did"])
return len(note.cards()) return len(note.cards())
def remNotes(self, ids: Sequence[int]) -> None: def remNotes(self, ids: Sequence[NoteID]) -> None:
self.remove_notes(ids) self.remove_notes(ids)
def _remNotes(self, ids: List[int]) -> None: def _remNotes(self, ids: List[NoteID]) -> None:
pass pass
# Cards # Cards
@ -445,11 +447,11 @@ class Collection:
def cardCount(self) -> Any: def cardCount(self) -> Any:
return self.db.scalar("select count() from cards") 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." "You probably want .remove_notes_by_card() instead."
self._backend.remove_cards(card_ids=card_ids) 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) return self._backend.set_deck(card_ids=card_ids, deck_id=deck_id)
def get_empty_cards(self) -> EmptyCardsReport: def get_empty_cards(self) -> EmptyCardsReport:
@ -457,10 +459,10 @@ class Collection:
# legacy # 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) self.remove_cards_and_orphaned_notes(ids)
def emptyCids(self) -> List[int]: def emptyCids(self) -> List[CardID]:
print("emptyCids() will go away") print("emptyCids() will go away")
return [] return []
@ -468,7 +470,7 @@ class Collection:
########################################################################## ##########################################################################
def after_note_updates( 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: ) -> None:
self._backend.after_note_updates( self._backend.after_note_updates(
nids=nids, generate_cards=generate_cards, mark_notes_modified=mark_modified nids=nids, generate_cards=generate_cards, mark_notes_modified=mark_modified
@ -476,11 +478,11 @@ class Collection:
# legacy # 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) self.after_note_updates(nids, mark_modified=False, generate_cards=False)
# this also updates field cache # 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) self.after_note_updates(nids, mark_modified=False, generate_cards=True)
# previously returned empty cards, no longer does # previously returned empty cards, no longer does
return [] return []
@ -493,7 +495,7 @@ class Collection:
query: str, query: str,
order: Union[bool, str, BuiltinSort.Kind.V] = False, order: Union[bool, str, BuiltinSort.Kind.V] = False,
reverse: bool = False, reverse: bool = False,
) -> Sequence[int]: ) -> Sequence[CardID]:
"""Return card ids matching the provided search. """Return card ids matching the provided search.
To programmatically construct a search string, see .build_search_string(). To programmatically construct a search string, see .build_search_string().
@ -523,9 +525,11 @@ class Collection:
mode = _pb.SortOrder( mode = _pb.SortOrder(
builtin=_pb.SortOrder.Builtin(kind=order, reverse=reverse) 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[int]: def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[NoteID]:
"""Return note ids matching the provided search or searches. """Return note ids matching the provided search or searches.
If more than one search is provided, they will be ANDed together. If more than one search is provided, they will be ANDed together.
@ -536,12 +540,15 @@ class Collection:
Eg: col.find_notes(SearchNode(deck="test"), "foo") will return notes Eg: col.find_notes(SearchNode(deck="test"), "foo") will return notes
that have a card in deck called "test", and have the text "foo". 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( def find_and_replace(
self, self,
*, *,
note_ids: Sequence[int], note_ids: Sequence[NoteID],
search: str, search: str,
replacement: str, replacement: str,
regex: bool = False, regex: bool = False,
@ -569,7 +576,7 @@ class Collection:
dupes = [] dupes = []
fields: Dict[int, int] = {} fields: Dict[int, int] = {}
def ordForMid(mid: int) -> int: def ordForMid(mid: NoteTypeID) -> int:
if mid not in fields: if mid not in fields:
model = self.models.get(mid) model = self.models.get(mid)
for c, f in enumerate(model["flds"]): for c, f in enumerate(model["flds"]):
@ -735,7 +742,7 @@ class Collection:
return CollectionStats(self) 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 import anki.stats as st
if include_revlog: if include_revlog:
@ -1028,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) return self._backend.set_flag(card_ids=cids, flag=flag)
def set_wants_abort(self) -> None: def set_wants_abort(self) -> None:

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any, Dict, NewType, Optional
import anki import anki
from anki.lang import TR from anki.lang import TR
@ -18,20 +18,22 @@ NEW_CARDS_RANDOM = 0
NEW_CARDS_DUE = 1 NEW_CARDS_DUE = 1
# Queue types # Queue types
QUEUE_TYPE_MANUALLY_BURIED = -3 CardQueue = NewType("CardQueue", int)
QUEUE_TYPE_SIBLING_BURIED = -2 QUEUE_TYPE_MANUALLY_BURIED = CardQueue(-3)
QUEUE_TYPE_SUSPENDED = -1 QUEUE_TYPE_SIBLING_BURIED = CardQueue(-2)
QUEUE_TYPE_NEW = 0 QUEUE_TYPE_SUSPENDED = CardQueue(-1)
QUEUE_TYPE_LRN = 1 QUEUE_TYPE_NEW = CardQueue(0)
QUEUE_TYPE_REV = 2 QUEUE_TYPE_LRN = CardQueue(1)
QUEUE_TYPE_DAY_LEARN_RELEARN = 3 QUEUE_TYPE_REV = CardQueue(2)
QUEUE_TYPE_PREVIEW = 4 QUEUE_TYPE_DAY_LEARN_RELEARN = CardQueue(3)
QUEUE_TYPE_PREVIEW = CardQueue(4)
# Card types # Card types
CARD_TYPE_NEW = 0 CardType = NewType("CardType", int)
CARD_TYPE_LRN = 1 CARD_TYPE_NEW = CardType(0)
CARD_TYPE_REV = 2 CARD_TYPE_LRN = CardType(1)
CARD_TYPE_RELEARNING = 3 CARD_TYPE_REV = CardType(2)
CARD_TYPE_RELEARNING = CardType(3)
# removal types # removal types
REM_CARD = 0 REM_CARD = 0

View file

@ -11,6 +11,7 @@ from typing import Any, Dict, Iterable, List, NewType, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki.cards import CardID
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithID from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithID
from anki.consts import * from anki.consts import *
from anki.errors import NotFoundError from anki.errors import NotFoundError
@ -30,6 +31,10 @@ DeckDict = Dict[str, Any]
DeckConfigDict = Dict[str, Any] DeckConfigDict = Dict[str, Any]
DeckID = NewType("DeckID", int) DeckID = NewType("DeckID", int)
DeckConfID = NewType("DeckConfID", int)
DEFAULT_DECK_ID = DeckID(1)
DEFAULT_DECK_CONF_ID = DeckConfID(1)
class DecksDictProxy: class DecksDictProxy:
@ -42,7 +47,7 @@ class DecksDictProxy:
def __getitem__(self, item: Any) -> Any: def __getitem__(self, item: Any) -> Any:
self._warn() 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: def __setitem__(self, key: Any, val: Any) -> None:
self._warn() self._warn()
@ -120,8 +125,8 @@ class DeckManager:
self, self,
name: str, name: str,
create: bool = True, create: bool = True,
type: int = 0, type: DeckConfID = DeckConfID(0),
) -> Optional[int]: ) -> Optional[DeckID]:
"Add a deck with NAME. Reuse deck if already exists. Return id as int." "Add a deck with NAME. Reuse deck if already exists. Return id as int."
id = self.id_for_name(name) id = self.id_for_name(name)
if id: if id:
@ -132,17 +137,17 @@ class DeckManager:
deck = self.new_deck_legacy(bool(type)) deck = self.new_deck_legacy(bool(type))
deck["name"] = name deck["name"] = name
out = self.add_deck_legacy(deck) out = self.add_deck_legacy(deck)
return out.id return DeckID(out.id)
@legacy_func(sub="remove") @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." "Remove the deck. If cardsToo, delete any cards inside."
if isinstance(did, str): if isinstance(did, str):
did = int(did) did = int(did)
assert cardsToo and childrenToo assert cardsToo and childrenToo
self.remove([did]) self.remove([did])
def remove(self, dids: Sequence[int]) -> OpChangesWithCount: def remove(self, dids: Sequence[DeckID]) -> OpChangesWithCount:
return self.col._backend.remove_decks(dids) return self.col._backend.remove_decks(dids)
def all_names_and_ids( def all_names_and_ids(
@ -153,20 +158,20 @@ class DeckManager:
skip_empty_default=skip_empty_default, include_filtered=include_filtered 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: try:
return self.col._backend.get_deck_id_by_name(name) return DeckID(self.col._backend.get_deck_id_by_name(name))
except NotFoundError: except NotFoundError:
return None return None
def get_legacy(self, did: int) -> Optional[DeckDict]: def get_legacy(self, did: DeckID) -> Optional[DeckDict]:
try: try:
return from_json_bytes(self.col._backend.get_deck_legacy(did)) return from_json_bytes(self.col._backend.get_deck_legacy(did))
except NotFoundError: except NotFoundError:
return None return None
def have(self, id: int) -> bool: def have(self, id: DeckID) -> bool:
return not self.get_legacy(int(id)) return not self.get_legacy(id)
def get_all_legacy(self) -> List[DeckDict]: def get_all_legacy(self) -> List[DeckDict]:
return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values()) return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values())
@ -188,7 +193,7 @@ class DeckManager:
@classmethod @classmethod
def find_deck_in_tree( def find_deck_in_tree(
cls, node: DeckTreeNode, deck_id: int cls, node: DeckTreeNode, deck_id: DeckID
) -> Optional[DeckTreeNode]: ) -> Optional[DeckTreeNode]:
if node.deck_id == deck_id: if node.deck_id == deck_id:
return node return node
@ -215,12 +220,12 @@ class DeckManager:
) )
] ]
def collapse(self, did: int) -> None: def collapse(self, did: DeckID) -> None:
deck = self.get(did) deck = self.get(did)
deck["collapsed"] = not deck["collapsed"] deck["collapsed"] = not deck["collapsed"]
self.save(deck) self.save(deck)
def collapseBrowser(self, did: int) -> None: def collapseBrowser(self, did: DeckID) -> None:
deck = self.get(did) deck = self.get(did)
collapsed = deck.get("browserCollapsed", False) collapsed = deck.get("browserCollapsed", False)
deck["browserCollapsed"] = not collapsed deck["browserCollapsed"] = not collapsed
@ -230,7 +235,7 @@ class DeckManager:
return len(self.all_names_and_ids()) return len(self.all_names_and_ids())
def card_count( def card_count(
self, dids: Union[int, Iterable[int]], include_subdecks: bool self, dids: Union[DeckID, Iterable[DeckID]], include_subdecks: bool
) -> Any: ) -> Any:
if isinstance(dids, int): if isinstance(dids, int):
dids = {dids} dids = {dids}
@ -244,18 +249,18 @@ class DeckManager:
) )
return count 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 not did:
if default: if default:
return self.get_legacy(1) return self.get_legacy(DEFAULT_DECK_ID)
else: else:
return None return None
id = int(did) id = DeckID(int(did))
deck = self.get_legacy(id) deck = self.get_legacy(id)
if deck: if deck:
return deck return deck
elif default: elif default:
return self.get_legacy(1) return self.get_legacy(DEFAULT_DECK_ID)
else: else:
return None return None
@ -294,7 +299,9 @@ class DeckManager:
# legacy # legacy
def renameForDragAndDrop( def renameForDragAndDrop(
self, draggedDeckDid: Union[int, str], ontoDeckDid: Optional[Union[int, str]] self,
draggedDeckDid: Union[DeckID, str],
ontoDeckDid: Optional[Union[DeckID, str]],
) -> None: ) -> None:
if not ontoDeckDid: if not ontoDeckDid:
onto = 0 onto = 0
@ -309,21 +316,21 @@ class DeckManager:
"A list of all deck config." "A list of all deck config."
return list(from_json_bytes(self.col._backend.all_deck_config_legacy())) 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) deck = self.get(did, default=False)
assert deck assert deck
if "conf" in 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) conf = self.get_config(dcid)
if not conf: if not conf:
# fall back on default # fall back on default
conf = self.get_config(1) conf = self.get_config(DEFAULT_DECK_CONF_ID)
conf["dyn"] = False conf["dyn"] = False
return conf return conf
# dynamic decks have embedded conf # dynamic decks have embedded conf
return deck return deck
def get_config(self, conf_id: int) -> Optional[DeckConfigDict]: def get_config(self, conf_id: DeckConfID) -> Optional[DeckConfigDict]:
try: try:
return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id)) return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id))
except NotFoundError: except NotFoundError:
@ -348,10 +355,10 @@ class DeckManager:
def add_config_returning_id( def add_config_returning_id(
self, name: str, clone_from: Optional[DeckConfigDict] = None self, name: str, clone_from: Optional[DeckConfigDict] = None
) -> int: ) -> DeckConfID:
return self.add_config(name, clone_from)["id"] 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." "Remove a configuration and update all decks using it."
self.col.modSchema(check=True) self.col.modSchema(check=True)
for g in self.all(): for g in self.all():
@ -363,11 +370,11 @@ class DeckManager:
self.save(g) self.save(g)
self.col._backend.remove_deck_config(id) 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 grp["conf"] = id
self.save(grp) self.save(grp)
def didsForConf(self, conf: DeckConfigDict) -> List[int]: def didsForConf(self, conf: DeckConfigDict) -> List[DeckID]:
dids = [] dids = []
for deck in self.all(): for deck in self.all():
if "conf" in deck and deck["conf"] == conf["id"]: if "conf" in deck and deck["conf"] == conf["id"]:
@ -394,19 +401,19 @@ class DeckManager:
# Deck utils # 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) deck = self.get(did, default=default)
if deck: if deck:
return deck["name"] return deck["name"]
return self.col.tr(TR.DECKS_NO_DECK) 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) deck = self.get(did, default=False)
if deck: if deck:
return deck["name"] return deck["name"]
return None return None
def setDeck(self, cids: List[int], did: int) -> None: def setDeck(self, cids: List[CardID], did: DeckID) -> None:
self.col.db.execute( self.col.db.execute(
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
did, did,
@ -414,7 +421,7 @@ class DeckManager:
intTime(), intTime(),
) )
def cids(self, did: int, children: bool = False) -> List[int]: def cids(self, did: DeckID, children: bool = False) -> List[CardID]:
if not children: if not children:
return self.col.db.list("select id from cards where did=?", did) return self.col.db.list("select id from cards where did=?", did)
dids = [did] dids = [did]
@ -422,13 +429,13 @@ class DeckManager:
dids.append(id) dids.append(id)
return self.col.db.list(f"select id from cards where did in {ids2str(dids)}") 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[DeckID]:
return self.col.db.list(f"select did from cards where id in {ids2str(cids)}") return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
# Deck selection # Deck selection
############################################################# #############################################################
def active(self) -> List[int]: def active(self) -> List[DeckID]:
"The currrently active dids." "The currrently active dids."
return self.col.get_config("activeDecks", [1]) return self.col.get_config("activeDecks", [1])
@ -439,10 +446,10 @@ class DeckManager:
def current(self) -> DeckDict: def current(self) -> DeckDict:
return self.get(self.selected()) return self.get(self.selected())
def select(self, did: int) -> None: def select(self, did: DeckID) -> None:
"Select a new branch." "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 = int(did) did = DeckID(did)
current = self.selected() current = self.selected()
active = self.deck_and_child_ids(did) active = self.deck_and_child_ids(did)
if current != did or active != self.active(): if current != did or active != self.active():
@ -483,29 +490,31 @@ class DeckManager:
def key(cls, deck: DeckDict) -> List[str]: def key(cls, deck: DeckDict) -> List[str]:
return cls.path(deck["name"]) 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)." "All children of did, as (name, id)."
name = self.get(did)["name"] name = self.get(did)["name"]
actv = [] actv = []
for g in self.all_names_and_ids(): for g in self.all_names_and_ids():
if g.name.startswith(f"{name}::"): if g.name.startswith(f"{name}::"):
actv.append((g.name, g.id)) actv.append((g.name, DeckID(g.id)))
return actv return actv
def child_ids(self, parent_name: str) -> Iterable[int]: def child_ids(self, parent_name: str) -> Iterable[DeckID]:
prefix = f"{parent_name}::" 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"] parent_name = self.get_legacy(deck_id)["name"]
out = [deck_id] out = [deck_id]
out.extend(self.child_ids(parent_name)) out.extend(self.child_ids(parent_name))
return out return out
childMapNode = Dict[int, Any] childMapNode = Dict[DeckID, Any]
# Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type # 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: def gather(node: DeckManager.childMapNode, arr: List) -> None:
for did, child in node.items(): for did, child in node.items():
arr.append(did) arr.append(did)
@ -533,7 +542,7 @@ class DeckManager:
return childMap return childMap
def parents( def parents(
self, did: int, nameMap: Optional[Dict[str, DeckDict]] = None self, did: DeckID, nameMap: Optional[Dict[str, DeckDict]] = None
) -> List[DeckDict]: ) -> List[DeckDict]:
"All parents of did." "All parents of did."
# get parent and grandparent names # get parent and grandparent names
@ -575,14 +584,14 @@ class DeckManager:
# Dynamic decks # 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." "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) self.select(did)
return did return did
# 1 for dyn, 0 for standard # 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"] return self.get(did)["dyn"]
# legacy # legacy

View file

@ -12,7 +12,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
from zipfile import ZipFile from zipfile import ZipFile
from anki import hooks from anki import hooks
from anki.cards import CardID
from anki.collection import Collection from anki.collection import Collection
from anki.decks import DeckID
from anki.lang import TR from anki.lang import TR
from anki.utils import ids2str, namedtmp, splitFields, stripHTML from anki.utils import ids2str, namedtmp, splitFields, stripHTML
@ -27,8 +29,8 @@ class Exporter:
def __init__( def __init__(
self, self,
col: Collection, col: Collection,
did: Optional[int] = None, did: Optional[DeckID] = None,
cids: Optional[List[int]] = None, cids: Optional[List[CardID]] = None,
) -> None: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.did = did self.did = did
@ -184,7 +186,7 @@ class AnkiExporter(Exporter):
def key(col: Collection) -> str: def key(col: Collection) -> str:
return col.tr(TR.EXPORTING_ANKI_20_DECK) return col.tr(TR.EXPORTING_ANKI_20_DECK)
def deckIds(self) -> List[int]: def deckIds(self) -> List[DeckID]:
if self.cids: if self.cids:
return self.col.decks.for_card_ids(self.cids) return self.col.decks.for_card_ids(self.cids)
elif self.did: elif self.did:

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Set from typing import TYPE_CHECKING, Optional, Set
from anki.hooks import * from anki.hooks import *
from anki.notes import NoteID
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.collection import Collection from anki.collection import Collection
@ -29,7 +30,7 @@ class Finder:
def findReplace( def findReplace(
col: Collection, col: Collection,
nids: List[int], nids: List[NoteID],
src: str, src: str,
dst: str, dst: str,
regex: bool = False, regex: bool = False,
@ -48,7 +49,7 @@ def findReplace(
).count ).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)) return list(col.field_names_for_note_ids(nids))

View file

@ -5,11 +5,14 @@ import os
import unicodedata import unicodedata
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from anki.cards import CardID
from anki.collection import Collection from anki.collection import Collection
from anki.consts import * from anki.consts import *
from anki.decks import DeckManager from anki.decks import DeckID, DeckManager
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.lang import TR from anki.lang import TR
from anki.models import NoteTypeID
from anki.notes import NoteID
from anki.utils import intTime, joinFields, splitFields, stripHTMLMedia from anki.utils import intTime, joinFields, splitFields, stripHTMLMedia
GUID = 1 GUID = 1
@ -29,7 +32,7 @@ class Anki2Importer(Importer):
super().__init__(col, file) super().__init__(col, file)
# set later, defined here for typechecking # set later, defined here for typechecking
self._decks: Dict[int, int] = {} self._decks: Dict[DeckID, DeckID] = {}
self.source_needs_upgrade = False self.source_needs_upgrade = False
def run(self, media: None = None) -> None: def run(self, media: None = None) -> None:
@ -79,7 +82,7 @@ class Anki2Importer(Importer):
def _importNotes(self) -> None: def _importNotes(self) -> None:
# build guid -> (id,mod,mid) hash & map of existing note ids # 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, NoteTypeID]] = {}
existing = {} existing = {}
for id, guid, mod, mid in self.dst.db.execute( for id, guid, mod, mid in self.dst.db.execute(
"select id, guid, mod, mid from notes" "select id, guid, mod, mid from notes"
@ -215,9 +218,9 @@ class Anki2Importer(Importer):
def _prepareModels(self) -> None: def _prepareModels(self) -> None:
"Prepare index of schema hashes." "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." "Return local id for remote MID."
# already processed this mid? # already processed this mid?
if srcMid in self._modelMap: if srcMid in self._modelMap:
@ -246,7 +249,7 @@ class Anki2Importer(Importer):
self.dst.models.update(model) self.dst.models.update(model)
break break
# as they don't match, try next id # as they don't match, try next id
mid += 1 mid = NoteTypeID(mid + 1)
# save map and return new mid # save map and return new mid
self._modelMap[srcMid] = mid self._modelMap[srcMid] = mid
return mid return mid
@ -254,7 +257,7 @@ class Anki2Importer(Importer):
# Decks # Decks
###################################################################### ######################################################################
def _did(self, did: int) -> Any: def _did(self, did: DeckID) -> Any:
"Given did in src col, return local id." "Given did in src col, return local id."
# already converted? # already converted?
if did in self._decks: if did in self._decks:
@ -305,7 +308,7 @@ class Anki2Importer(Importer):
if self.source_needs_upgrade: if self.source_needs_upgrade:
self.src.upgrade_to_v2_scheduler() self.src.upgrade_to_v2_scheduler()
# build map of (guid, ord) -> cid and used id cache # 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 = {} existing = {}
for guid, ord, cid in self.dst.db.execute( 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" "select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id"
@ -430,7 +433,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
# the user likely used subdirectories # the user likely used subdirectories
pass pass
def _mungeMedia(self, mid: int, fieldsStr: str) -> str: def _mungeMedia(self, mid: NoteTypeID, fieldsStr: str) -> str:
fields = splitFields(fieldsStr) fields = splitFields(fieldsStr)
def repl(match): def repl(match):

View file

@ -10,6 +10,8 @@ from anki.config import Config
from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.lang import TR from anki.lang import TR
from anki.models import NoteTypeID
from anki.notes import NoteID
from anki.utils import ( from anki.utils import (
fieldChecksum, fieldChecksum,
guid64, guid64,
@ -19,6 +21,11 @@ from anki.utils import (
timestampID, timestampID,
) )
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 # Stores a list of fields, tags and deck
###################################################################### ######################################################################
@ -122,7 +129,7 @@ class NoteImporter(Importer):
if f == "_tags": if f == "_tags":
self._tagsMapped = True self._tagsMapped = True
# gather checks for duplicate comparison # gather checks for duplicate comparison
csums: Dict[str, List[int]] = {} csums: Dict[str, List[NoteID]] = {}
for csum, id in self.col.db.execute( for csum, id in self.col.db.execute(
"select csum, id from notes where mid = ?", self.model["id"] "select csum, id from notes where mid = ?", self.model["id"]
): ):
@ -133,12 +140,12 @@ class NoteImporter(Importer):
firsts: Dict[str, bool] = {} firsts: Dict[str, bool] = {}
fld0idx = self.mapping.index(self.model["flds"][0]["name"]) fld0idx = self.mapping.index(self.model["flds"][0]["name"])
self._fmap = self.col.models.fieldMap(self.model) 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 # loop through the notes
updates = [] updates: List[Updates] = []
updateLog = [] updateLog = []
new = [] new = []
self._ids: List[int] = [] self._ids: List[NoteID] = []
self._cards: List[Tuple] = [] self._cards: List[Tuple] = []
dupeCount = 0 dupeCount = 0
dupes: List[str] = [] dupes: List[str] = []
@ -203,9 +210,9 @@ class NoteImporter(Importer):
found = False found = False
# newly add # newly add
if not found: if not found:
data = self.newData(n) new_data = self.newData(n)
if data: if new_data:
new.append(data) new.append(new_data)
# note that we've seen this note once already # note that we've seen this note once already
firsts[fld0] = True firsts[fld0] = True
self.addNew(new) self.addNew(new)
@ -235,15 +242,17 @@ class NoteImporter(Importer):
self.log.extend(updateLog) self.log.extend(updateLog)
self.total = len(self._ids) self.total = len(self._ids)
def newData(self, n: ForeignNote) -> Optional[list]: def newData(
self, n: ForeignNote
) -> Tuple[NoteID, str, NoteTypeID, int, int, str, str, str, int, int, str]:
id = self._nextID id = self._nextID
self._nextID += 1 self._nextID = NoteID(self._nextID + 1)
self._ids.append(id) self._ids.append(id)
self.processFields(n) self.processFields(n)
# note id for card updates later # note id for card updates later
for ord, c in list(n.cards.items()): for ord, c in list(n.cards.items()):
self._cards.append((id, ord, c)) self._cards.append((id, ord, c))
return [ return (
id, id,
guid64(), guid64(),
self.model["id"], self.model["id"],
@ -255,28 +264,35 @@ class NoteImporter(Importer):
0, 0,
0, 0,
"", "",
] )
def addNew(self, rows: List[List[Union[int, str]]]) -> None: def addNew(
self,
rows: List[
Tuple[NoteID, str, NoteTypeID, int, int, str, str, str, int, int, str]
],
) -> None:
self.col.db.executemany( self.col.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", rows "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: NoteID, sflds: List[str]
) -> Optional[Updates]:
self._ids.append(id) self._ids.append(id)
self.processFields(n, sflds) self.processFields(n, sflds)
if self._tagsMapped: if self._tagsMapped:
tags = self.col.tags.join(n.tags) 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: elif self.tagModified:
tags = self.col.db.scalar("select tags from notes where id = ?", id) tags = self.col.db.scalar("select tags from notes where id = ?", id)
tagList = self.col.tags.split(tags) + self.tagModified.split() tagList = self.col.tags.split(tags) + self.tagModified.split()
tags = self.col.tags.join(tagList) 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: 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[Updates]) -> None:
changes = self.col.db.scalar("select total_changes()") changes = self.col.db.scalar("select total_changes()")
if self._tagsMapped: if self._tagsMapped:
self.col.db.executemany( self.col.db.executemany(

View file

@ -17,6 +17,7 @@ import anki
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki.consts import * from anki.consts import *
from anki.latex import render_latex, render_latex_returning_errors from anki.latex import render_latex, render_latex_returning_errors
from anki.models import NoteTypeID
from anki.sound import SoundOrVideoTag from anki.sound import SoundOrVideoTag
from anki.template import av_tags_to_native from anki.template import av_tags_to_native
from anki.utils import intTime from anki.utils import intTime
@ -159,7 +160,7 @@ class MediaManager:
########################################################################## ##########################################################################
def filesInStr( def filesInStr(
self, mid: int, string: str, includeRemote: bool = False self, mid: NoteTypeID, string: str, includeRemote: bool = False
) -> List[str]: ) -> List[str]:
l = [] l = []
model = self.col.models.get(mid) model = self.col.models.get(mid)

View file

@ -8,7 +8,7 @@ import pprint
import sys import sys
import time import time
import traceback 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 # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
@ -35,6 +35,7 @@ NoteTypeNameIDUseCount = _pb.NoteTypeNameIDUseCount
NoteType = Dict[str, Any] NoteType = Dict[str, Any]
Field = Dict[str, Any] Field = Dict[str, Any]
Template = Dict[str, Union[str, int, None]] Template = Dict[str, Union[str, int, None]]
NoteTypeID = NewType("NoteTypeID", int)
class ModelsDictProxy: class ModelsDictProxy:
@ -47,7 +48,7 @@ class ModelsDictProxy:
def __getitem__(self, item: Any) -> Any: def __getitem__(self, item: Any) -> Any:
self._warn() 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: def __setitem__(self, key: str, val: Any) -> None:
self._warn() self._warn()
@ -114,16 +115,16 @@ class ModelManager:
# need to cache responses from the backend. Please do not # need to cache responses from the backend. Please do not
# access the cache directly! # access the cache directly!
_cache: Dict[int, NoteType] = {} _cache: Dict[NoteTypeID, NoteType] = {}
def _update_cache(self, nt: NoteType) -> None: def _update_cache(self, nt: NoteType) -> None:
self._cache[nt["id"]] = nt 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: if ntid in self._cache:
del self._cache[ntid] 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) return self._cache.get(ntid)
def _clear_cache(self) -> None: def _clear_cache(self) -> None:
@ -143,11 +144,11 @@ class ModelManager:
def allNames(self) -> List[str]: def allNames(self) -> List[str]:
return [n.name for n in self.all_names_and_ids()] return [n.name for n in self.all_names_and_ids()]
def ids(self) -> List[int]: def ids(self) -> List[NoteTypeID]:
return [n.id for n in self.all_names_and_ids()] return [NoteTypeID(n.id) for n in self.all_names_and_ids()]
# only used by importing code # only used by importing code
def have(self, id: int) -> bool: def have(self, id: NoteTypeID) -> bool:
if isinstance(id, str): if isinstance(id, str):
id = int(id) id = int(id)
return any(True for e in self.all_names_and_ids() if e.id == 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"]) m = self.get(self.col.conf["curModel"])
if m: if m:
return 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: def setCurrent(self, m: NoteType) -> None:
self.col.conf["curModel"] = m["id"] self.col.conf["curModel"] = m["id"]
@ -170,13 +171,13 @@ class ModelManager:
# Retrieving and creating models # Retrieving and creating models
############################################################# #############################################################
def id_for_name(self, name: str) -> Optional[int]: def id_for_name(self, name: str) -> Optional[NoteTypeID]:
try: try:
return self.col._backend.get_notetype_id_by_name(name) return NoteTypeID(self.col._backend.get_notetype_id_by_name(name))
except NotFoundError: except NotFoundError:
return None return None
def get(self, id: int) -> Optional[NoteType]: def get(self, id: NoteTypeID) -> Optional[NoteType]:
"Get model with ID, or None." "Get model with ID, or None."
# deal with various legacy input types # deal with various legacy input types
if id is None: if id is None:
@ -195,7 +196,7 @@ class ModelManager:
def all(self) -> List[NoteType]: def all(self) -> List[NoteType]:
"Get all models." "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]: def byName(self, name: str) -> Optional[NoteType]:
"Get model with NAME." "Get model with NAME."
@ -222,10 +223,10 @@ class ModelManager:
def remove_all_notetypes(self) -> None: def remove_all_notetypes(self) -> None:
for nt in self.all_names_and_ids(): 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) self.col._backend.remove_notetype(nt.id)
def remove(self, id: int) -> None: def remove(self, id: NoteTypeID) -> None:
"Modifies schema." "Modifies schema."
self._remove_from_cache(id) self._remove_from_cache(id)
self.col._backend.remove_notetype(id) self.col._backend.remove_notetype(id)
@ -257,7 +258,7 @@ class ModelManager:
# Tools # Tools
################################################## ##################################################
def nids(self, ntid: int) -> List[int]: def nids(self, ntid: NoteTypeID) -> List[anki.notes.NoteID]:
"Note ids for M." "Note ids for M."
if isinstance(ntid, dict): if isinstance(ntid, dict):
# legacy callers passed in note type # legacy callers passed in note type
@ -403,7 +404,7 @@ class ModelManager:
self.reposition_template(m, template, idx) self.reposition_template(m, template, idx)
self.save(m) 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( return self.col.db.scalar(
""" """
select count() from cards, notes where cards.nid = notes.id select count() from cards, notes where cards.nid = notes.id
@ -420,7 +421,7 @@ and notes.mid = ? and cards.ord = ?""",
def change( def change(
self, self,
m: NoteType, m: NoteType,
nids: List[int], nids: List[anki.notes.NoteID],
newModel: NoteType, newModel: NoteType,
fmap: Optional[Dict[int, Union[None, int]]], fmap: Optional[Dict[int, Union[None, int]]],
cmap: Optional[Dict[int, Union[None, int]]], cmap: Optional[Dict[int, Union[None, int]]],
@ -434,7 +435,10 @@ and notes.mid = ? and cards.ord = ?""",
self.col.after_note_updates(nids, mark_modified=True) self.col.after_note_updates(nids, mark_modified=True)
def _changeNotes( 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: ) -> None:
d = [] d = []
nfields = len(newModel["flds"]) nfields = len(newModel["flds"])
@ -464,7 +468,7 @@ and notes.mid = ? and cards.ord = ?""",
def _changeCards( def _changeCards(
self, self,
nids: List[int], nids: List[anki.notes.NoteID],
oldModel: NoteType, oldModel: NoteType,
newModel: NoteType, newModel: NoteType,
map: Dict[int, Union[None, int]], map: Dict[int, Union[None, int]],

View file

@ -5,28 +5,33 @@ from __future__ import annotations
import copy import copy
import pprint 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 # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.consts import MODEL_STD from anki.consts import MODEL_STD
from anki.models import NoteType, Template from anki.models import NoteType, NoteTypeID, Template
from anki.utils import joinFields from anki.utils import joinFields
DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State
# types
NoteID = NewType("NoteID", int)
class Note: class Note:
# not currently exposed # not currently exposed
flags = 0 flags = 0
data = "" data = ""
id: NoteID
mid: NoteTypeID
def __init__( def __init__(
self, self,
col: anki.collection.Collection, col: anki.collection.Collection,
model: Optional[NoteType] = None, model: Optional[NoteType] = None,
id: Optional[int] = None, id: Optional[NoteID] = None,
) -> None: ) -> None:
assert not (model and id) assert not (model and id)
self.col = col.weakref() self.col = col.weakref()
@ -46,9 +51,9 @@ class Note:
self._load_from_backend_note(n) self._load_from_backend_note(n)
def _load_from_backend_note(self, n: _pb.Note) -> None: def _load_from_backend_note(self, n: _pb.Note) -> None:
self.id = n.id self.id = NoteID(n.id)
self.guid = n.guid self.guid = n.guid
self.mid = n.notetype_id self.mid = NoteTypeID(n.notetype_id)
self.mod = n.mtime_secs self.mod = n.mtime_secs
self.usn = n.usn self.usn = n.usn
self.tags = list(n.tags) self.tags = list(n.tags)
@ -93,7 +98,7 @@ class Note:
) -> anki.cards.Card: ) -> anki.cards.Card:
card = anki.cards.Card(self.col) card = anki.cards.Card(self.col)
card.ord = ord card.ord = ord
card.did = 1 card.did = anki.decks.DEFAULT_DECK_ID
model = custom_note_type or self.model() model = custom_note_type or self.model()
template = copy.copy( template = copy.copy(
@ -119,7 +124,7 @@ class Note:
def cards(self) -> List[anki.cards.Card]: def cards(self) -> List[anki.cards.Card]:
return [self.col.getCard(id) for id in self.card_ids()] 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) return self.col.card_ids_of_note(self.id)
def model(self) -> Optional[NoteType]: def model(self) -> Optional[NoteType]:

View file

@ -13,6 +13,7 @@ SchedTimingToday = _pb.SchedTimingTodayOut
from typing import List, Optional, Sequence 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.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
from anki.decks import DeckConfigDict, DeckID, DeckTreeNode from anki.decks import DeckConfigDict, DeckID, DeckTreeNode
from anki.notes import Note from anki.notes import Note
@ -90,10 +91,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Filtered deck handling # 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) 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) return self.col._backend.empty_filtered_deck(deck_id)
def get_or_create_filtered_deck(self, deck_id: DeckID) -> FilteredDeckForUpdate: def get_or_create_filtered_deck(self, deck_id: DeckID) -> FilteredDeckForUpdate:
@ -107,10 +108,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Suspending & burying # 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) 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) return self.col._backend.restore_buried_and_suspended_cards(ids)
def unbury_cards_in_current_deck( 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: ) -> None:
self.col._backend.unbury_cards_in_current_deck(mode) 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( return self.col._backend.bury_or_suspend_cards(
card_ids=ids, mode=BuryOrSuspend.SUSPEND 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: if manual:
mode = BuryOrSuspend.BURY_USER mode = BuryOrSuspend.BURY_USER
else: else:
@ -137,13 +138,13 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Resetting/rescheduling # 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." "Put cards at the end of the new queue."
return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True) return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
def set_due_date( def set_due_date(
self, self,
card_ids: List[int], card_ids: List[CardID],
days: str, days: str,
config_key: Optional[Config.String.Key.V] = None, config_key: Optional[Config.String.Key.V] = None,
) -> OpChanges: ) -> 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 config_key=key, # type: ignore
) )
def resetCards(self, ids: List[int]) -> None: def resetCards(self, ids: List[CardID]) -> None:
"Completely reset cards for export." "Completely reset cards for export."
sids = ids2str(ids) sids = ids2str(ids)
assert self.col.db 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( def reposition_new_cards(
self, self,
card_ids: Sequence[int], card_ids: Sequence[CardID],
starting_from: int, starting_from: int,
step_size: int, step_size: int,
randomize: bool, randomize: bool,
@ -198,10 +199,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
shift_existing=shift_existing, 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) 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) self.col._backend.sort_deck(deck_id=did, randomize=False)
def resortConf(self, conf: DeckConfigDict) -> None: def resortConf(self, conf: DeckConfigDict) -> None:
@ -212,7 +213,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self.orderCards(did) self.orderCards(did)
# for post-import # for post-import
def maybeRandomizeDeck(self, did: Optional[int] = None) -> None: def maybeRandomizeDeck(self, did: Optional[DeckID] = None) -> None:
if not did: if not did:
did = self.col.decks.selected() did = self.col.decks.selected()
conf = self.col.decks.confForDid(did) conf = self.col.decks.confForDid(did)
@ -223,7 +224,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# legacy # legacy
def sortCards( def sortCards(
self, self,
cids: List[int], cids: List[CardID],
start: int = 1, start: int = 1,
step: int = 1, step: int = 1,
shuffle: bool = False, shuffle: bool = False,

View file

@ -3,9 +3,10 @@
from typing import List, Optional, Tuple 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.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.scheduler.base import SchedulerBase, UnburyCurrentDeck
from anki.utils import from_json_bytes, ids2str from anki.utils import from_json_bytes, ids2str
@ -14,11 +15,11 @@ class SchedulerBaseWithLegacy(SchedulerBase):
"Legacy aliases and helpers. These will go away in the future." "Legacy aliases and helpers. These will go away in the future."
def reschedCards( 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: ) -> None:
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") 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) note = self.col.get_note(nid)
self.bury_cards(note.card_ids()) self.bury_cards(note.card_ids())
@ -48,7 +49,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
print("_nextDueMsg() is obsolete") print("_nextDueMsg() is obsolete")
return "" 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() did = did or self.col.decks.selected()
count = self.rebuild_filtered_deck(did).count or None count = self.rebuild_filtered_deck(did).count or None
if not count: if not count:
@ -57,7 +58,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
self.col.decks.select(did) self.col.decks.select(did)
return count 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: if lim is None:
self.empty_filtered_deck(did) self.empty_filtered_deck(did)
return return
@ -79,13 +80,13 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
self.col.usn(), 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") self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
# used by v2 scheduler and some add-ons # used by v2 scheduler and some add-ons
def update_stats( def update_stats(
self, self,
deck_id: int, deck_id: DeckID,
new_delta: int = 0, new_delta: int = 0,
review_delta: int = 0, review_delta: int = 0,
milliseconds_delta: int = 0, milliseconds_delta: int = 0,

View file

@ -12,6 +12,7 @@ import anki
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.decks import DeckID
from anki.utils import ids2str, intTime from anki.utils import ids2str, intTime
from .v2 import QueueConfig from .v2 import QueueConfig
@ -97,7 +98,7 @@ class Scheduler(V2):
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
if idx == QUEUE_TYPE_LRN: if idx == QUEUE_TYPE_LRN:
counts[QUEUE_TYPE_LRN] += card.left // 1000 counts[int(QUEUE_TYPE_LRN)] += card.left // 1000
else: else:
counts[idx] += 1 counts[idx] += 1
@ -299,10 +300,11 @@ limit %d"""
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid
card.odue = 0 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 rescheduling is off, it needs to be set back to a new card
if not resched and not lapse: 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") card.due = self.col.nextID("pos")
def _startingLeft(self, card: Card) -> int: def _startingLeft(self, card: Card) -> int:
@ -401,7 +403,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 = ( cnt = (
self.col.db.scalar( self.col.db.scalar(
f""" f"""
@ -426,7 +428,7 @@ and due <= ? limit ?)""",
# Reviews # Reviews
########################################################################## ##########################################################################
def _deckRevLimit(self, did: int) -> int: def _deckRevLimit(self, did: DeckID) -> int:
return self._deckNewLimit(did, self._deckRevLimitSingle) return self._deckNewLimit(did, self._deckRevLimitSingle)
def _resetRev(self) -> None: def _resetRev(self) -> None:
@ -541,7 +543,7 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
card.due = card.odue card.due = card.odue
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid
card.odid = 0 card.odid = DeckID(0)
card.odue = 0 card.odue = 0
# Interval management # Interval management
@ -617,7 +619,8 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
card.due = card.odue card.due = card.odue
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid
card.odue = card.odid = 0 card.odue = 0
card.odid = DeckID(0)
card.queue = QUEUE_TYPE_SUSPENDED card.queue = QUEUE_TYPE_SUSPENDED
# notify UI # notify UI
hooks.card_did_leech(card) hooks.card_did_leech(card)

View file

@ -11,9 +11,9 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card, CardID
from anki.consts import * from anki.consts import *
from anki.decks import DeckConfigDict, DeckDict from anki.decks import DeckConfigDict, DeckDict, DeckID
from anki.lang import FormatTimeSpan from anki.lang import FormatTimeSpan
from anki.scheduler.legacy import SchedulerBaseWithLegacy from anki.scheduler.legacy import SchedulerBaseWithLegacy
from anki.utils import ids2str, intTime from anki.utils import ids2str, intTime
@ -75,7 +75,9 @@ class Scheduler(SchedulerBaseWithLegacy):
def _reset_counts(self) -> None: def _reset_counts(self) -> None:
tree = self.deck_due_tree(self.col.decks.selected()) 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: if not node:
# current deck points to a missing deck # current deck points to a missing deck
self.newCount = 0 self.newCount = 0
@ -144,7 +146,7 @@ class Scheduler(SchedulerBaseWithLegacy):
def _resetNew(self) -> None: def _resetNew(self) -> None:
self._newDids = self.col.decks.active()[:] self._newDids = self.col.decks.active()[:]
self._newQueue: List[int] = [] self._newQueue: List[CardID] = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self, recursing: bool = False) -> bool: def _fillNew(self, recursing: bool = False) -> bool:
@ -210,7 +212,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return None return None
def _deckNewLimit( def _deckNewLimit(
self, did: int, fn: Optional[Callable[[DeckDict], int]] = None self, did: DeckID, fn: Optional[Callable[[DeckDict], int]] = None
) -> int: ) -> int:
if not fn: if not fn:
fn = self._deckNewLimitSingle fn = self._deckNewLimitSingle
@ -225,7 +227,7 @@ class Scheduler(SchedulerBaseWithLegacy):
lim = min(rem, lim) lim = min(rem, lim)
return 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." "New count for a single deck."
if not lim: if not lim:
return 0 return 0
@ -301,8 +303,8 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
def _resetLrn(self) -> None: def _resetLrn(self) -> None:
self._updateLrnCutoff(force=True) self._updateLrnCutoff(force=True)
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue: List[Tuple[int, int]] = [] self._lrnQueue: List[Tuple[int, CardID]] = []
self._lrnDayQueue: List[int] = [] self._lrnDayQueue: List[CardID] = []
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
@ -397,7 +399,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
return hooks.scheduler_review_limit_for_single_deck(lim, d) return hooks.scheduler_review_limit_for_single_deck(lim, d)
def _resetRev(self) -> None: def _resetRev(self) -> None:
self._revQueue: List[int] = [] self._revQueue: List[CardID] = []
def _fillRev(self, recursing: bool = False) -> bool: def _fillRev(self, recursing: bool = False) -> bool:
"True if a review card can be fetched." "True if a review card can be fetched."
@ -720,7 +722,8 @@ limit ?"""
card.ivl = self._graduatingIvl(card, conf, early) card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today + card.ivl card.due = self.today + card.ivl
card.factor = conf["initialFactor"] card.factor = conf["initialFactor"]
card.type = card.queue = QUEUE_TYPE_REV card.type = CARD_TYPE_REV
card.queue = QUEUE_TYPE_REV
def _logLrn( def _logLrn(
self, self,
@ -788,7 +791,7 @@ limit ?"""
if card.odid: if card.odid:
card.did = card.odid card.did = card.odid
card.odue = 0 card.odue = 0
card.odid = 0 card.odid = DeckID(0)
def _restorePreviewCard(self, card: Card) -> None: def _restorePreviewCard(self, card: Card) -> None:
assert card.odid assert card.odid
@ -803,7 +806,7 @@ limit ?"""
else: else:
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
else: else:
card.queue = card.type card.queue = CardQueue(card.type)
# Answering a review card # Answering a review card
########################################################################## ##########################################################################
@ -1072,7 +1075,7 @@ limit ?"""
########################################################################## ##########################################################################
def _burySiblings(self, card: Card) -> None: def _burySiblings(self, card: Card) -> None:
toBury: List[int] = [] toBury: List[CardID] = []
nconf = self._newConf(card) nconf = self._newConf(card)
buryNew = nconf.get("bury", True) buryNew = nconf.get("bury", True)
rconf = self._revConf(card) rconf = self._revConf(card)

View file

@ -19,6 +19,8 @@ import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
import anki.collection import anki.collection
from anki.collection import OpChangesWithCount from anki.collection import OpChangesWithCount
from anki.decks import DeckID
from anki.notes import NoteID
from anki.utils import ids2str from anki.utils import ids2str
# public exports # public exports
@ -48,7 +50,7 @@ class TagManager:
def clear_unused_tags(self) -> OpChangesWithCount: def clear_unused_tags(self) -> OpChangesWithCount:
return self.col._backend.clear_unused_tags() 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" basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children: if not children:
query = f"{basequery} AND c.did=?" query = f"{basequery} AND c.did=?"
@ -68,11 +70,11 @@ class TagManager:
# Bulk addition/removal from specific notes # 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.""" """Add space-separate tags to provided notes, returning changed count."""
return self.col._backend.add_note_tags(note_ids=note_ids, tags=tags) 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) return self.col._backend.remove_note_tags(note_ids=note_ids, tags=tags)
# Find&replace # Find&replace
@ -175,12 +177,12 @@ class TagManager:
) -> None: ) -> None:
print("tags.register() is deprecated and no longer works") 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." "Add tags in bulk. TAGS is space-separated."
if add: if add:
self.bulk_add(ids, tags) self.bulk_add(ids, tags)
else: else:
self.bulk_remove(ids, tags) 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) self.bulkAdd(ids, tags, False)

View file

@ -172,7 +172,7 @@ class TemplateRenderContext:
# add (most) special fields # add (most) special fields
fields["Tags"] = self._note.stringTags().strip() fields["Tags"] = self._note.stringTags().strip()
fields["Type"] = self._note_type["name"] 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.current_deck_id())
fields["Subdeck"] = DeckManager.basename(fields["Deck"]) fields["Subdeck"] = DeckManager.basename(fields["Deck"])
if self._template: if self._template:
fields["Card"] = self._template["name"] fields["Card"] = self._template["name"]

View file

@ -23,7 +23,7 @@ hooks = [
Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"), Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"),
Hook( Hook(
name="notes_will_be_deleted", 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", legacy_hook="remNotes",
), ),
Hook(name="media_files_did_export", args=["count: int"]), Hook(name="media_files_did_export", args=["count: int"]),

View file

@ -8,7 +8,9 @@ import aqt.editor
import aqt.forms import aqt.forms
from anki.collection import OpChanges, SearchNode from anki.collection import OpChanges, SearchNode
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.notes import DuplicateOrEmptyResult, Note from anki.decks import DeckID
from anki.models import NoteTypeID
from anki.notes import DuplicateOrEmptyResult, Note, NoteID
from anki.utils import htmlToTextLine, isMac from anki.utils import htmlToTextLine, isMac
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.note_ops import add_note from aqt.note_ops import add_note
@ -47,7 +49,7 @@ class AddCards(QDialog):
self.setupEditor() self.setupEditor()
self.setupButtons() self.setupButtons()
self._load_new_note() self._load_new_note()
self.history: List[int] = [] self.history: List[NoteID] = []
self._last_added_note: Optional[Note] = None self._last_added_note: Optional[Note] = None
restoreGeom(self, "add") restoreGeom(self, "add")
addCloseShortcut(self) addCloseShortcut(self)
@ -64,12 +66,12 @@ class AddCards(QDialog):
self.notetype_chooser = NoteTypeChooser( self.notetype_chooser = NoteTypeChooser(
mw=self.mw, mw=self.mw,
widget=self.form.modelArea, 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_button_activated=self.show_notetype_selector,
on_notetype_changed=self.on_notetype_change, on_notetype_changed=self.on_notetype_change,
) )
self.deck_chooser = aqt.deckchooser.DeckChooser( 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: def helpRequested(self) -> None:
@ -109,7 +111,7 @@ class AddCards(QDialog):
def show_notetype_selector(self) -> None: def show_notetype_selector(self) -> None:
self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype) 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: NoteTypeID) -> None:
# need to adjust current deck? # need to adjust current deck?
if deck_id := self.mw.col.default_deck_for_notetype(notetype_id): if deck_id := self.mw.col.default_deck_for_notetype(notetype_id):
self.deck_chooser.selected_deck_id = deck_id self.deck_chooser.selected_deck_id = deck_id
@ -178,7 +180,7 @@ class AddCards(QDialog):
gui_hooks.add_cards_will_show_history_menu(self, m) gui_hooks.add_cards_will_show_history_menu(self, m)
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) 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),)) aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
def add_current_note(self) -> None: def add_current_note(self) -> None:

View file

@ -22,12 +22,13 @@ from typing import (
import aqt import aqt
import aqt.forms 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.collection import BrowserRow, Collection, Config, OpChanges, SearchNode
from anki.consts import * from anki.consts import *
from anki.errors import NotFoundError from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.models import NoteType from anki.models import NoteType
from anki.notes import NoteID
from anki.stats import CardStats from anki.stats import CardStats
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
from anki.utils import ids2str, isMac, isWin from anki.utils import ids2str, isMac, isWin
@ -96,7 +97,7 @@ class SearchContext:
browser: Browser browser: Browser
order: Union[bool, str] = True order: Union[bool, str] = True
# if set, provided card ids will be used instead of the regular search # 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 # Data model
@ -169,13 +170,13 @@ class DataModel(QAbstractTableModel):
self.activeCols: List[str] = self.col.get_config( self.activeCols: List[str] = self.col.get_config(
"activeCols", ["noteFld", "template", "cardDue", "deck"] "activeCols", ["noteFld", "template", "cardDue", "deck"]
) )
self.cards: Sequence[int] = [] self.cards: Sequence[CardID] = []
self._rows: Dict[int, CellRow] = {} self._rows: Dict[int, CellRow] = {}
self._last_refresh = 0.0 self._last_refresh = 0.0
# serve stale content to avoid hitting the DB? # serve stale content to avoid hitting the DB?
self.block_updates = False self.block_updates = False
def get_id(self, index: QModelIndex) -> int: def get_id(self, index: QModelIndex) -> CardID:
return self.cards[index.row()] return self.cards[index.row()]
def get_cell(self, index: QModelIndex) -> Cell: def get_cell(self, index: QModelIndex) -> Cell:
@ -197,7 +198,7 @@ class DataModel(QAbstractTableModel):
self._rows[cid] = self._fetch_row_from_backend(cid) self._rows[cid] = self._fetch_row_from_backend(cid)
return self._rows[cid] return self._rows[cid]
def _fetch_row_from_backend(self, cid: int) -> CellRow: def _fetch_row_from_backend(self, cid: CardID) -> CellRow:
try: try:
row = CellRow(*self.col.browser_row_for_card(cid)) row = CellRow(*self.col.browser_row_for_card(cid))
except NotFoundError: except NotFoundError:
@ -1048,13 +1049,13 @@ QTableView {{ gridline-color: {grid} }}
# Menu helpers # Menu helpers
###################################################################### ######################################################################
def selected_cards(self) -> List[int]: def selected_cards(self) -> List[CardID]:
return [ return [
self.model.cards[idx.row()] self.model.cards[idx.row()]
for idx in self.form.tableView.selectionModel().selectedRows() 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( return self.col.db.list(
""" """
select distinct nid from cards select distinct nid from cards
@ -1067,13 +1068,13 @@ where id in %s"""
) )
) )
def selectedNotesAsCards(self) -> List[int]: def selectedNotesAsCards(self) -> List[CardID]:
return self.col.db.list( return self.col.db.list(
"select id from cards where nid in (%s)" "select id from cards where nid in (%s)"
% ",".join([str(s) for s in self.selected_notes()]) % ",".join([str(s) for s in self.selected_notes()])
) )
def oneModelNotes(self) -> List[int]: def oneModelNotes(self) -> List[NoteID]:
sf = self.selected_notes() sf = self.selected_notes()
if not sf: if not sf:
return [] return []
@ -1589,7 +1590,7 @@ where id in %s"""
def onCardList(self) -> None: def onCardList(self) -> None:
self.form.tableView.setFocus() self.form.tableView.setFocus()
def focusCid(self, cid: int) -> None: def focusCid(self, cid: CardID) -> None:
try: try:
row = list(self.model.cards).index(cid) row = list(self.model.cards).index(cid)
except ValueError: except ValueError:
@ -1603,7 +1604,7 @@ where id in %s"""
class ChangeModel(QDialog): 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) QDialog.__init__(self, browser)
self.browser = browser self.browser = browser
self.nids = nids self.nids = nids

View file

@ -5,12 +5,14 @@ from __future__ import annotations
from typing import Sequence from typing import Sequence
from anki.cards import CardID
from anki.decks import DeckID
from aqt import AnkiQt 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)) 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)) mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids))

View file

@ -40,7 +40,7 @@ class DeckBrowserContent:
@dataclass @dataclass
class RenderDeckNodeContext: class RenderDeckNodeContext:
current_deck_id: int current_deck_id: DeckID
class DeckBrowser: class DeckBrowser:
@ -101,7 +101,7 @@ class DeckBrowser:
source, target = arg.split(",") source, target = arg.split(",")
self._handle_drag_and_drop(DeckID(int(source)), DeckID(int(target or 0))) self._handle_drag_and_drop(DeckID(int(source)), DeckID(int(target or 0)))
elif cmd == "collapse": elif cmd == "collapse":
self._collapse(int(arg)) self._collapse(DeckID(int(arg)))
elif cmd == "v2upgrade": elif cmd == "v2upgrade":
self._confirm_upgrade() self._confirm_upgrade()
elif cmd == "v2upgradeinfo": elif cmd == "v2upgradeinfo":
@ -112,7 +112,7 @@ class DeckBrowser:
return False return False
def _selDeck(self, did: str) -> None: def _selDeck(self, did: str) -> None:
self.mw.col.decks.select(int(did)) self.mw.col.decks.select(DeckID(int(did)))
self.mw.onOverview() self.mw.onOverview()
# HTML generation # HTML generation
@ -255,13 +255,13 @@ class DeckBrowser:
a = m.addAction(tr(TR.ACTIONS_OPTIONS)) a = m.addAction(tr(TR.ACTIONS_OPTIONS))
qconnect(a.triggered, lambda b, did=did: self._options(DeckID(int(did)))) qconnect(a.triggered, lambda b, did=did: self._options(DeckID(int(did))))
a = m.addAction(tr(TR.ACTIONS_EXPORT)) 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)) a = m.addAction(tr(TR.ACTIONS_DELETE))
qconnect(a.triggered, lambda b, did=did: self._delete(DeckID(int(did)))) qconnect(a.triggered, lambda b, did=did: self._delete(DeckID(int(did))))
gui_hooks.deck_browser_will_show_options_menu(m, int(did)) gui_hooks.deck_browser_will_show_options_menu(m, int(did))
m.exec_(QCursor.pos()) m.exec_(QCursor.pos())
def _export(self, did: int) -> None: def _export(self, did: DeckID) -> None:
self.mw.onExport(did=did) self.mw.onExport(did=did)
def _rename(self, did: DeckID) -> None: def _rename(self, did: DeckID) -> None:
@ -279,7 +279,7 @@ class DeckBrowser:
self.mw.col.decks.select(did) self.mw.col.decks.select(did)
self.mw.onDeckConf() self.mw.onDeckConf()
def _collapse(self, did: int) -> None: def _collapse(self, did: DeckID) -> None:
self.mw.col.decks.collapse(did) self.mw.col.decks.collapse(did)
node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did) node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did)
if node: if node:

View file

@ -3,6 +3,7 @@
from typing import Optional from typing import Optional
from anki.decks import DEFAULT_DECK_ID, DeckID
from aqt import AnkiQt from aqt import AnkiQt
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, HelpPage, shortcut, tr from aqt.utils import TR, HelpPage, shortcut, tr
@ -14,17 +15,17 @@ class DeckChooser(QHBoxLayout):
mw: AnkiQt, mw: AnkiQt,
widget: QWidget, widget: QWidget,
label: bool = True, label: bool = True,
starting_deck_id: Optional[int] = None, starting_deck_id: Optional[DeckID] = None,
) -> None: ) -> None:
QHBoxLayout.__init__(self) QHBoxLayout.__init__(self)
self._widget = widget # type: ignore self._widget = widget # type: ignore
self.mw = mw self.mw = mw
self._setup_ui(show_label=label) 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 # default to current deck if starting id not provided
if starting_deck_id is None: 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 self.selected_deck_id = starting_deck_id
def _setup_ui(self, show_label: bool) -> None: def _setup_ui(self, show_label: bool) -> None:
@ -56,13 +57,13 @@ class DeckChooser(QHBoxLayout):
) )
@property @property
def selected_deck_id(self) -> int: def selected_deck_id(self) -> DeckID:
self._ensure_selected_deck_valid() self._ensure_selected_deck_valid()
return self._selected_deck_id return self._selected_deck_id
@selected_deck_id.setter @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: if id != self._selected_deck_id:
self._selected_deck_id = id self._selected_deck_id = id
self._ensure_selected_deck_valid() self._ensure_selected_deck_valid()
@ -70,7 +71,7 @@ class DeckChooser(QHBoxLayout):
def _ensure_selected_deck_valid(self) -> None: def _ensure_selected_deck_valid(self) -> None:
if not self.mw.col.decks.get(self._selected_deck_id, default=False): 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: def _update_button_label(self) -> None:
self.deck.setText(self.selected_deck_name().replace("&", "&&")) self.deck.setText(self.selected_deck_name().replace("&", "&&"))
@ -103,7 +104,7 @@ class DeckChooser(QHBoxLayout):
onDeckChange = choose_deck onDeckChange = choose_deck
deckName = selected_deck_name deckName = selected_deck_name
def selectedId(self) -> int: def selectedId(self) -> DeckID:
return self.selected_deck_id return self.selected_deck_id
def cleanup(self) -> None: def cleanup(self) -> None:

View file

@ -5,9 +5,10 @@ from __future__ import annotations
import re import re
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any from typing import Any, List
import aqt import aqt
from anki.cards import CardID
from anki.collection import EmptyCardsReport from anki.collection import EmptyCardsReport
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import QDialog, QDialogButtonBox, qconnect from aqt.qt import QDialog, QDialogButtonBox, qconnect
@ -88,14 +89,14 @@ class EmptyCardsDialog(QDialog):
self.mw.taskman.run_in_background(delete, on_done) self.mw.taskman.run_in_background(delete, on_done)
def _delete_cards(self, keep_notes: bool) -> int: def _delete_cards(self, keep_notes: bool) -> int:
to_delete = [] to_delete: List[CardID] = []
note: EmptyCardsReport.NoteWithEmptyCards note: EmptyCardsReport.NoteWithEmptyCards
for note in self.report.notes: for note in self.report.notes:
if keep_notes and note.will_delete_note: if keep_notes and note.will_delete_note:
# leave first card # leave first card
to_delete.extend(note.card_ids[1:]) to_delete.extend([CardID(id) for id in note.card_ids[1:]])
else: 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) self.mw.col.remove_cards_and_orphaned_notes(to_delete)
return len(to_delete) return len(to_delete)

View file

@ -11,6 +11,8 @@ from typing import List, Optional
import aqt import aqt
from anki import hooks from anki import hooks
from anki.cards import CardID
from anki.decks import DeckID
from anki.exporting import Exporter, exporters from anki.exporting import Exporter, exporters
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
@ -28,8 +30,8 @@ class ExportDialog(QDialog):
def __init__( def __init__(
self, self,
mw: aqt.main.AnkiQt, mw: aqt.main.AnkiQt,
did: Optional[int] = None, did: Optional[DeckID] = None,
cids: Optional[List[int]] = None, cids: Optional[List[CardID]] = None,
): ):
QDialog.__init__(self, mw, Qt.Window) QDialog.__init__(self, mw, Qt.Window)
self.mw = mw self.mw = mw
@ -42,7 +44,7 @@ class ExportDialog(QDialog):
self.setup(did) self.setup(did)
self.exec_() self.exec_()
def setup(self, did: Optional[int]) -> None: def setup(self, did: Optional[DeckID]) -> None:
self.exporters = exporters(self.col) self.exporters = exporters(self.col)
# if a deck specified, start with .apkg type selected # if a deck specified, start with .apkg type selected
idx = 0 idx = 0

View file

@ -7,6 +7,7 @@ from typing import List, Optional, Sequence
import aqt import aqt
from anki.lang import TR from anki.lang import TR
from anki.notes import NoteID
from aqt import AnkiQt, QWidget from aqt import AnkiQt, QWidget
from aqt.qt import QDialog, Qt from aqt.qt import QDialog, Qt
from aqt.utils import ( from aqt.utils import (
@ -31,7 +32,7 @@ def find_and_replace(
*, *,
mw: AnkiQt, mw: AnkiQt,
parent: QWidget, parent: QWidget,
note_ids: Sequence[int], note_ids: Sequence[NoteID],
search: str, search: str,
replacement: str, replacement: str,
regex: bool, regex: bool,
@ -82,7 +83,9 @@ def find_and_replace_tag(
class FindAndReplaceDialog(QDialog): class FindAndReplaceDialog(QDialog):
COMBO_NAME = "BrowserFindAndReplace" 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) super().__init__(parent)
self.mw = mw self.mw = mw
self.note_ids = note_ids self.note_ids = note_ids

View file

@ -52,8 +52,9 @@ from anki.collection import (
UndoResult, UndoResult,
UndoStatus, UndoStatus,
) )
from anki.decks import DeckDict from anki.decks import DeckDict, DeckID
from anki.hooks import runHook from anki.hooks import runHook
from anki.notes import NoteID
from anki.sound import AVTag, SoundOrVideoTag from anki.sound import AVTag, SoundOrVideoTag
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
@ -1381,7 +1382,7 @@ title="%s" %s>%s</button>""" % (
aqt.importing.onImport(self) aqt.importing.onImport(self)
def onExport(self, did: Optional[int] = None) -> None: def onExport(self, did: Optional[DeckID] = None) -> None:
import aqt.exporting import aqt.exporting
aqt.exporting.ExportDialog(self, did=did) aqt.exporting.ExportDialog(self, did=did)
@ -1534,7 +1535,7 @@ title="%s" %s>%s</button>""" % (
# Log note deletion # 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") path = os.path.join(self.pm.profileFolder(), "deleted.txt")
existed = os.path.exists(path) existed = os.path.exists(path)
with open(path, "ab") as f: with open(path, "ab") as f:

View file

@ -8,7 +8,7 @@ from typing import Any, List, Optional, Sequence
import aqt.clayout import aqt.clayout
from anki import stdmodels from anki import stdmodels
from anki.lang import without_unicode_isolation 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 anki.notes import Note
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -33,7 +33,7 @@ class Models(QDialog):
mw: AnkiQt, mw: AnkiQt,
parent: Optional[QWidget] = None, parent: Optional[QWidget] = None,
fromMain: bool = False, fromMain: bool = False,
selected_notetype_id: Optional[int] = None, selected_notetype_id: Optional[NoteTypeID] = None,
): ):
self.mw = mw self.mw = mw
parent = parent or mw parent = parent or mw

View file

@ -5,7 +5,8 @@ from __future__ import annotations
from typing import Callable, Sequence from typing import Callable, Sequence
from anki.notes import Note from anki.decks import DeckID
from anki.notes import Note, NoteID
from aqt import AnkiQt from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
@ -14,7 +15,7 @@ def add_note(
*, *,
mw: AnkiQt, mw: AnkiQt,
note: Note, note: Note,
target_deck_id: int, target_deck_id: DeckID,
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success) mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success)
@ -30,7 +31,7 @@ def update_note(*, mw: AnkiQt, note: Note, after_hooks: Callable[[], None]) -> N
def remove_notes( def remove_notes(
*, *,
mw: AnkiQt, mw: AnkiQt,
note_ids: Sequence[int], note_ids: Sequence[NoteID],
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success) mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success)

View file

@ -2,6 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import List, Optional from typing import List, Optional
from anki.models import NoteTypeID
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, HelpPage, shortcut, tr from aqt.utils import TR, HelpPage, shortcut, tr
@ -22,14 +23,16 @@ class NoteTypeChooser(QHBoxLayout):
deleted. deleted.
""" """
_selected_notetype_id: NoteTypeID
def __init__( def __init__(
self, self,
*, *,
mw: AnkiQt, mw: AnkiQt,
widget: QWidget, widget: QWidget,
starting_notetype_id: int, starting_notetype_id: NoteTypeID,
on_button_activated: Optional[Callable[[], None]] = None, on_button_activated: Optional[Callable[[], None]] = None,
on_notetype_changed: Optional[Callable[[int], None]] = None, on_notetype_changed: Optional[Callable[[NoteTypeID], None]] = None,
show_prefix_label: bool = True, show_prefix_label: bool = True,
) -> None: ) -> None:
QHBoxLayout.__init__(self) QHBoxLayout.__init__(self)
@ -41,7 +44,7 @@ class NoteTypeChooser(QHBoxLayout):
self.on_button_activated = self.choose_notetype self.on_button_activated = self.choose_notetype
self._setup_ui(show_label=show_prefix_label) self._setup_ui(show_label=show_prefix_label)
gui_hooks.state_did_reset.append(self.reset_state) 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 # triggers UI update; avoid firing changed hook on startup
self.on_notetype_changed = None self.on_notetype_changed = None
self.selected_notetype_id = starting_notetype_id self.selected_notetype_id = starting_notetype_id
@ -118,7 +121,7 @@ class NoteTypeChooser(QHBoxLayout):
self.selected_notetype_id = id self.selected_notetype_id = id
@property @property
def selected_notetype_id(self) -> int: def selected_notetype_id(self) -> NoteTypeID:
# theoretically this should not be necessary, as we're listening to # theoretically this should not be necessary, as we're listening to
# resets # resets
self._ensure_selected_notetype_valid() self._ensure_selected_notetype_valid()
@ -126,7 +129,7 @@ class NoteTypeChooser(QHBoxLayout):
return self._selected_notetype_id return self._selected_notetype_id
@selected_notetype_id.setter @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: if id != self._selected_notetype_id:
self._selected_notetype_id = id self._selected_notetype_id = id
self._ensure_selected_notetype_valid() self._ensure_selected_notetype_valid()
@ -139,7 +142,9 @@ class NoteTypeChooser(QHBoxLayout):
def _ensure_selected_notetype_valid(self) -> None: def _ensure_selected_notetype_valid(self) -> None:
if not self.mw.col.models.get(self._selected_notetype_id): 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: def _update_button_label(self) -> None:
self.button.setText(self.selected_notetype_name().replace("&", "&&")) self.button.setText(self.selected_notetype_name().replace("&", "&&"))

View file

@ -13,7 +13,7 @@ from typing import Any, Callable, List, Match, Optional, Sequence, Tuple, Union
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card, CardID
from anki.collection import Config, OpChanges from anki.collection import Config, OpChanges
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
from anki.utils import stripHTML from anki.utils import stripHTML
@ -74,7 +74,7 @@ class Reviewer:
self.card: Optional[Card] = None self.card: Optional[Card] = None
self.cardQueue: List[Card] = [] self.cardQueue: List[Card] = []
self.hadCardQueue = False self.hadCardQueue = False
self._answeredIds: List[int] = [] self._answeredIds: List[CardID] = []
self._recordedAudio: Optional[str] = None self._recordedAudio: Optional[str] = None
self.typeCorrect: str = None # web init happens before this is set self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None self.state: Optional[str] = None
@ -834,7 +834,7 @@ time = %(time)d;
qconnect(a.triggered, func) qconnect(a.triggered, func)
def onOptions(self) -> None: 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.current_deck_id()))
def set_flag_on_current_card(self, desired_flag: int) -> None: def set_flag_on_current_card(self, desired_flag: int) -> None:
# need to toggle off? # need to toggle off?

View file

@ -6,9 +6,11 @@ from __future__ import annotations
from typing import List, Optional, Sequence from typing import List, Optional, Sequence
import aqt import aqt
from anki.cards import CardID
from anki.collection import CARD_TYPE_NEW, Config from anki.collection import CARD_TYPE_NEW, Config
from anki.decks import DeckID from anki.decks import DeckID
from anki.lang import TR from anki.lang import TR
from anki.notes import NoteID
from anki.scheduler import FilteredDeckForUpdate from anki.scheduler import FilteredDeckForUpdate
from aqt import AnkiQt from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
@ -20,7 +22,7 @@ def set_due_date_dialog(
*, *,
mw: aqt.AnkiQt, mw: aqt.AnkiQt,
parent: QWidget, parent: QWidget,
card_ids: List[int], card_ids: List[CardID],
config_key: Optional[Config.String.Key.V], config_key: Optional[Config.String.Key.V],
) -> None: ) -> None:
if not card_ids: if not card_ids:
@ -53,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: if not card_ids:
return return
@ -66,7 +68,7 @@ def forget_cards(*, mw: aqt.AnkiQt, parent: QWidget, card_ids: List[int]) -> Non
def reposition_new_cards_dialog( def reposition_new_cards_dialog(
*, mw: AnkiQt, parent: QWidget, card_ids: Sequence[int] *, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardID]
) -> None: ) -> None:
assert mw.col.db assert mw.col.db
row = mw.col.db.first( row = mw.col.db.first(
@ -111,7 +113,7 @@ def reposition_new_cards(
*, *,
mw: AnkiQt, mw: AnkiQt,
parent: QWidget, parent: QWidget,
card_ids: Sequence[int], card_ids: Sequence[CardID],
starting_from: int, starting_from: int,
step_size: int, step_size: int,
randomize: bool, randomize: bool,
@ -134,7 +136,7 @@ def reposition_new_cards(
def suspend_cards( def suspend_cards(
*, *,
mw: AnkiQt, mw: AnkiQt,
card_ids: Sequence[int], card_ids: Sequence[CardID],
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success) mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success)
@ -143,7 +145,7 @@ def suspend_cards(
def suspend_note( def suspend_note(
*, *,
mw: AnkiQt, mw: AnkiQt,
note_id: int, note_id: NoteID,
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.taskman.run_in_background( mw.taskman.run_in_background(
@ -152,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)) mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids))
def bury_cards( def bury_cards(
*, *,
mw: AnkiQt, mw: AnkiQt,
card_ids: Sequence[int], card_ids: Sequence[CardID],
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success) mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success)
@ -168,7 +170,7 @@ def bury_cards(
def bury_note( def bury_note(
*, *,
mw: AnkiQt, mw: AnkiQt,
note_id: int, note_id: NoteID,
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
mw.taskman.run_in_background( mw.taskman.run_in_background(

View file

@ -9,6 +9,7 @@ from typing import Dict, Iterable, List, Optional, Tuple, cast
import aqt import aqt
from anki.collection import Config, OpChanges, SearchJoiner, SearchNode from anki.collection import Config, OpChanges, SearchJoiner, SearchNode
from anki.decks import DeckID, DeckTreeNode from anki.decks import DeckID, DeckTreeNode
from anki.models import NoteTypeID
from anki.notes import Note from anki.notes import Note
from anki.tags import TagTreeNode from anki.tags import TagTreeNode
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
@ -976,7 +977,7 @@ class SidebarTreeView(QTreeView):
for node in nodes: for node in nodes:
def toggle_expand() -> Callable[[bool], None]: 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) return lambda _: self.mw.col.decks.collapseBrowser(did)
item = SidebarItem( item = SidebarItem(
@ -1158,7 +1159,7 @@ class SidebarTreeView(QTreeView):
########################### ###########################
def rename_deck(self, item: SidebarItem, new_name: str) -> None: 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: if not new_name:
return return
new_name = item.name_prefix + new_name new_name = item.name_prefix + new_name
@ -1291,11 +1292,14 @@ class SidebarTreeView(QTreeView):
def manage_notetype(self, item: SidebarItem) -> None: def manage_notetype(self, item: SidebarItem) -> None:
Models( 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: 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) CardLayout(self.mw, note, ord=item.id, parent=self, fill_empty=True)
# Helpers # Helpers

View file

@ -5,6 +5,7 @@ from typing import List, Optional
import aqt import aqt
from anki.collection import OpChangesWithID from anki.collection import OpChangesWithID
from anki.decks import DeckID
from aqt import gui_hooks from aqt import gui_hooks
from aqt.deck_ops import add_deck_dialog from aqt.deck_ops import add_deck_dialog
from aqt.qt import * from aqt.qt import *
@ -167,7 +168,7 @@ class StudyDeck(QDialog):
default = self.names[self.form.list.currentRow()] default = self.names[self.form.list.currentRow()]
def success(out: OpChangesWithID) -> None: 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"] self.name = deck["name"]
# make sure we clean up reset hook when manually exiting # make sure we clean up reset hook when manually exiting

View file

@ -7,6 +7,7 @@ from typing import Callable, Sequence
from anki.collection import OpChangesWithCount from anki.collection import OpChangesWithCount
from anki.lang import TR from anki.lang import TR
from anki.notes import NoteID
from aqt import AnkiQt, QWidget from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
from aqt.utils import showInfo, tooltip, tr from aqt.utils import showInfo, tooltip, tr
@ -15,7 +16,7 @@ from aqt.utils import showInfo, tooltip, tr
def add_tags( def add_tags(
*, *,
mw: AnkiQt, mw: AnkiQt,
note_ids: Sequence[int], note_ids: Sequence[NoteID],
space_separated_tags: str, space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None:
@ -27,7 +28,7 @@ def add_tags(
def remove_tags_for_notes( def remove_tags_for_notes(
*, *,
mw: AnkiQt, mw: AnkiQt,
note_ids: Sequence[int], note_ids: Sequence[NoteID],
space_separated_tags: str, space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
) -> None: ) -> None: