diff --git a/pylib/anki/_legacy.py b/pylib/anki/_legacy.py index 3d6e73358..b0b7d2293 100644 --- a/pylib/anki/_legacy.py +++ b/pylib/anki/_legacy.py @@ -3,7 +3,11 @@ from __future__ import annotations -from typing import Any, Callable, Dict, Optional, Tuple, Union +import functools +import os +import pathlib +import traceback +from typing import Any, Callable, Dict, Optional, Tuple, Union, no_type_check import stringcase @@ -18,20 +22,37 @@ def _target_to_string(target: DeprecatedAliasTarget) -> str: return target[1] # type: ignore +def partial_path(full_path: str, components: int) -> str: + path = pathlib.Path(full_path) + return os.path.join(*path.parts[-components:]) + + +def _print_deprecation_warning(old: str, doc: str) -> None: + path, linenum, fn, y = traceback.extract_stack(limit=5)[2] + path = partial_path(path, components=3) + print(f"{path}:{linenum}:{old} is deprecated: {doc}") + + class DeprecatedNamesMixin: "Expose instance methods/vars as camelCase for legacy callers." + # the @no_type_check lines are required to prevent mypy allowing arbitrary + # attributes on the consuming class + _deprecated_aliases: Dict[str, str] = {} + @no_type_check def __getattr__(self, name: str) -> Any: remapped = self._deprecated_aliases.get(name) or stringcase.snakecase(name) if remapped == name: raise AttributeError out = getattr(self, remapped) - print(f"please use {remapped} instead of {name} on {self}") + _print_deprecation_warning(f"'{name}'", f"please use '{remapped}'") + return out + @no_type_check @classmethod def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None: """Manually add aliases that are not a simple transform. @@ -47,15 +68,15 @@ def deprecated(replaced_by: Optional[Callable] = None, info: str = "") -> Callab """Print a deprecation warning, telling users to use `replaced_by`, or show `doc`.""" def decorator(func: Callable) -> Callable: + @functools.wraps(func) def decorated_func(*args: Any, **kwargs: Any) -> Any: if replaced_by: doc = f"please use {replaced_by.__name__} instead." else: doc = info - print( - f"'{func.__name__}' is deprecated, and will be removed in the future: ", - doc, - ) + + _print_deprecation_warning(f"{func.__name__}()", doc) + return func(*args, **kwargs) return decorated_func diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 809efd577..bd227bdcf 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# pylint: enable=invalid-name + from __future__ import annotations import pprint @@ -10,6 +12,7 @@ from typing import List, NewType, Optional import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb from anki import hooks +from anki._legacy import DeprecatedNamesMixin, deprecated from anki.consts import * from anki.models import NotetypeDict, TemplateDict from anki.notes import Note @@ -22,7 +25,7 @@ from anki.sound import AVTag # Queue: same as above, and: # -1=suspended, -2=user buried, -3=sched buried # Due is used differently for different queues. -# - new queue: note id or random int +# - new queue: position # - rev queue: integer day # - lrn queue: integer timestamp @@ -31,9 +34,8 @@ CardId = NewType("CardId", int) BackendCard = _pb.Card -class Card: +class Card(DeprecatedNamesMixin): _note: Optional[Note] - timerStarted: Optional[float] lastIvl: int ord: int nid: anki.notes.NoteId @@ -50,7 +52,7 @@ class Card: backend_card: Optional[BackendCard] = None, ) -> None: self.col = col.weakref() - self.timerStarted = None + self.timer_started: Optional[float] = None self._render_output: Optional[anki.template.TemplateRenderOutput] = None if id: # existing card @@ -63,31 +65,31 @@ class Card: self._load_from_backend_card(_pb.Card()) def load(self) -> None: - c = self.col._backend.get_card(self.id) - assert c - self._load_from_backend_card(c) + card = self.col._backend.get_card(self.id) + assert card + self._load_from_backend_card(card) - def _load_from_backend_card(self, c: _pb.Card) -> None: + def _load_from_backend_card(self, card: _pb.Card) -> None: self._render_output = None self._note = None - self.id = CardId(c.id) - self.nid = anki.notes.NoteId(c.note_id) - self.did = anki.decks.DeckId(c.deck_id) - self.ord = c.template_idx - self.mod = c.mtime_secs - self.usn = c.usn - self.type = CardType(c.ctype) - self.queue = CardQueue(c.queue) - self.due = c.due - self.ivl = c.interval - self.factor = c.ease_factor - self.reps = c.reps - self.lapses = c.lapses - self.left = c.remaining_steps - self.odue = c.original_due - self.odid = anki.decks.DeckId(c.original_deck_id) - self.flags = c.flags - self.data = c.data + self.id = CardId(card.id) + self.nid = anki.notes.NoteId(card.note_id) + self.did = anki.decks.DeckId(card.deck_id) + self.ord = card.template_idx + self.mod = card.mtime_secs + self.usn = card.usn + self.type = CardType(card.ctype) + self.queue = CardQueue(card.queue) + self.due = card.due + self.ivl = card.interval + self.factor = card.ease_factor + self.reps = card.reps + self.lapses = card.lapses + self.left = card.remaining_steps + self.odue = card.original_due + self.odid = anki.decks.DeckId(card.original_deck_id) + self.flags = card.flags + self.data = card.data def _to_backend_card(self) -> _pb.Card: # mtime & usn are set by backend @@ -131,10 +133,6 @@ class Card: def answer_av_tags(self) -> List[AVTag]: return self.render_output().answer_av_tags - # legacy - def css(self) -> str: - return f"" - def render_output( self, reload: bool = False, browser: bool = False ) -> anki.template.TemplateRenderOutput: @@ -157,31 +155,25 @@ class Card: def note_type(self) -> NotetypeDict: return self.col.models.get(self.note().mid) - # legacy aliases - flushSched = flush - q = question - a = answer - model = note_type - def template(self) -> TemplateDict: - m = self.model() - if m["type"] == MODEL_STD: - return self.model()["tmpls"][self.ord] + notetype = self.note_type() + if notetype["type"] == MODEL_STD: + return self.note_type()["tmpls"][self.ord] else: - return self.model()["tmpls"][0] + return self.note_type()["tmpls"][0] - def startTimer(self) -> None: - self.timerStarted = time.time() + def start_timer(self) -> None: + self.timer_started = time.time() def current_deck_id(self) -> anki.decks.DeckId: return anki.decks.DeckId(self.odid or self.did) - def timeLimit(self) -> int: + def time_limit(self) -> int: "Time limit for answering in milliseconds." conf = self.col.decks.confForDid(self.current_deck_id()) return conf["maxTaken"] * 1000 - def shouldShowTimer(self) -> bool: + def should_show_timer(self) -> bool: conf = self.col.decks.confForDid(self.current_deck_id()) return conf["timer"] @@ -192,23 +184,19 @@ class Card: def autoplay(self) -> bool: return self.col.decks.confForDid(self.current_deck_id())["autoplay"] - def timeTaken(self) -> int: + def time_taken(self) -> int: "Time taken to answer card, in integer MS." - total = int((time.time() - self.timerStarted) * 1000) - return min(total, self.timeLimit()) + total = int((time.time() - self.timer_started) * 1000) + return min(total, self.time_limit()) - # legacy - def isEmpty(self) -> bool: - return False - - def __repr__(self) -> str: - d = dict(self.__dict__) + def description(self) -> str: + dict_copy = dict(self.__dict__) # remove non-useful elements - del d["_note"] - del d["_render_output"] - del d["col"] - del d["timerStarted"] - return f"{super().__repr__()} {pprint.pformat(d, width=300)}" + del dict_copy["_note"] + del dict_copy["_render_output"] + del dict_copy["col"] + del dict_copy["timerStarted"] + return f"{super().__repr__()} {pprint.pformat(dict_copy, width=300)}" def user_flag(self) -> int: return self.flags & 0b111 @@ -218,7 +206,18 @@ class Card: assert 0 <= flag <= 7 self.flags = (self.flags & ~0b111) | flag - # legacy + @deprecated(info="use card.render_output() directly") + def css(self) -> str: + return f"" - userFlag = user_flag - setUserFlag = set_user_flag + @deprecated(info="handled by template rendering") + def is_empty(self) -> bool: + return False + + +Card.register_deprecated_aliases( + flushSched=Card.flush, + q=Card.question, + a=Card.answer, + model=Card.note_type, +) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index bdea0f3b7..a3bc3fcba 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -429,7 +429,7 @@ class Collection: return Note(self, self.models.current(forDeck)) def addNote(self, note: Note) -> int: - self.add_note(note, note.model()["did"]) + self.add_note(note, note.note_type()["did"]) return len(note.cards()) def remNotes(self, ids: Sequence[NoteId]) -> None: diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 9b5f15986..c06363e7b 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -118,8 +118,8 @@ class TextCardExporter(Exporter): out = "" for cid in ids: c = self.col.getCard(cid) - out += esc(c.q()) - out += "\t" + esc(c.a()) + "\n" + out += esc(c.question()) + out += "\t" + esc(c.answer()) + "\n" file.write(out.encode("utf-8")) diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 555e48c25..9d0234b8c 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -61,7 +61,7 @@ class Note(DeprecatedNamesMixin): self.usn = note.usn self.tags = list(note.tags) self.fields = list(note.fields) - self._fmap = self.col.models.fieldMap(self.model()) + self._fmap = self.col.models.fieldMap(self.note_type()) def _to_backend_note(self) -> _pb.Note: hooks.note_will_flush(self) @@ -98,7 +98,7 @@ class Note(DeprecatedNamesMixin): card.ord = ord card.did = anki.decks.DEFAULT_DECK_ID - model = custom_note_type or self.model() + model = custom_note_type or self.note_type() template = copy.copy( custom_template or ( @@ -125,10 +125,10 @@ class Note(DeprecatedNamesMixin): def card_ids(self) -> Sequence[anki.cards.CardId]: return self.col.card_ids_of_note(self.id) - def model(self) -> Optional[NotetypeDict]: + def note_type(self) -> Optional[NotetypeDict]: return self.col.models.get(self.mid) - _model = property(model) + _note_type = property(note_type) def cloze_numbers_in_fields(self) -> Sequence[int]: return self.col._backend.cloze_numbers_in_note(self._to_backend_note()) @@ -193,4 +193,6 @@ class Note(DeprecatedNamesMixin): dupeOrEmpty = duplicate_or_empty = fields_check -Note.register_deprecated_aliases(delTag=Note.remove_tag, _fieldOrd=Note._field_index) +Note.register_deprecated_aliases( + delTag=Note.remove_tag, _fieldOrd=Note._field_index, model=Note.note_type +) diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 988eb7aab..d84054387 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -85,7 +85,7 @@ class Scheduler(V2): card.did, new_delta=new_delta, review_delta=review_delta, - milliseconds_delta=+card.timeTaken(), + milliseconds_delta=+card.time_taken(), ) card.mod = intTime() @@ -363,7 +363,7 @@ limit %d""" ivl, lastIvl, card.factor, - card.timeTaken(), + card.time_taken(), type, ) diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index 1c7cb1c2b..8b358e394 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -105,7 +105,7 @@ class Scheduler(SchedulerBaseWithLegacy): self.col.log(card) if not self._burySiblingsOnAnswer: self._burySiblings(card) - card.startTimer() + card.start_timer() return card return None @@ -494,7 +494,7 @@ limit ?""" card.did, new_delta=new_delta, review_delta=review_delta, - milliseconds_delta=+card.timeTaken(), + milliseconds_delta=+card.time_taken(), ) # once a card has been answered once, the original due date @@ -760,7 +760,7 @@ limit ?""" ivl, lastIvl, card.factor, - card.timeTaken(), + card.time_taken(), type, ) @@ -891,7 +891,7 @@ limit ?""" -delay or card.ivl, card.lastIvl, card.factor, - card.timeTaken(), + card.time_taken(), type, ) diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 35bdf0f98..4986e279a 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -78,7 +78,7 @@ class Scheduler(SchedulerBaseWithLegacy): new_state=new_state, rating=rating, answered_at_millis=intTime(1000), - milliseconds_taken=card.timeTaken(), + milliseconds_taken=card.time_taken(), ) def answer_card(self, input: CardAnswer) -> OpChanges: @@ -106,7 +106,7 @@ class Scheduler(SchedulerBaseWithLegacy): card = Card(self.col) card._load_from_backend_card(queued_card.card) - card.startTimer() + card.start_timer() return card def _is_finished(self) -> bool: diff --git a/pylib/anki/template.py b/pylib/anki/template.py index a3786befb..332420f7d 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -155,7 +155,7 @@ class TemplateRenderContext: self._fields: Optional[Dict] = None self._latex_svg = False if not notetype: - self._note_type = note.model() + self._note_type = note.note_type() else: self._note_type = notetype @@ -190,7 +190,7 @@ class TemplateRenderContext: def card(self) -> Card: """Returns the card being rendered. - Be careful not to call .q() or .a() on the card, or you'll create an + Be careful not to call .question() or .answer() on the card, or you'll create an infinite loop.""" return self._card diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index f92daa365..5da64bfb0 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -72,7 +72,7 @@ def test_noteAddDelete(): assert col.cardCount() == 4 # check q/a generation c0 = note.cards()[0] - assert "three" in c0.q() + assert "three" in c0.question() # it should not be a duplicate assert not note.fields_check() # now let's make a duplicate @@ -138,15 +138,15 @@ def test_furigana(): n["Front"] = "foo[abc]" col.addNote(n) c = n.cards()[0] - assert c.q().endswith("abc") + assert c.question().endswith("abc") # and should avoid sound n["Front"] = "foo[sound:abc.mp3]" n.flush() - assert "anki:play" in c.q(reload=True) + assert "anki:play" in c.question(reload=True) # it shouldn't throw an error while people are editing m["tmpls"][0]["qfmt"] = "{{kana:}}" mm.save(m) - c.q(reload=True) + c.question(reload=True) def test_translate(): diff --git a/pylib/tests/test_decks.py b/pylib/tests/test_decks.py index 297a63bad..03ad466e4 100644 --- a/pylib/tests/test_decks.py +++ b/pylib/tests/test_decks.py @@ -53,12 +53,12 @@ def test_remove(): deck1 = col.decks.id("deck1") note = col.newNote() note["Front"] = "1" - note.model()["did"] = deck1 + note.note_type()["did"] = deck1 col.addNote(note) c = note.cards()[0] assert c.did == deck1 assert col.cardCount() == 1 - col.decks.rem(deck1) + col.decks.remove([deck1]) assert col.cardCount() == 0 # if we try to get it, we get the default assert col.decks.name(c.did) == "[no deck]" @@ -142,7 +142,7 @@ def test_renameForDragAndDrop(): new_hsk_did = col.decks.id("hsk") col.decks.renameForDragAndDrop(new_hsk_did, chinese_did) assert deckNames() == ["Chinese", "Chinese::HSK", "Chinese::hsk+", "Languages"] - col.decks.rem(new_hsk_did) + col.decks.remove([new_hsk_did]) # '' is a convenient alias for the top level DID col.decks.renameForDragAndDrop(hsk_did, "") diff --git a/pylib/tests/test_exporting.py b/pylib/tests/test_exporting.py index d5659c921..fe3691896 100644 --- a/pylib/tests/test_exporting.py +++ b/pylib/tests/test_exporting.py @@ -36,7 +36,7 @@ def setup1(): note = col.newNote() note["Front"] = "baz" note["Back"] = "qux" - note.model()["did"] = col.decks.id("new col") + note.note_type()["did"] = col.decks.id("new col") col.addNote(note) diff --git a/pylib/tests/test_flags.py b/pylib/tests/test_flags.py index bd0d44c88..9408b7099 100644 --- a/pylib/tests/test_flags.py +++ b/pylib/tests/test_flags.py @@ -35,11 +35,3 @@ def test_flags(): col.set_user_flag_for_cards(0, [c.id]) c.load() assert c.user_flag() == 0 - - # should work with Cards method as well - c.set_user_flag(2) - assert c.user_flag() == 2 - c.set_user_flag(3) - assert c.user_flag() == 3 - c.set_user_flag(0) - assert c.user_flag() == 0 diff --git a/pylib/tests/test_importing.py b/pylib/tests/test_importing.py index 2fbbe4903..b90ccc230 100644 --- a/pylib/tests/test_importing.py +++ b/pylib/tests/test_importing.py @@ -38,7 +38,7 @@ def test_anki2_mediadupes(): # add a note that references a sound n = col.newNote() n["Front"] = "[sound:foo.mp3]" - mid = n.model()["id"] + mid = n.note_type()["id"] col.addNote(n) # add that sound to media folder with open(os.path.join(col.media.dir(), "foo.mp3"), "w") as note: diff --git a/pylib/tests/test_latex.py b/pylib/tests/test_latex.py index 879df5b2c..5ad4b3529 100644 --- a/pylib/tests/test_latex.py +++ b/pylib/tests/test_latex.py @@ -23,7 +23,7 @@ def test_latex(): # but since latex couldn't run, there's nothing there assert len(os.listdir(col.media.dir())) == 0 # check the error message - msg = note.cards()[0].q() + msg = note.cards()[0].question() assert "executing nolatex" in without_unicode_isolation(msg) assert "installed" in msg # check if we have latex installed, and abort test if we don't @@ -35,12 +35,12 @@ def test_latex(): # check media db should cause latex to be generated col.media.render_all_latex() assert len(os.listdir(col.media.dir())) == 1 - assert ".png" in note.cards()[0].q() + assert ".png" in note.cards()[0].question() # adding new notes should cause generation on question display note = col.newNote() note["Front"] = "[latex]world[/latex]" col.addNote(note) - note.cards()[0].q() + note.cards()[0].question() assert len(os.listdir(col.media.dir())) == 2 # another note with the same media should reuse note = col.newNote() @@ -48,7 +48,7 @@ def test_latex(): col.addNote(note) assert len(os.listdir(col.media.dir())) == 2 oldcard = note.cards()[0] - assert ".png" in oldcard.q() + assert ".png" in oldcard.question() # if we turn off building, then previous cards should work, but cards with # missing media will show a broken image anki.latex.build = False @@ -56,7 +56,7 @@ def test_latex(): note["Front"] = "[latex]foo[/latex]" col.addNote(note) assert len(os.listdir(col.media.dir())) == 2 - assert ".png" in oldcard.q() + assert ".png" in oldcard.question() # turn it on again so other test don't suffer anki.latex.build = True @@ -95,5 +95,5 @@ def _test_includes_bad_command(bad): note = col.newNote() note["Front"] = "[latex]%s[/latex]" % bad col.addNote(note) - q = without_unicode_isolation(note.cards()[0].q()) + q = without_unicode_isolation(note.cards()[0].question()) return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q) diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index 89080edd0..d411d5970 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -112,7 +112,7 @@ def test_templates(): # and should have updated the other cards' ordinals c = note.cards()[0] assert c.ord == 0 - assert stripHTML(c.q()) == "1" + assert stripHTML(c.question()) == "1" # it shouldn't be possible to orphan notes by removing templates t = mm.newTemplate("template name") t["qfmt"] = "{{Front}}2" @@ -158,14 +158,14 @@ def test_text(): note = col.newNote() note["Front"] = "helloworld" col.addNote(note) - assert "helloworld" in note.cards()[0].q() + assert "helloworld" in note.cards()[0].question() def test_cloze(): col = getEmptyCol() col.models.setCurrent(col.models.byName("Cloze")) note = col.newNote() - assert note.model()["name"] == "Cloze" + assert note.note_type()["name"] == "Cloze" # a cloze model with no clozes is not empty note["Text"] = "nothing" assert col.addNote(note) @@ -173,30 +173,30 @@ def test_cloze(): note = col.newNote() note["Text"] = "hello {{c1::world}}" assert col.addNote(note) == 1 - assert "hello [...]" in note.cards()[0].q() - assert "hello world" in note.cards()[0].a() + assert "hello [...]" in note.cards()[0].question() + assert "hello world" in note.cards()[0].answer() # and with a comment note = col.newNote() note["Text"] = "hello {{c1::world::typical}}" assert col.addNote(note) == 1 - assert "[typical]" in note.cards()[0].q() - assert "world" in note.cards()[0].a() + assert "[typical]" in note.cards()[0].question() + assert "world" in note.cards()[0].answer() # and with 2 clozes note = col.newNote() note["Text"] = "hello {{c1::world}} {{c2::bar}}" assert col.addNote(note) == 2 (c1, c2) = note.cards() - assert "[...] bar" in c1.q() - assert "world bar" in c1.a() - assert "world [...]" in c2.q() - assert "world bar" in c2.a() + assert "[...] bar" in c1.question() + assert "world bar" in c1.answer() + assert "world [...]" in c2.question() + assert "world bar" in c2.answer() # if there are multiple answers for a single cloze, they are given in a # list note = col.newNote() note["Text"] = "a {{c1::b}} {{c1::c}}" assert col.addNote(note) == 1 assert "b c" in ( - note.cards()[0].a() + note.cards()[0].answer() ) # if we add another cloze, a card should be generated cnt = col.cardCount() @@ -218,11 +218,11 @@ def test_cloze_mathjax(): ] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}" assert col.addNote(note) assert len(note.cards()) == 5 - assert "class=cloze" in note.cards()[0].q() - assert "class=cloze" in note.cards()[1].q() - assert "class=cloze" not in note.cards()[2].q() - assert "class=cloze" in note.cards()[3].q() - assert "class=cloze" in note.cards()[4].q() + assert "class=cloze" in note.cards()[0].question() + assert "class=cloze" in note.cards()[1].question() + assert "class=cloze" not in note.cards()[2].question() + assert "class=cloze" in note.cards()[3].question() + assert "class=cloze" in note.cards()[4].question() note = col.newNote() note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]" @@ -230,7 +230,7 @@ def test_cloze_mathjax(): assert len(note.cards()) == 1 assert ( note.cards()[0] - .q() + .question() .endswith(r"\(a\) [...] \[ [...] \]") ) @@ -244,7 +244,7 @@ def test_typecloze(): note = col.newNote() note["Text"] = "hello {{c1::world}}" col.addNote(note) - assert "[[type:cloze:Text]]" in note.cards()[0].q() + assert "[[type:cloze:Text]]" in note.cards()[0].question() def test_chained_mods(): @@ -275,11 +275,11 @@ def test_chained_mods(): assert col.addNote(note) == 1 assert ( "This [sentence] demonstrates [chained] clozes." - in note.cards()[0].q() + in note.cards()[0].question() ) assert ( "This phrase demonstrates en chaine clozes." - in note.cards()[0].a() + in note.cards()[0].answer() ) @@ -309,16 +309,16 @@ def test_modelChange(): # switch cards c0 = note.cards()[0] c1 = note.cards()[1] - assert "b123" in c0.q() - assert "note" in c1.q() + assert "b123" in c0.question() + assert "note" in c1.question() assert c0.ord == 0 assert c1.ord == 1 col.models.change(basic, [note.id], basic, noop, map) note.load() c0.load() c1.load() - assert "note" in c0.q() - assert "b123" in c1.q() + assert "note" in c0.question() + assert "b123" in c1.question() assert c0.ord == 1 assert c1.ord == 0 # .cards() returns cards in order diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index 0abb4d805..d73ba8861 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -78,7 +78,7 @@ def test_new(): # qs = ("2", "3", "2", "3") # for n in range(4): # c = col.sched.getCard() - # assert qs[n] in c.q() + # assert qs[n] in c.question() # col.sched.answerCard(c, 2) @@ -90,7 +90,7 @@ def test_newLimits(): note = col.newNote() note["Front"] = str(i) if i > 4: - note.model()["did"] = deck2 + note.note_type()["did"] = deck2 col.addNote(note) # give the child deck a different configuration c2 = col.decks.add_config_returning_id("new conf") @@ -224,17 +224,17 @@ def test_learn_collapsed(): col.reset() # should get '1' first c = col.sched.getCard() - assert c.q().endswith("1") + assert c.question().endswith("1") # pass it so it's due in 10 minutes col.sched.answerCard(c, 2) # get the other card c = col.sched.getCard() - assert c.q().endswith("2") + assert c.question().endswith("2") # fail it so it's due in 1 minute col.sched.answerCard(c, 1) # we shouldn't get the same card again c = col.sched.getCard() - assert not c.q().endswith("2") + assert not c.question().endswith("2") def test_learn_day(): @@ -316,7 +316,7 @@ def test_reviews(): c.reps = 3 c.lapses = 1 c.ivl = 100 - c.startTimer() + c.start_timer() c.flush() # save it for later use as well cardcopy = copy.copy(c) @@ -393,7 +393,7 @@ def test_button_spacing(): c.due = col.sched.today c.reps = 1 c.ivl = 1 - c.startTimer() + c.start_timer() c.flush() col.reset() ni = col.sched.nextIvlStr @@ -575,7 +575,7 @@ def test_cram(): c.due = col.sched.today + 25 c.mod = 1 c.factor = STARTING_FACTOR - c.startTimer() + c.start_timer() c.flush() col.reset() assert col.sched.counts() == (0, 0, 0) @@ -638,7 +638,7 @@ def test_cram(): assert col.sched.nextIvl(c, 2) == 600 assert col.sched.nextIvl(c, 3) == 86400 # delete the deck, returning the card mid-study - col.decks.rem(col.decks.selected()) + col.decks.remove([col.decks.selected()]) assert len(col.sched.deck_due_tree().children) == 1 c.load() assert c.ivl == 1 @@ -964,7 +964,7 @@ def test_deckDue(): # and one that's a child note = col.newNote() note["Front"] = "two" - default1 = note.model()["did"] = col.decks.id("Default::1") + default1 = note.note_type()["did"] = col.decks.id("Default::1") col.addNote(note) # make it a review card c = note.cards()[0] @@ -974,12 +974,12 @@ def test_deckDue(): # add one more with a new deck note = col.newNote() note["Front"] = "two" - note.model()["did"] = col.decks.id("foo::bar") + note.note_type()["did"] = col.decks.id("foo::bar") col.addNote(note) # and one that's a sibling note = col.newNote() note["Front"] = "three" - note.model()["did"] = col.decks.id("foo::baz") + note.note_type()["did"] = col.decks.id("foo::baz") col.addNote(note) col.reset() assert len(col.decks.all_names_and_ids()) == 5 @@ -1010,12 +1010,12 @@ def test_deckFlow(): # and one that's a child note = col.newNote() note["Front"] = "two" - note.model()["did"] = col.decks.id("Default::2") + note.note_type()["did"] = col.decks.id("Default::2") col.addNote(note) # and another that's higher up note = col.newNote() note["Front"] = "three" - default1 = note.model()["did"] = col.decks.id("Default::1") + default1 = note.note_type()["did"] = col.decks.id("Default::1") col.addNote(note) # should get top level one first, then ::1, then ::2 col.reset() @@ -1040,7 +1040,7 @@ def test_norelearn(): c.reps = 3 c.lapses = 1 c.ivl = 100 - c.startTimer() + c.start_timer() c.flush() col.reset() col.sched.answerCard(c, 1) @@ -1062,7 +1062,7 @@ def test_failmult(): c.factor = STARTING_FACTOR c.reps = 3 c.lapses = 1 - c.startTimer() + c.start_timer() c.flush() conf = col.sched._cardConf(c) conf["lapse"]["mult"] = 0.5 diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 5184e112a..b53fb6a17 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -90,7 +90,7 @@ def test_new(): # qs = ("2", "3", "2", "3") # for n in range(4): # c = col.sched.getCard() - # assert qs[n] in c.q() + # assert qs[n] in c.question() # col.sched.answerCard(c, 2) @@ -102,7 +102,7 @@ def test_newLimits(): note = col.newNote() note["Front"] = str(i) if i > 4: - note.model()["did"] = deck2 + note.note_type()["did"] = deck2 col.addNote(note) # give the child deck a different configuration c2 = col.decks.add_config_returning_id("new conf") @@ -270,17 +270,17 @@ def test_learn_collapsed(): col.reset() # should get '1' first c = col.sched.getCard() - assert c.q().endswith("1") + assert c.question().endswith("1") # pass it so it's due in 10 minutes col.sched.answerCard(c, 3) # get the other card c = col.sched.getCard() - assert c.q().endswith("2") + assert c.question().endswith("2") # fail it so it's due in 1 minute col.sched.answerCard(c, 1) # we shouldn't get the same card again c = col.sched.getCard() - assert not c.q().endswith("2") + assert not c.question().endswith("2") def test_learn_day(): @@ -374,7 +374,7 @@ def test_reviews(): c.reps = 3 c.lapses = 1 c.ivl = 100 - c.startTimer() + c.start_timer() c.flush() # save it for later use as well cardcopy = copy.copy(c) @@ -509,7 +509,7 @@ def test_button_spacing(): c.due = col.sched.today c.reps = 1 c.ivl = 1 - c.startTimer() + c.start_timer() c.flush() col.reset() ni = col.sched.nextIvlStr @@ -731,7 +731,7 @@ def test_filt_reviewing_early_normal(): c.due = col.sched.today + 25 c.mod = 1 c.factor = STARTING_FACTOR - c.startTimer() + c.start_timer() c.flush() col.reset() assert col.sched.counts() == (0, 0, 0) @@ -1082,7 +1082,7 @@ def test_deckDue(): # and one that's a child note = col.newNote() note["Front"] = "two" - default1 = note.model()["did"] = col.decks.id("Default::1") + default1 = note.note_type()["did"] = col.decks.id("Default::1") col.addNote(note) # make it a review card c = note.cards()[0] @@ -1092,12 +1092,12 @@ def test_deckDue(): # add one more with a new deck note = col.newNote() note["Front"] = "two" - note.model()["did"] = col.decks.id("foo::bar") + note.note_type()["did"] = col.decks.id("foo::bar") col.addNote(note) # and one that's a sibling note = col.newNote() note["Front"] = "three" - note.model()["did"] = col.decks.id("foo::baz") + note.note_type()["did"] = col.decks.id("foo::baz") col.addNote(note) col.reset() assert len(col.decks.all_names_and_ids()) == 5 @@ -1138,12 +1138,12 @@ def test_deckFlow(): # and one that's a child note = col.newNote() note["Front"] = "two" - note.model()["did"] = col.decks.id("Default::2") + note.note_type()["did"] = col.decks.id("Default::2") col.addNote(note) # and another that's higher up note = col.newNote() note["Front"] = "three" - default1 = note.model()["did"] = col.decks.id("Default::1") + default1 = note.note_type()["did"] = col.decks.id("Default::1") col.addNote(note) col.reset() assert col.sched.counts() == (3, 0, 0) @@ -1255,7 +1255,7 @@ def test_norelearn(): c.reps = 3 c.lapses = 1 c.ivl = 100 - c.startTimer() + c.start_timer() c.flush() col.reset() col.sched.answerCard(c, 1) @@ -1277,7 +1277,7 @@ def test_failmult(): c.factor = STARTING_FACTOR c.reps = 3 c.lapses = 1 - c.startTimer() + c.start_timer() c.flush() conf = col.sched._cardConf(c) conf["lapse"]["mult"] = 0.5 diff --git a/pylib/tests/test_template.py b/pylib/tests/test_template.py index 9af10c625..3da192de1 100644 --- a/pylib/tests/test_template.py +++ b/pylib/tests/test_template.py @@ -15,4 +15,4 @@ def test_deferred_frontside(): note["Back"] = "" col.addNote(note) - assert "xxtest" in note.cards()[0].a() + assert "xxtest" in note.cards()[0].answer() diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 0ea042be8..b833fd1bb 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -128,14 +128,14 @@ class AddCards(QDialog): if old: old_fields = list(old.keys()) new_fields = list(new.keys()) - for n, f in enumerate(new.model()["flds"]): + for n, f in enumerate(new.note_type()["flds"]): field_name = f["name"] # copy identical fields if field_name in old_fields: new[field_name] = old[field_name] - elif n < len(old.model()["flds"]): + elif n < len(old.note_type()["flds"]): # set non-identical fields by field index - old_field_name = old.model()["flds"][n]["name"] + old_field_name = old.note_type()["flds"][n]["name"] if old_field_name not in new_fields: new.fields[n] = old.fields[n] new.tags = old.tags @@ -147,7 +147,7 @@ class AddCards(QDialog): def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None: note = self._new_note() if old_note := sticky_fields_from: - flds = note.model()["flds"] + flds = note.note_type()["flds"] # copy fields from old note if old_note: for n in range(min(len(note.fields), len(old_note.fields))): diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py index 73afb8462..4d0425983 100644 --- a/qt/aqt/browser/previewer.py +++ b/qt/aqt/browser/previewer.py @@ -187,11 +187,11 @@ class Previewer(QDialog): return # need to force reload even if answer - txt = c.q(reload=True) + txt = c.question(reload=True) if self._state == "answer": func = "_showAnswer" - txt = c.a() + txt = c.answer() txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt) bodyclass = theme_manager.body_classes_for_card_ord(c.ord) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index b3a44e48e..e30b123f9 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -52,7 +52,7 @@ class CardLayout(QDialog): self.ord = ord self.col = self.mw.col.weakref() self.mm = self.mw.col.models - self.model = note.model() + self.model = note.note_type() self.templates = self.model["tmpls"] self.fill_empty_action_toggled = fill_empty self.night_mode_is_enabled = self.mw.pm.night_mode() @@ -493,11 +493,11 @@ class CardLayout(QDialog): ) if self.pform.preview_front.isChecked(): - q = ti(self.mw.prepare_card_text_for_display(c.q())) + q = ti(self.mw.prepare_card_text_for_display(c.question())) q = gui_hooks.card_will_show(q, c, "clayoutQuestion") text = q else: - a = ti(self.mw.prepare_card_text_for_display(c.a()), type="a") + a = ti(self.mw.prepare_card_text_for_display(c.answer()), type="a") a = gui_hooks.card_will_show(a, c, "clayoutAnswer") text = a diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 8a49096be..7766dd9e8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -325,7 +325,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def _onFields(self) -> None: from aqt.fields import FieldDialog - FieldDialog(self.mw, self.note.model(), parent=self.parentWindow) + FieldDialog(self.mw, self.note.note_type(), parent=self.parentWindow) def onCardLayout(self) -> None: self.call_after_note_saved(self._onCardLayout) @@ -394,7 +394,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ (type, num) = cmd.split(":", 1) ord = int(num) - model = self.note.model() + model = self.note.note_type() fld = model["flds"][ord] new_state = not fld["sticky"] fld["sticky"] = new_state @@ -463,7 +463,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ ) if self.addMode: - sticky = [field["sticky"] for field in self.note.model()["flds"]] + sticky = [field["sticky"] for field in self.note.note_type()["flds"]] js += " setSticky(%s);" % json.dumps(sticky) js = gui_hooks.editor_will_load_note(js, self.note, self) @@ -478,7 +478,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def fonts(self) -> List[Tuple[str, int, bool]]: return [ (gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) - for f in self.note.model()["flds"] + for f in self.note.note_type()["flds"] ] def call_after_note_saved( @@ -532,7 +532,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ search=( SearchNode( dupe=SearchNode.Dupe( - notetype_id=self.note.model()["id"], + notetype_id=self.note.note_type()["id"], first_field=self.note.fields[0], ) ), @@ -542,7 +542,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool: if not self.note: return True - m = self.note.model() + m = self.note.note_type() for c, f in enumerate(self.note.fields): f = f.replace("
", "").strip() notChangedvalues = {"", "
"} @@ -681,7 +681,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def _onCloze(self) -> None: # check that the model is set up for cloze deletion - if self.note.model()["type"] != MODEL_CLOZE: + if self.note.note_type()["type"] != MODEL_CLOZE: if self.addMode: tooltip(tr.editing_warning_cloze_deletions_will_not_work()) else: @@ -1280,7 +1280,7 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting) def set_cloze_button(editor: Editor) -> None: - if editor.note.model()["type"] == MODEL_CLOZE: + if editor.note.note_type()["type"] == MODEL_CLOZE: editor.web.eval( '$editorToolbar.then(({ templateButtons }) => templateButtons.showButton("cloze")); ' ) diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index 0fa7a7154..c1af62f81 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -118,7 +118,7 @@ class ImportDialog(QDialog): self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False) def modelChanged(self, unused: Any = None) -> None: - self.importer.model = self.mw.col.models.current() + self.importer.note_type = self.mw.col.models.current() self.importer.initMapping() self.showMapping() @@ -191,9 +191,9 @@ class ImportDialog(QDialog): self.importer.tagModified = self.frm.tagModified.text() self.mw.pm.profile["tagModified"] = self.importer.tagModified self.mw.col.set_aux_notetype_config( - self.importer.model["id"], "lastDeck", self.deck.selected_deck_id + self.importer.note_type["id"], "lastDeck", self.deck.selected_deck_id ) - self.mw.col.models.save(self.importer.model, updateReqs=False) + self.mw.col.models.save(self.importer.note_type, updateReqs=False) self.mw.progress.start() self.mw.checkpoint(tr.actions_import()) @@ -273,7 +273,7 @@ class ImportDialog(QDialog): qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n)) def changeMappingNum(self, n: int) -> None: - f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField() + f = ChangeMap(self.mw, self.importer.note_type, self.mapping[n]).getField() try: # make sure we don't have it twice index = self.mapping.index(f) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index f39d3f0f9..326019f3a 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -218,7 +218,7 @@ class Reviewer: if self.cardQueue: # undone/edited cards to show card = self.cardQueue.pop() - card.startTimer() + card.start_timer() self.hadCardQueue = True else: if self.hadCardQueue: @@ -236,7 +236,7 @@ class Reviewer: return self._v3 = V3CardInfo.from_queue(output) self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) - self.card.startTimer() + self.card.start_timer() def get_next_states(self) -> Optional[NextStates]: if v3 := self._v3: @@ -320,7 +320,7 @@ class Reviewer: self.typedAnswer: str = None c = self.card # grab the question and play audio - q = c.q() + q = c.question() # play audio? if c.autoplay(): AnkiWebView.setPlaybackRequiresGesture(False) @@ -370,7 +370,7 @@ class Reviewer: return self.state = "answer" c = self.card - a = c.a() + a = c.answer() # play audio? if c.autoplay(): sounds = c.answer_av_tags() @@ -535,7 +535,7 @@ class Reviewer: clozeIdx = self.card.ord + 1 fld = fld.split(":")[1] # loop through fields for a match - for f in self.card.model()["flds"]: + for f in self.card.note_type()["flds"]: if f["name"] == fld: self.typeCorrect = self.card.note()[f["name"]] if clozeIdx: @@ -733,7 +733,7 @@ time = %(time)d; editkey=tr.actions_shortcut_key(val="E"), more=tr.studying_more(), downArrow=downArrow(), - time=self.card.timeTaken() // 1000, + time=self.card.time_taken() // 1000, ) def _showAnswerButton(self) -> None: @@ -749,8 +749,8 @@ time = %(time)d; "
%s
" % middle ) - if self.card.shouldShowTimer(): - maxTime = self.card.timeLimit() / 1000 + if self.card.should_show_timer(): + maxTime = self.card.time_limit() / 1000 else: maxTime = 0 self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 64c5cac00..535fb2e69 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -7,7 +7,7 @@ mod learning; mod main; pub(crate) mod undo; -use std::{collections::VecDeque, time::Instant}; +use std::collections::VecDeque; pub(crate) use builder::{DueCard, NewCard}; pub(crate) use entry::{QueueEntry, QueueEntryKind}; @@ -205,9 +205,7 @@ impl Collection { self.discard_undo_and_study_queues(); } if self.state.card_queues.is_none() { - let now = Instant::now(); self.state.card_queues = Some(self.build_queues(deck)?); - println!("queue build in {:?}", now.elapsed()); } Ok(self.state.card_queues.as_mut().unwrap())