diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index 18cb0ca5e..981a5c5f5 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -20,6 +20,7 @@ from . import rsbridge if TYPE_CHECKING: from anki.lang import FormatTimeSpanContextValue, TRValue +# pylint: disable=c-extension-no-member assert rsbridge.buildhash() == anki.buildinfo.buildhash diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 75d566d45..55ce2b76a 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -33,7 +33,9 @@ LABEL_REQUIRED = 2 LABEL_REPEATED = 3 # messages we don't want to unroll in codegen -SKIP_UNROLL_INPUT = {"TranslateString"} +SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences"} +SKIP_UNROLL_OUTPUT = {"GetPreferences"} + SKIP_DECODE = {"Graphs", "GetGraphPreferences"} @@ -114,6 +116,7 @@ def render_method(method, idx): if ( len(method.output_type.fields) == 1 and method.output_type.fields[0].type != TYPE_ENUM + and method.name not in SKIP_UNROLL_OUTPUT ): # unwrap single return arg f = method.output_type.fields[0] diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index ec49f8bda..8cc135703 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -32,6 +32,7 @@ from anki.models import ModelManager from anki.notes import Note from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler +from anki.sync import SyncAuth, SyncOutput, SyncStatus from anki.tags import TagManager from anki.utils import ( devMode, @@ -53,6 +54,7 @@ EmptyCardsReport = _pb.EmptyCardsReport NoteWithEmptyCards = _pb.NoteWithEmptyCards GraphPreferences = _pb.GraphPreferences BuiltinSortKind = _pb.SortOrder.Builtin.Kind # pylint: disable=no-member +Preferences = _pb.Preferences # pylint: disable=no-member if TYPE_CHECKING: @@ -819,6 +821,44 @@ table.review-log {{ {revlog_style} }} intTime(), ) + ########################################################################## + + def set_wants_abort(self) -> None: + self.backend.set_wants_abort() + + def i18n_resources(self) -> bytes: + return self.backend.i18n_resources() + + def abort_media_sync(self) -> None: + self.backend.abort_media_sync() + + def abort_sync(self) -> None: + self.backend.abort_sync() + + def full_upload(self, auth: SyncAuth) -> None: + self.backend.full_upload(auth) + + def full_download(self, auth: SyncAuth) -> None: + self.backend.full_download(auth) + + def sync_login(self, username: str, password: str) -> SyncAuth: + return self.backend.sync_login(username=username, password=password) + + def sync_collection(self, auth: SyncAuth) -> SyncOutput: + return self.backend.sync_collection(auth) + + def sync_media(self, auth: SyncAuth) -> None: + self.backend.sync_media(auth) + + def sync_status(self, auth: SyncAuth) -> SyncStatus: + return self.backend.sync_status(auth) + + def get_preferences(self) -> Preferences: + return self.backend.get_preferences() + + def set_preferences(self, prefs: Preferences): + self.backend.set_preferences(prefs) + class ProgressKind(enum.Enum): NoProgress = 0 diff --git a/pylib/anki/media.py b/pylib/anki/media.py index c31789237..4721f1efb 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -17,6 +17,8 @@ import anki import anki._backend.backend_pb2 as _pb from anki.consts import * from anki.latex import render_latex, render_latex_returning_errors +from anki.sound import SoundOrVideoTag +from anki.template import av_tags_to_native from anki.utils import intTime @@ -98,6 +100,24 @@ class MediaManager: except FileNotFoundError: pass + def empty_trash(self) -> None: + self.col.backend.empty_trash() + + def restore_trash(self) -> None: + self.col.backend.restore_trash() + + def strip_av_tags(self, text: str) -> str: + return self.col.backend.strip_av_tags(text) + + def _extract_filenames(self, text: str) -> List[str]: + "This only exists do support a legacy function; do not use." + out = self.col.backend.extract_av_tags(text=text, question_side=True) + return [ + x.filename + for x in av_tags_to_native(out.av_tags) + if isinstance(x, SoundOrVideoTag) + ] + # File manipulation ########################################################################## diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index bfcd8c9a5..d07101aee 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -92,7 +92,7 @@ class TagManager: nids=nids, tags=tags, replacement=replacement, regex=regex ) - def rename_tag(self, old: str, new: str) -> int: + def rename(self, old: str, new: str) -> int: "Rename provided tag, returning number of changed notes." nids = self.col.find_notes(anki.collection.SearchTerm(tag=old)) if not nids: @@ -100,6 +100,9 @@ class TagManager: escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) return self.bulk_update(nids, escaped_name, new, False) + def remove(self, tag: str) -> None: + self.col.backend.clear_tag(tag) + # legacy routines def bulkAdd(self, ids: List[int], tags: str, add: bool = True) -> None: diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index a2403685f..50729eb12 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -10,8 +10,6 @@ from typing import List import anki import aqt -from anki.sound import SoundOrVideoTag -from anki.template import av_tags_to_native from aqt.theme import theme_manager # Routines removed from pylib/ @@ -25,21 +23,16 @@ def bodyClass(col, card) -> str: def allSounds(text) -> List: print("allSounds() deprecated") - out = aqt.mw.col.backend.extract_av_tags(text=text, question_side=True) - return [ - x.filename - for x in av_tags_to_native(out.av_tags) - if isinstance(x, SoundOrVideoTag) - ] + return aqt.mw.col.media._extract_filenames(text) def stripSounds(text) -> str: print("stripSounds() deprecated") - return aqt.mw.col.backend.strip_av_tags(text) + return aqt.mw.col.media.strip_av_tags(text) def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): - print("fmtTimeSpan() has become col.backend.format_time_span()") + print("fmtTimeSpan() has become col.format_timespan()") return aqt.mw.col.format_timespan(time) diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 79a560632..0f844a643 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -69,7 +69,7 @@ class MediaChecker: try: if self.progress_dialog.wantCancel: - self.mw.col.backend.set_wants_abort() + self.mw.col.set_wants_abort() except AttributeError: # dialog may not be active pass @@ -188,7 +188,7 @@ class MediaChecker: self._set_progress_enabled(True) def empty_trash(): - self.mw.col.backend.empty_trash() + self.mw.col.media.empty_trash() def on_done(fut: Future): self.mw.progress.finish() @@ -205,7 +205,7 @@ class MediaChecker: self._set_progress_enabled(True) def restore_trash(): - self.mw.col.backend.restore_trash() + self.mw.col.media.restore_trash() def on_done(fut: Future): self.mw.progress.finish() diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 1da2565ae..6609a5a7c 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -274,7 +274,7 @@ post_handlers = { "graphPreferences": graph_preferences, "setGraphPreferences": set_graph_preferences, # pylint: disable=unnecessary-lambda - "i18nResources": lambda: aqt.mw.col.backend.i18n_resources(), + "i18nResources": lambda: aqt.mw.col.i18n_resources(), "congratsInfo": congrats_info, } diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 3e60a3e9e..ba5cd95d4 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -64,7 +64,7 @@ class MediaSyncer: gui_hooks.media_sync_did_start_or_stop(True) def run() -> None: - self.mw.col.backend.sync_media(auth) + self.mw.col.sync_media(auth) self.mw.taskman.run_in_background(run, self._on_finished) @@ -107,8 +107,8 @@ class MediaSyncer: if not self.is_syncing(): return self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTING)) - self.mw.col.backend.set_wants_abort() - self.mw.col.backend.abort_media_sync() + self.mw.col.set_wants_abort() + self.mw.col.abort_media_sync() def is_syncing(self) -> bool: return self._syncing diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 8ed2ac384..1248696d8 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -47,7 +47,7 @@ class Preferences(QDialog): self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES) ) self.silentlyClose = True - self.prefs = self.mw.col.backend.get_preferences() + self.prefs = self.mw.col.get_preferences() self.setupLang() self.setupCollection() self.setupNetwork() @@ -114,7 +114,7 @@ class Preferences(QDialog): f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True))) - s = self.prefs + s = self.prefs.sched f.lrnCutoff.setValue(int(s.learn_ahead_secs / 60.0)) f.timeLimit.setValue(int(s.time_limit_secs / 60.0)) f.showEstimates.setChecked(s.show_intervals_on_buttons) @@ -156,7 +156,7 @@ class Preferences(QDialog): qc = d.conf qc["addToCur"] = not f.useCurrent.currentIndex() - s = self.prefs + s = self.prefs.sched s.show_remaining_due_counts = f.showProgress.isChecked() s.show_intervals_on_buttons = f.showEstimates.isChecked() s.new_review_mix = f.newSpread.currentIndex() @@ -168,7 +168,7 @@ class Preferences(QDialog): # if moving this, make sure scheduler change is moved to Rust or # happens afterwards - self.mw.col.backend.set_preferences(self.prefs) + self.mw.col.set_preferences(self.prefs) self._updateSchedVer(f.newSched.isChecked()) d.setMod() diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index c1c3749ad..d3c3e1ee5 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -691,8 +691,8 @@ class SidebarTreeView(QTreeView): old_name = item.full_name def do_remove(): - self.mw.col.backend.clear_tag(old_name) - self.col.tags.rename_tag(old_name, "") + self.mw.col.tags.remove(old_name) + self.col.tags.rename(old_name, "") def on_done(fut: Future): self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self) @@ -714,8 +714,8 @@ class SidebarTreeView(QTreeView): return def do_rename(): - self.mw.col.backend.clear_tag(old_name) - return self.col.tags.rename_tag(old_name, new_name) + self.mw.col.tags.remove(old_name) + return self.col.tags.rename(old_name, new_name) def on_done(fut: Future): self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index c71ae132c..695fe8d02 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -54,9 +54,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]) return callback(out) - mw.taskman.run_in_background( - lambda: mw.col.backend.sync_status(auth), on_future_done - ) + mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done) def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception): @@ -82,7 +80,7 @@ def on_normal_sync_timer(mw: aqt.main.AnkiQt) -> None: mw.progress.set_title(progress.val.stage) if mw.progress.want_cancel(): - mw.col.backend.abort_sync() + mw.col.abort_sync() def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: @@ -116,7 +114,7 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: mw.col.save(trx=False) mw.taskman.with_progress( - lambda: mw.col.backend.sync_collection(auth), + lambda: mw.col.sync_collection(auth), on_future_done, label=tr(TR.SYNC_CHECKING), immediate=True, @@ -167,7 +165,7 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None: ) if mw.progress.want_cancel(): - mw.col.backend.abort_sync() + mw.col.abort_sync() def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: @@ -192,7 +190,7 @@ def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: return on_done() mw.taskman.with_progress( - lambda: mw.col.backend.full_download(mw.pm.sync_auth()), + lambda: mw.col.full_download(mw.pm.sync_auth()), on_future_done, label=tr(TR.SYNC_DOWNLOADING_FROM_ANKIWEB), ) @@ -221,7 +219,7 @@ def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: return on_done() mw.taskman.with_progress( - lambda: mw.col.backend.full_upload(mw.pm.sync_auth()), + lambda: mw.col.full_upload(mw.pm.sync_auth()), on_future_done, label=tr(TR.SYNC_UPLOADING_TO_ANKIWEB), ) @@ -258,7 +256,7 @@ def sync_login( on_success() mw.taskman.with_progress( - lambda: mw.col.backend.sync_login(username=username, password=password), + lambda: mw.col.sync_login(username=username, password=password), on_future_done, )