diff --git a/pylib/.isort.cfg b/pylib/.isort.cfg index e3baa20af..2c1fbb53b 100644 --- a/pylib/.isort.cfg +++ b/pylib/.isort.cfg @@ -1,5 +1,5 @@ [settings] -skip=aqt/forms,backend_pb2.py,backend_pb2.pyi,fluent_pb2.py,fluent_pb2.pyi,rsbackend_gen.py,hooks_gen.py,genbackend.py +skip=aqt/forms,backend_pb2.py,backend_pb2.pyi,fluent_pb2.py,fluent_pb2.pyi,rsbackend_gen.py,generated.py,hooks_gen.py,genbackend.py profile=black multi_line_output=3 include_trailing_comma=True diff --git a/pylib/anki/BUILD.bazel b/pylib/anki/BUILD.bazel index 58df9805a..cb9304ea4 100644 --- a/pylib/anki/BUILD.bazel +++ b/pylib/anki/BUILD.bazel @@ -1,7 +1,6 @@ load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@rules_python//python:defs.bzl", "py_library") load("@py_deps//:requirements.bzl", "requirement") -load("//pylib:protobuf.bzl", "py_proto_library_typed") load("@rules_python//experimental/python:wheel.bzl", "py_package", "py_wheel") load("@bazel_skylib//lib:selects.bzl", "selects") load("//:defs.bzl", "anki_version") @@ -13,13 +12,6 @@ copy_file( out = "buildinfo.txt", ) -genrule( - name = "rsbackend_gen", - outs = ["rsbackend_gen.py"], - cmd = "$(location //pylib/tools:genbackend) > $@", - tools = ["//pylib/tools:genbackend"], -) - genrule( name = "hooks_gen", outs = ["hooks_gen.py"], @@ -27,22 +19,6 @@ genrule( tools = ["//pylib/tools:genhooks"], ) -py_proto_library_typed( - name = "backend_pb2", - src = "//rslib:backend.proto", - visibility = [ - "//visibility:public", - ], -) - -py_proto_library_typed( - name = "fluent_pb2", - src = "//rslib:fluent.proto", - visibility = [ - "//visibility:public", - ], -) - copy_file( name = "rsbridge_unix", src = "//pylib/rsbridge", @@ -69,7 +45,6 @@ alias( _py_srcs = glob( ["**/*.py"], exclude = [ - "rsbackend_gen.py", "hooks_gen.py", ], ) @@ -79,12 +54,10 @@ py_library( srcs = _py_srcs, data = [ "py.typed", - ":backend_pb2", ":buildinfo", - ":fluent_pb2", ":hooks_gen", - ":rsbackend_gen", ":rsbridge", + "//pylib/anki/_backend", ], imports = [ "..", diff --git a/pylib/anki/_backend/BUILD.bazel b/pylib/anki/_backend/BUILD.bazel new file mode 100644 index 000000000..02391c8e7 --- /dev/null +++ b/pylib/anki/_backend/BUILD.bazel @@ -0,0 +1,50 @@ +load("@rules_python//python:defs.bzl", "py_binary") +load("@py_deps//:requirements.bzl", "requirement") +load("//pylib:protobuf.bzl", "py_proto_library_typed") + +py_proto_library_typed( + name = "backend_pb2", + src = "//rslib:backend.proto", + visibility = [ + "//visibility:public", + ], +) + +py_proto_library_typed( + name = "fluent_pb2", + src = "//rslib:fluent.proto", + visibility = [ + "//visibility:public", + ], +) + +py_binary( + name = "genbackend", + srcs = [ + "backend_pb2", + "genbackend.py", + ], + deps = [ + requirement("black"), + requirement("stringcase"), + requirement("protobuf"), + ], +) + +genrule( + name = "rsbackend_gen", + outs = ["generated.py"], + cmd = "$(location genbackend) > $@", + tools = ["genbackend"], +) + +filegroup( + name = "_backend", + srcs = [ + "__init__.py", + ":backend_pb2", + ":fluent_pb2", + ":rsbackend_gen", + ], + visibility = ["//pylib:__subpackages__"], +) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/_backend/__init__.py similarity index 52% rename from pylib/anki/rsbackend.py rename to pylib/anki/_backend/__init__.py index aa1a886cd..9ed58cece 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/_backend/__init__.py @@ -22,19 +22,19 @@ import os from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union +import anki._backend.backend_pb2 as pb import anki._rsbridge -import anki.backend_pb2 as pb import anki.buildinfo from anki import hooks +from anki._backend.generated import RustBackendGenerated from anki.dbproxy import Row as DBRow from anki.dbproxy import ValueForDB -from anki.fluent_pb2 import FluentString as TR -from anki.rsbackend_gen import RustBackendGenerated +from anki.errors import backend_exception_to_pylib +from anki.lang import FormatTimeSpanContext +from anki.utils import from_json_bytes, to_json_bytes if TYPE_CHECKING: - from anki.fluent_pb2 import FluentStringValue as TRValue - - FormatTimeSpanContextValue = pb.FormatTimespanIn.ContextValue + from anki.lang import FormatTimeSpanContextValue, TRValue assert anki._rsbridge.buildhash() == anki.buildinfo.buildhash @@ -48,151 +48,10 @@ BackendNote = pb.Note Tag = pb.Tag TagTreeNode = pb.TagTreeNode NoteType = pb.NoteType -DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType ConcatSeparator = pb.ConcatenateSearchesIn.Separator -SyncAuth = pb.SyncAuth -SyncOutput = pb.SyncCollectionOut -SyncStatus = pb.SyncStatusOut CountsForDeckToday = pb.CountsForDeckTodayOut -try: - import orjson - - to_json_bytes = orjson.dumps - from_json_bytes = orjson.loads -except: - print("orjson is missing; DB operations will be slower") - to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore - from_json_bytes = json.loads - - -class Interrupted(Exception): - pass - - -class StringError(Exception): - def __str__(self) -> str: - return self.args[0] # pylint: disable=unsubscriptable-object - - -NetworkErrorKind = pb.NetworkError.NetworkErrorKind -SyncErrorKind = pb.SyncError.SyncErrorKind - - -class NetworkError(StringError): - def kind(self) -> pb.NetworkError.NetworkErrorKindValue: - return self.args[1] - - -class SyncError(StringError): - def kind(self) -> pb.SyncError.SyncErrorKindValue: - return self.args[1] - - -class IOError(StringError): - pass - - -class DBError(StringError): - pass - - -class TemplateError(StringError): - pass - - -class NotFoundError(Exception): - pass - - -class ExistsError(Exception): - pass - - -class DeckIsFilteredError(Exception): - pass - - -class InvalidInput(StringError): - pass - - -def proto_exception_to_native(err: pb.BackendError) -> Exception: - val = err.WhichOneof("value") - if val == "interrupted": - return Interrupted() - elif val == "network_error": - return NetworkError(err.localized, err.network_error.kind) - elif val == "sync_error": - return SyncError(err.localized, err.sync_error.kind) - elif val == "io_error": - return IOError(err.localized) - elif val == "db_error": - return DBError(err.localized) - elif val == "template_parse": - return TemplateError(err.localized) - elif val == "invalid_input": - return InvalidInput(err.localized) - elif val == "json_error": - return StringError(err.localized) - elif val == "not_found_error": - return NotFoundError() - elif val == "exists": - return ExistsError() - elif val == "deck_is_filtered": - return DeckIsFilteredError() - elif val == "proto_error": - return StringError(err.localized) - else: - print("unhandled error type:", val) - return StringError(err.localized) - - -MediaSyncProgress = pb.MediaSyncProgress -FullSyncProgress = pb.FullSyncProgress -NormalSyncProgress = pb.NormalSyncProgress -DatabaseCheckProgress = pb.DatabaseCheckProgress - -FormatTimeSpanContext = pb.FormatTimespanIn.Context - - -class ProgressKind(enum.Enum): - NoProgress = 0 - MediaSync = 1 - MediaCheck = 2 - FullSync = 3 - NormalSync = 4 - DatabaseCheck = 5 - - -@dataclass -class Progress: - kind: ProgressKind - val: Union[ - MediaSyncProgress, - pb.FullSyncProgress, - NormalSyncProgress, - DatabaseCheckProgress, - str, - ] - - @staticmethod - def from_proto(proto: pb.Progress) -> Progress: - kind = proto.WhichOneof("value") - if kind == "media_sync": - return Progress(kind=ProgressKind.MediaSync, val=proto.media_sync) - elif kind == "media_check": - return Progress(kind=ProgressKind.MediaCheck, val=proto.media_check) - elif kind == "full_sync": - return Progress(kind=ProgressKind.FullSync, val=proto.full_sync) - elif kind == "normal_sync": - return Progress(kind=ProgressKind.NormalSync, val=proto.normal_sync) - elif kind == "database_check": - return Progress(kind=ProgressKind.DatabaseCheck, val=proto.database_check) - else: - return Progress(kind=ProgressKind.NoProgress, val="") - class RustBackend(RustBackendGenerated): def __init__( @@ -240,7 +99,7 @@ class RustBackend(RustBackendGenerated): err_bytes = bytes(e.args[0]) err = pb.BackendError() err.ParseFromString(err_bytes) - raise proto_exception_to_native(err) + raise backend_exception_to_pylib(err) def translate(self, key: TRValue, **kwargs: Union[str, int, float]) -> str: return self.translate_string(translate_string_in(key, **kwargs)) @@ -263,7 +122,7 @@ class RustBackend(RustBackendGenerated): err_bytes = bytes(e.args[0]) err = pb.BackendError() err.ParseFromString(err_bytes) - raise proto_exception_to_native(err) + raise backend_exception_to_pylib(err) def translate_string_in( diff --git a/pylib/anki/_backend/backend_pb2.pyi b/pylib/anki/_backend/backend_pb2.pyi new file mode 120000 index 000000000..fb507905b --- /dev/null +++ b/pylib/anki/_backend/backend_pb2.pyi @@ -0,0 +1 @@ +../../../bazel-bin/pylib/anki/_backend/backend_pb2.pyi \ No newline at end of file diff --git a/pylib/anki/_backend/fluent_pb2.pyi b/pylib/anki/_backend/fluent_pb2.pyi new file mode 120000 index 000000000..a2c47c62e --- /dev/null +++ b/pylib/anki/_backend/fluent_pb2.pyi @@ -0,0 +1 @@ +../../../bazel-bin/pylib/anki/_backend/fluent_pb2.pyi \ No newline at end of file diff --git a/pylib/tools/genbackend.py b/pylib/anki/_backend/genbackend.py similarity index 98% rename from pylib/tools/genbackend.py rename to pylib/anki/_backend/genbackend.py index 3074bde63..75d566d45 100755 --- a/pylib/tools/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -5,7 +5,7 @@ import os import re import sys -import pylib.anki.backend_pb2 as pb +import pylib.anki._backend.backend_pb2 as pb import stringcase @@ -168,7 +168,7 @@ col.decks.all_config() from typing import * -import anki.backend_pb2 as pb +import anki._backend.backend_pb2 as pb class RustBackendGenerated: def _run_command(self, method: int, input: Any) -> bytes: diff --git a/pylib/anki/_backend/generated.py b/pylib/anki/_backend/generated.py new file mode 120000 index 000000000..922921371 --- /dev/null +++ b/pylib/anki/_backend/generated.py @@ -0,0 +1 @@ +../../../bazel-bin/pylib/anki/_backend/generated.py \ No newline at end of file diff --git a/pylib/anki/backend_pb2.pyi b/pylib/anki/backend_pb2.pyi deleted file mode 120000 index d3937d76d..000000000 --- a/pylib/anki/backend_pb2.pyi +++ /dev/null @@ -1 +0,0 @@ -../../bazel-bin/pylib/anki/backend_pb2.pyi \ No newline at end of file diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 324b0bd3e..9feb923ea 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -8,11 +8,11 @@ import time from typing import List, Optional import anki # pylint: disable=unused-import +import anki._backend.backend_pb2 as _pb from anki import hooks from anki.consts import * from anki.models import NoteType, Template from anki.notes import Note -from anki.rsbackend import BackendCard from anki.sound import AVTag # Cards @@ -45,14 +45,14 @@ class Card: self.load() else: # new card with defaults - self._load_from_backend_card(BackendCard()) + 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) - def _load_from_backend_card(self, c: BackendCard) -> None: + def _load_from_backend_card(self, c: _pb.Card) -> None: self._render_output = None self._note = None self.id = c.id @@ -86,7 +86,7 @@ class Card: self._bugcheck() hooks.card_will_flush(self) # mtime & usn are set by backend - card = BackendCard( + card = _pb.Card( id=self.id, note_id=self.nid, deck_id=self.did, diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 5ea9a876c..5666184e2 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -4,51 +4,66 @@ from __future__ import annotations import copy +import enum import os import pprint import re import time import traceback import weakref +from dataclasses import dataclass from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Union -import anki.backend_pb2 as pb +import anki._backend.backend_pb2 as _pb import anki.find import anki.latex # sets up hook import anki.template from anki import hooks -from anki.backend_pb2 import SearchTerm +from anki._backend import ( # pylint: disable=unused-import + ConcatSeparator, + FormatTimeSpanContext, + RustBackend, +) + +# from anki._backend import _SyncStatus as SyncStatus from anki.cards import Card from anki.config import ConfigManager from anki.consts import * from anki.dbproxy import DBProxy from anki.decks import DeckManager -from anki.errors import AnkiError +from anki.errors import AnkiError, DBError +from anki.lang import TR from anki.media import MediaManager, media_paths_from_col_path from anki.models import ModelManager from anki.notes import Note -from anki.rsbackend import ( # pylint: disable=unused-import - TR, - ConcatSeparator, - DBError, - FormatTimeSpanContext, - InvalidInput, - Progress, - RustBackend, - from_json_bytes, - pb, -) from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.tags import TagManager -from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia +from anki.utils import ( + devMode, + from_json_bytes, + ids2str, + intTime, + splitFields, + stripHTMLMedia, +) -ConfigBoolKey = pb.ConfigBool.Key # pylint: disable=no-member +# public exports +SearchTerm = _pb.SearchTerm +MediaSyncProgress = _pb.MediaSyncProgress +FullSyncProgress = _pb.FullSyncProgress +NormalSyncProgress = _pb.NormalSyncProgress +DatabaseCheckProgress = _pb.DatabaseCheckProgress +ConfigBoolKey = _pb.ConfigBool.Key # pylint: disable=no-member +EmptyCardsReport = _pb.EmptyCardsReport +NoteWithEmptyCards = _pb.NoteWithEmptyCards +GraphPreferences = _pb.GraphPreferences +# pylint: disable=no-member if TYPE_CHECKING: - from anki.rsbackend import FormatTimeSpanContextValue, TRValue + from anki.lang import FormatTimeSpanContextValue, TRValue - ConfigBoolKeyValue = pb.ConfigBool.KeyValue # pylint: disable=no-member + ConfigBoolKeyValue = _pb.ConfigBool.KeyValue class Collection: @@ -394,6 +409,9 @@ class Collection: def set_deck(self, card_ids: List[int], deck_id: int) -> None: self.backend.set_deck(card_ids=card_ids, deck_id=deck_id) + def get_empty_cards(self) -> EmptyCardsReport: + return self.backend.get_empty_cards() + # legacy def remCards(self, ids: List[int], notes: bool = True) -> None: @@ -445,20 +463,20 @@ class Collection: order: Union[ bool, str, - pb.BuiltinSearchOrder.BuiltinSortKindValue, # pylint: disable=no-member + _pb.BuiltinSearchOrder.BuiltinSortKindValue, # pylint: disable=no-member ] = False, reverse: bool = False, ) -> Sequence[int]: if isinstance(order, str): - mode = pb.SortOrder(custom=order) + mode = _pb.SortOrder(custom=order) elif isinstance(order, bool): if order is True: - mode = pb.SortOrder(from_config=pb.Empty()) + mode = _pb.SortOrder(from_config=_pb.Empty()) else: - mode = pb.SortOrder(none=pb.Empty()) + mode = _pb.SortOrder(none=_pb.Empty()) else: - mode = pb.SortOrder( - builtin=pb.BuiltinSearchOrder(kind=order, reverse=reverse) + mode = _pb.SortOrder( + builtin=_pb.BuiltinSearchOrder(kind=order, reverse=reverse) ) return self.backend.search_cards(search=query, order=mode) @@ -603,6 +621,19 @@ table.review-log {{ {revlog_style} }} def studied_today(self) -> str: return self.backend.studied_today() + def graph_data(self, search: str, days: int) -> bytes: + return self.backend.graphs(search=search, days=days) + + def get_graph_preferences(self) -> bytes: + return self.backend.get_graph_preferences() + + def set_graph_preferences(self, prefs: GraphPreferences) -> None: + self.backend.set_graph_preferences(input=prefs) + + def congrats_info(self) -> bytes: + "Don't use this, it will likely go away in the future." + return self.backend.congrats_info().SerializeToString() + # legacy def cardStats(self, card: Card) -> str: @@ -797,5 +828,42 @@ table.review-log {{ {revlog_style} }} ) +class ProgressKind(enum.Enum): + NoProgress = 0 + MediaSync = 1 + MediaCheck = 2 + FullSync = 3 + NormalSync = 4 + DatabaseCheck = 5 + + +@dataclass +class Progress: + kind: ProgressKind + val: Union[ + MediaSyncProgress, + FullSyncProgress, + NormalSyncProgress, + DatabaseCheckProgress, + str, + ] + + @staticmethod + def from_proto(proto: _pb.Progress) -> Progress: + kind = proto.WhichOneof("value") + if kind == "media_sync": + return Progress(kind=ProgressKind.MediaSync, val=proto.media_sync) + elif kind == "media_check": + return Progress(kind=ProgressKind.MediaCheck, val=proto.media_check) + elif kind == "full_sync": + return Progress(kind=ProgressKind.FullSync, val=proto.full_sync) + elif kind == "normal_sync": + return Progress(kind=ProgressKind.NormalSync, val=proto.normal_sync) + elif kind == "database_check": + return Progress(kind=ProgressKind.DatabaseCheck, val=proto.database_check) + else: + return Progress(kind=ProgressKind.NoProgress, val="") + + # legacy name _Collection = Collection diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 55cfcf585..bad5a804f 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -23,7 +23,8 @@ import weakref from typing import Any import anki -from anki.rsbackend import NotFoundError, from_json_bytes, to_json_bytes +from anki.errors import NotFoundError +from anki.utils import from_json_bytes, to_json_bytes class ConfigManager: diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index 6ff036b1e..18c134ea2 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import Any, Dict, Optional import anki -from anki.rsbackend import TR +from anki.lang import TR # whether new cards should be mixed with reviews, or shown first or last NEW_CARDS_DISTRIBUTE = 0 diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 1a9c024ff..7ec126e04 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -21,7 +21,7 @@ class DBProxy: # Lifecycle ############### - def __init__(self, backend: anki.rsbackend.RustBackend) -> None: + def __init__(self, backend: anki._backend.RustBackend) -> None: self._backend = backend self.mod = False self.last_begin_at = 0 diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index d3acfe4dc..d50c4b611 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -8,17 +8,14 @@ import pprint from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import -import anki.backend_pb2 as pb +import anki._backend.backend_pb2 as _pb from anki.consts import * -from anki.errors import DeckRenameError -from anki.rsbackend import ( - TR, - DeckTreeNode, - NotFoundError, - from_json_bytes, - to_json_bytes, -) -from anki.utils import ids2str, intTime +from anki.errors import DeckIsFilteredError, DeckRenameError, NotFoundError +from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes + +# public exports +DeckTreeNode = _pb.DeckTreeNode +DeckNameID = _pb.DeckNameID # legacy code may pass this in as the type argument to .id() defaultDeck = 0 @@ -139,7 +136,7 @@ class DeckManager: def all_names_and_ids( self, skip_empty_default=False, include_filtered=True - ) -> Sequence[pb.DeckNameID]: + ) -> Sequence[DeckNameID]: "A sorted sequence of deck names and IDs." return self.col.backend.get_deck_names( skip_empty_default=skip_empty_default, include_filtered=include_filtered @@ -166,7 +163,7 @@ class DeckManager: def new_deck_legacy(self, filtered: bool) -> Deck: return from_json_bytes(self.col.backend.new_deck_legacy(filtered)) - def deck_tree(self) -> pb.DeckTreeNode: + def deck_tree(self) -> DeckTreeNode: return self.col.backend.deck_tree(top_deck_id=0, now=0) @classmethod @@ -250,7 +247,7 @@ class DeckManager: g["id"] = self.col.backend.add_or_update_deck_legacy( deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn ) - except anki.rsbackend.DeckIsFilteredError as exc: + except DeckIsFilteredError as exc: raise DeckRenameError("deck was filtered") from exc def rename(self, g: Deck, newName: str) -> None: diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index 4bfc9aa24..bed39fab0 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -1,8 +1,92 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + from typing import Any +import anki._backend.backend_pb2 as _pb + +# fixme: notfounderror etc need to be in rsbackend.py + + +class StringError(Exception): + def __str__(self) -> str: + return self.args[0] # pylint: disable=unsubscriptable-object + + +class Interrupted(Exception): + pass + + +class NetworkError(StringError): + pass + + +class SyncError(StringError): + # pylint: disable=no-member + def is_auth_error(self) -> bool: + return self.args[1] == _pb.SyncError.SyncErrorKind.AUTH_FAILED + + +class IOError(StringError): + pass + + +class DBError(StringError): + pass + + +class TemplateError(StringError): + pass + + +class NotFoundError(Exception): + pass + + +class ExistsError(Exception): + pass + + +class DeckIsFilteredError(Exception): + pass + + +class InvalidInput(StringError): + pass + + +def backend_exception_to_pylib(err: _pb.BackendError) -> Exception: + val = err.WhichOneof("value") + if val == "interrupted": + return Interrupted() + elif val == "network_error": + return NetworkError(err.localized, err.network_error.kind) + elif val == "sync_error": + return SyncError(err.localized, err.sync_error.kind) + elif val == "io_error": + return IOError(err.localized) + elif val == "db_error": + return DBError(err.localized) + elif val == "template_parse": + return TemplateError(err.localized) + elif val == "invalid_input": + return InvalidInput(err.localized) + elif val == "json_error": + return StringError(err.localized) + elif val == "not_found_error": + return NotFoundError() + elif val == "exists": + return ExistsError() + elif val == "deck_is_filtered": + return DeckIsFilteredError() + elif val == "proto_error": + return StringError(err.localized) + else: + print("unhandled error type:", val) + return StringError(err.localized) + class AnkiError(Exception): def __init__(self, type, **data) -> None: diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 1d2fd4a13..13c66cdda 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -13,7 +13,7 @@ from zipfile import ZipFile from anki import hooks from anki.collection import Collection -from anki.rsbackend import TR +from anki.lang import TR from anki.utils import ids2str, namedtmp, splitFields, stripHTML diff --git a/pylib/anki/fluent_pb2.pyi b/pylib/anki/fluent_pb2.pyi deleted file mode 120000 index a8cb14b62..000000000 --- a/pylib/anki/fluent_pb2.pyi +++ /dev/null @@ -1 +0,0 @@ -../../bazel-bin/pylib/anki/fluent_pb2.pyi \ No newline at end of file diff --git a/pylib/anki/importing/__init__.py b/pylib/anki/importing/__init__.py index 03e645c30..ec7e05080 100644 --- a/pylib/anki/importing/__init__.py +++ b/pylib/anki/importing/__init__.py @@ -7,8 +7,7 @@ from anki.importing.csvfile import TextImporter from anki.importing.mnemo import MnemosyneImporter from anki.importing.pauker import PaukerImporter from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore -from anki.lang import tr_legacyglobal -from anki.rsbackend import TR +from anki.lang import TR, tr_legacyglobal Importers = ( (tr_legacyglobal(TR.IMPORTING_TEXT_SEPARATED_BY_TABS_OR_SEMICOLONS), TextImporter), diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 6eb6a9c59..5c1bbec6b 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -9,7 +9,7 @@ from anki.collection import Collection from anki.consts import * from anki.decks import DeckManager from anki.importing.base import Importer -from anki.rsbackend import TR +from anki.lang import TR from anki.utils import intTime, joinFields, splitFields GUID = 1 diff --git a/pylib/anki/importing/csvfile.py b/pylib/anki/importing/csvfile.py index eeee98d46..081553f18 100644 --- a/pylib/anki/importing/csvfile.py +++ b/pylib/anki/importing/csvfile.py @@ -7,7 +7,7 @@ from typing import Any, List, Optional, TextIO, Union from anki.collection import Collection from anki.importing.noteimp import ForeignNote, NoteImporter -from anki.rsbackend import TR +from anki.lang import TR class TextImporter(NoteImporter): diff --git a/pylib/anki/importing/mnemo.py b/pylib/anki/importing/mnemo.py index ed798ecab..f3cafedbd 100644 --- a/pylib/anki/importing/mnemo.py +++ b/pylib/anki/importing/mnemo.py @@ -7,7 +7,7 @@ from typing import cast from anki.db import DB from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter -from anki.rsbackend import TR +from anki.lang import TR from anki.stdmodels import addBasicModel, addClozeModel diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index d90c18dfb..e3617330b 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple, Union from anki.collection import Collection from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR from anki.importing.base import Importer -from anki.rsbackend import TR +from anki.lang import TR from anki.utils import ( fieldChecksum, guid64, diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index 54dc5e06c..2cc2eb8ed 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -5,9 +5,20 @@ from __future__ import annotations import locale import re -from typing import Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple import anki +import anki._backend.backend_pb2 as _pb +import anki._backend.fluent_pb2 as _fluent_pb + +# public exports +TR = _fluent_pb.FluentString +FormatTimeSpanContext = _pb.FormatTimespanIn.Context # pylint: disable=no-member + +# pylint: disable=no-member +if TYPE_CHECKING: + TRValue = _fluent_pb.FluentStringValue + FormatTimeSpanContextValue = _pb.FormatTimespanIn.ContextValue langs = sorted( [ @@ -142,7 +153,7 @@ def lang_to_disk_lang(lang: str) -> str: currentLang = "en" # the current Fluent translation instance -current_i18n: Optional[anki.rsbackend.RustBackend] = None +current_i18n: Optional[anki._backend.RustBackend] = None # path to locale folder locale_folder = "" @@ -169,7 +180,7 @@ def tr_legacyglobal(*args, **kwargs) -> str: def set_lang(lang: str, locale_dir: str) -> None: global currentLang, current_i18n, locale_folder currentLang = lang - current_i18n = anki.rsbackend.RustBackend(ftl_folder=locale_folder, langs=[lang]) + current_i18n = anki._backend.RustBackend(ftl_folder=locale_folder, langs=[lang]) locale_folder = locale_dir diff --git a/pylib/anki/latex.py b/pylib/anki/latex.py index 782502eb9..4fb5138e8 100644 --- a/pylib/anki/latex.py +++ b/pylib/anki/latex.py @@ -10,9 +10,10 @@ from dataclasses import dataclass from typing import Any, List, Optional, Tuple import anki +import anki._backend.backend_pb2 as _pb from anki import hooks +from anki.lang import TR from anki.models import NoteType -from anki.rsbackend import TR, pb from anki.template import TemplateRenderContext, TemplateRenderOutput from anki.utils import call, isMac, namedtmp, tmpdir @@ -45,7 +46,7 @@ class ExtractedLatexOutput: latex: List[ExtractedLatex] @staticmethod - def from_proto(proto: pb.ExtractLatexOut) -> ExtractedLatexOutput: + def from_proto(proto: _pb.ExtractLatexOut) -> ExtractedLatexOutput: return ExtractedLatexOutput( html=proto.text, latex=[ diff --git a/pylib/anki/media.py b/pylib/anki/media.py index 2d9a2478e..c31789237 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -14,9 +14,9 @@ import urllib.request from typing import Any, Callable, List, Optional, Tuple 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.rsbackend import pb from anki.utils import intTime @@ -26,6 +26,9 @@ def media_paths_from_col_path(col_path: str) -> Tuple[str, str]: return (media_folder, media_db) +CheckMediaOut = _pb.CheckMediaOut + + # fixme: look into whether we can drop chdir() below # - need to check aa89d06304fecd3597da4565330a3e55bdbb91fe # - and audio handling code @@ -188,7 +191,7 @@ class MediaManager: # Checking media ########################################################################## - def check(self) -> pb.CheckMediaOut: + def check(self) -> CheckMediaOut: output = self.col.backend.check_media() # files may have been renamed on disk, so an undo at this point could # break file references diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 5f8351246..2a8e29b26 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -9,17 +9,24 @@ import time from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import -import anki.backend_pb2 as pb +import anki._backend.backend_pb2 as _pb from anki.consts import * -from anki.lang import without_unicode_isolation -from anki.rsbackend import ( - TR, - NotFoundError, - StockNoteType, +from anki.errors import NotFoundError +from anki.lang import TR, without_unicode_isolation +from anki.utils import ( + checksum, from_json_bytes, + ids2str, + intTime, + joinFields, + splitFields, to_json_bytes, ) -from anki.utils import checksum, ids2str, intTime, joinFields, splitFields + +# public exports +NoteTypeNameID = _pb.NoteTypeNameID +NoteTypeNameIDUseCount = _pb.NoteTypeNameIDUseCount + # types NoteType = Dict[str, Any] @@ -121,10 +128,10 @@ class ModelManager: # Listing note types ############################################################# - def all_names_and_ids(self) -> Sequence[pb.NoteTypeNameID]: + def all_names_and_ids(self) -> Sequence[NoteTypeNameID]: return self.col.backend.get_notetype_names() - def all_use_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]: + def all_use_counts(self) -> Sequence[NoteTypeNameIDUseCount]: return self.col.backend.get_notetype_names_and_counts() # legacy @@ -200,7 +207,7 @@ class ModelManager: # caller should call save() after modifying nt = from_json_bytes( self.col.backend.get_stock_notetype_legacy( - StockNoteType.STOCK_NOTE_TYPE_BASIC + _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC ) ) nt["flds"] = [] @@ -293,7 +300,7 @@ class ModelManager: assert isinstance(name, str) nt = from_json_bytes( self.col.backend.get_stock_notetype_legacy( - StockNoteType.STOCK_NOTE_TYPE_BASIC + _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC ) ) field = nt["flds"][0] @@ -354,7 +361,7 @@ class ModelManager: def new_template(self, name: str) -> Template: nt = from_json_bytes( self.col.backend.get_stock_notetype_legacy( - StockNoteType.STOCK_NOTE_TYPE_BASIC + _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC ) ) template = nt["tmpls"][0] @@ -508,5 +515,5 @@ and notes.mid = ? and cards.ord = ?""", self, m: NoteType, flds: str, allowEmpty: bool = True ) -> List[int]: print("_availClozeOrds() is deprecated; use note.cloze_numbers_in_fields()") - note = anki.rsbackend.BackendNote(fields=[flds]) + note = anki._backend.BackendNote(fields=[flds]) return list(self.col.backend.cloze_numbers_in_note(note)) diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 80d86b6cf..0cf0ff3f3 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -7,9 +7,9 @@ import pprint from typing import Any, List, Optional, Sequence, Tuple import anki # pylint: disable=unused-import +import anki._backend.backend_pb2 as _pb from anki import hooks from anki.models import NoteType -from anki.rsbackend import BackendNote from anki.utils import joinFields @@ -41,7 +41,7 @@ class Note: assert n self._load_from_backend_note(n) - def _load_from_backend_note(self, n: BackendNote) -> None: + def _load_from_backend_note(self, n: _pb.Note) -> None: self.id = n.id self.guid = n.guid self.mid = n.notetype_id @@ -51,9 +51,9 @@ class Note: self.fields = list(n.fields) self._fmap = self.col.models.fieldMap(self.model()) - def to_backend_note(self) -> BackendNote: + def to_backend_note(self) -> _pb.Note: hooks.note_will_flush(self) - return BackendNote( + return _pb.Note( id=self.id, guid=self.guid, notetype_id=self.mid, diff --git a/pylib/anki/rsbackend_gen.py b/pylib/anki/rsbackend_gen.py deleted file mode 120000 index b247575fd..000000000 --- a/pylib/anki/rsbackend_gen.py +++ /dev/null @@ -1 +0,0 @@ -../../bazel-bin/pylib/anki/rsbackend_gen.py \ No newline at end of file diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 86c5073cd..d9a0a21dc 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -20,30 +20,25 @@ from typing import ( ) import anki # pylint: disable=unused-import -import anki.backend_pb2 as pb +import anki._backend.backend_pb2 as _pb from anki import hooks +from anki._backend import CountsForDeckToday, FormatTimeSpanContext, SchedTimingToday from anki.cards import Card from anki.consts import * -from anki.decks import Deck, DeckConfig, DeckManager, QueueConfig +from anki.decks import Deck, DeckConfig, DeckManager, DeckTreeNode, QueueConfig from anki.notes import Note -from anki.rsbackend import ( - TR, - CountsForDeckToday, - DeckTreeNode, - FormatTimeSpanContext, - SchedTimingToday, - from_json_bytes, -) -from anki.utils import ids2str, intTime +from anki.utils import from_json_bytes, ids2str, intTime -UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint:disable=no-member -BuryOrSuspendMode = pb.BuryOrSuspendCardsIn.Mode # pylint:disable=no-member +CongratsInfoOut = anki._backend.backend_pb2.CongratsInfoOut + +UnburyCurrentDeckMode = _pb.UnburyCardsInCurrentDeckIn.Mode # pylint:disable=no-member +BuryOrSuspendMode = _pb.BuryOrSuspendCardsIn.Mode # pylint:disable=no-member if TYPE_CHECKING: UnburyCurrentDeckModeValue = ( - pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint:disable=no-member + _pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint:disable=no-member ) BuryOrSuspendModeValue = ( - pb.BuryOrSuspendCardsIn.ModeValue # pylint:disable=no-member + _pb.BuryOrSuspendCardsIn.ModeValue # pylint:disable=no-member ) # card types: 0=new, 1=lrn, 2=rev, 3=relrn @@ -1241,7 +1236,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe # Deck finished state ########################################################################## - def congratulations_info(self) -> pb.CongratsInfoOut: + def congratulations_info(self) -> CongratsInfoOut: return self.col.backend.congrats_info() def finishedMsg(self) -> str: diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 164b5516d..0fb451ea1 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import anki from anki.consts import * -from anki.rsbackend import TR, FormatTimeSpanContext +from anki.lang import TR, FormatTimeSpanContext from anki.utils import ids2str # Card stats diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py index e4db1eaf6..32c4c4bb5 100644 --- a/pylib/anki/stdmodels.py +++ b/pylib/anki/stdmodels.py @@ -5,12 +5,15 @@ from __future__ import annotations from typing import TYPE_CHECKING, Callable, List, Tuple +from anki._backend import StockNoteType from anki.collection import Collection from anki.models import NoteType -from anki.rsbackend import StockNoteType, from_json_bytes +from anki.utils import from_json_bytes if TYPE_CHECKING: - from anki.backend_pb2 import StockNoteTypeValue # pylint: disable=no-name-in-module + from anki._backend.backend_pb2 import ( # pylint: disable=no-name-in-module + StockNoteTypeValue, + ) # add-on authors can add ("note type name", function_like_addBasicModel) diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 9bd6409b5..331902280 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -1,8 +1,15 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -# + +import anki._backend.backend_pb2 as _pb + +# public exports +SyncAuth = _pb.SyncAuth +SyncOutput = _pb.SyncCollectionOut +SyncStatus = _pb.SyncStatusOut + + # Legacy attributes some add-ons may be using -# from .httpclient import HttpClient diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py index 1d2db82db..04d5c10ce 100644 --- a/pylib/anki/syncserver/__init__.py +++ b/pylib/anki/syncserver/__init__.py @@ -26,7 +26,7 @@ except ImportError as e: from flask import Response from anki import Collection -from anki.backend_pb2 import SyncServerMethodIn +from anki._backend.backend_pb2 import SyncServerMethodIn Method = SyncServerMethodIn.Method # pylint: disable=no-member diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index c8e969568..bfcd8c9a5 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -16,9 +16,13 @@ import re from typing import Collection, List, Optional, Sequence, Tuple import anki # pylint: disable=unused-import -from anki.collection import SearchTerm +import anki._backend.backend_pb2 as _pb +import anki.collection from anki.utils import ids2str +# public exports +TagTreeNode = _pb.TagTreeNode + class TagManager: def __init__(self, col: anki.collection.Collection) -> None: @@ -37,6 +41,9 @@ class TagManager: def allItems(self) -> List[Tuple[str, int]]: return [(t.name, t.usn) for t in self.col.backend.all_tags()] + def tree(self) -> TagTreeNode: + return self.col.backend.tag_tree() + # Registering and fetching tags ############################################################# @@ -87,7 +94,7 @@ class TagManager: def rename_tag(self, old: str, new: str) -> int: "Rename provided tag, returning number of changed notes." - nids = self.col.find_notes(SearchTerm(tag=old)) + nids = self.col.find_notes(anki.collection.SearchTerm(tag=old)) if not nids: return 0 escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 0050f4c24..535d0c80f 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -32,13 +32,15 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import anki +import anki._backend.backend_pb2 as _pb from anki import hooks from anki.cards import Card from anki.decks import DeckManager +from anki.errors import TemplateError from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import pb, to_json_bytes from anki.sound import AVTag, SoundOrVideoTag, TTSTag +from anki.utils import to_json_bytes CARD_BLANK_HELP = ( "https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank" @@ -61,7 +63,7 @@ class PartiallyRenderedCard: anodes: TemplateReplacementList @classmethod - def from_proto(cls, out: pb.RenderCardOut) -> PartiallyRenderedCard: + def from_proto(cls, out: _pb.RenderCardOut) -> PartiallyRenderedCard: qnodes = cls.nodes_from_proto(out.question_nodes) anodes = cls.nodes_from_proto(out.answer_nodes) @@ -69,7 +71,7 @@ class PartiallyRenderedCard: @staticmethod def nodes_from_proto( - nodes: Sequence[pb.RenderedTemplateNode], + nodes: Sequence[_pb.RenderedTemplateNode], ) -> TemplateReplacementList: results: TemplateReplacementList = [] for node in nodes: @@ -86,7 +88,7 @@ class PartiallyRenderedCard: return results -def av_tag_to_native(tag: pb.AVTag) -> AVTag: +def av_tag_to_native(tag: _pb.AVTag) -> AVTag: val = tag.WhichOneof("value") if val == "sound_or_video": return SoundOrVideoTag(filename=tag.sound_or_video) @@ -100,7 +102,7 @@ def av_tag_to_native(tag: pb.AVTag) -> AVTag: ) -def av_tags_to_native(tags: Sequence[pb.AVTag]) -> List[AVTag]: +def av_tags_to_native(tags: Sequence[_pb.AVTag]) -> List[AVTag]: return list(map(av_tag_to_native, tags)) @@ -206,7 +208,7 @@ class TemplateRenderContext: def render(self) -> TemplateRenderOutput: try: partial = self._partially_render() - except anki.rsbackend.TemplateError as e: + except TemplateError as e: return TemplateRenderOutput( question_text=str(e), answer_text=str(e), diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index d65971224..d8d3975ab 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -26,6 +26,17 @@ from anki.dbproxy import DBProxy _tmpdir: Optional[str] +try: + # pylint: disable=c-extension-no-member + import orjson + + to_json_bytes = orjson.dumps + from_json_bytes = orjson.loads +except: + print("orjson is missing; DB operations will be slower") + to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore + from_json_bytes = json.loads + # Time handling ############################################################################## diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index 601ca9dc2..00824bebd 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -5,8 +5,7 @@ import tempfile from anki import Collection as aopen from anki.dbproxy import emulate_named_args -from anki.lang import without_unicode_isolation -from anki.rsbackend import TR +from anki.lang import TR, without_unicode_isolation from anki.stdmodels import addBasicModel, get_stock_notetypes from anki.utils import isWin from tests.shared import assertException, getEmptyCol diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index 9cbc5a46b..114ec635d 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -1,9 +1,9 @@ # coding: utf-8 import pytest +from anki._backend import BuiltinSortKind from anki.collection import ConfigBoolKey from anki.consts import * -from anki.rsbackend import BuiltinSortKind from tests.shared import getEmptyCol, isNearCutoff diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index a5c2aafcb..47e6cf55f 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -2,7 +2,7 @@ import time from anki.consts import MODEL_CLOZE -from anki.rsbackend import NotFoundError +from anki.errors import NotFoundError from anki.utils import isWin, stripHTML from tests.shared import getEmptyCol diff --git a/pylib/tools/BUILD.bazel b/pylib/tools/BUILD.bazel index 25b2277cb..f2a4fa2d6 100644 --- a/pylib/tools/BUILD.bazel +++ b/pylib/tools/BUILD.bazel @@ -22,20 +22,6 @@ py_binary( ], ) -py_binary( - name = "genbackend", - srcs = [ - "genbackend.py", - "//pylib/anki:backend_pb2", - ], - visibility = ["//pylib:__subpackages__"], - deps = [ - requirement("black"), - requirement("stringcase"), - requirement("protobuf"), - ], -) - py_library( name = "hookslib", srcs = ["hookslib.py"], diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 61d3c91d1..350a24446 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -14,8 +14,8 @@ from typing import Any, Callable, Dict, Optional, Union import anki.lang from anki import version as _version +from anki._backend import RustBackend from anki.consts import HELP_SITE -from anki.rsbackend import RustBackend from anki.utils import checksum, isLin, isMac from aqt.qt import * from aqt.utils import TR, locale_dir, tr diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 5b29e646b..2465191da 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -13,8 +13,9 @@ from typing import List, Optional, Sequence, Tuple, cast import aqt import aqt.forms from anki.cards import Card -from anki.collection import Collection, ConfigBoolKey, InvalidInput, SearchTerm +from anki.collection import Collection, ConfigBoolKey, SearchTerm from anki.consts import * +from anki.errors import InvalidInput from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 1b82e62c3..0fc9fb3d9 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -9,9 +9,9 @@ from typing import Any, Dict, List, Optional import aqt from anki.cards import Card from anki.consts import * +from anki.errors import TemplateError from anki.lang import without_unicode_isolation from anki.notes import Note -from anki.rsbackend import TemplateError from anki.template import TemplateRenderContext from aqt import AnkiQt, gui_hooks from aqt.qt import * diff --git a/qt/aqt/dbcheck.py b/qt/aqt/dbcheck.py index 864ffffec..ad46bf231 100644 --- a/qt/aqt/dbcheck.py +++ b/qt/aqt/dbcheck.py @@ -4,7 +4,7 @@ from __future__ import annotations import aqt -from anki.rsbackend import DatabaseCheckProgress, ProgressKind +from anki.collection import DatabaseCheckProgress, ProgressKind from aqt.qt import * from aqt.utils import showText, tooltip diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index b585ddfaf..10041e9fb 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -9,8 +9,8 @@ from dataclasses import dataclass from typing import Any import aqt +from anki.decks import DeckTreeNode from anki.errors import DeckRenameError -from anki.rsbackend import DeckTreeNode from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.sound import av_player diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index c9d0d6338..12b5180d4 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -4,7 +4,8 @@ from typing import List, Optional import aqt -from anki.collection import InvalidInput, SearchTerm +from anki.collection import SearchTerm +from anki.errors import InvalidInput from anki.lang import without_unicode_isolation from aqt.qt import * from aqt.utils import ( diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index e399ce1da..b9e94d8de 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -6,7 +6,7 @@ from __future__ import annotations import re import aqt -from anki.backend_pb2 import EmptyCardsReport, NoteWithEmptyCards +from anki.collection import EmptyCardsReport, NoteWithEmptyCards from aqt import gui_hooks from aqt.qt import QDialog, QDialogButtonBox, qconnect from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, tr @@ -24,7 +24,7 @@ def show_empty_cards(mw: aqt.main.AnkiQt) -> None: diag = EmptyCardsDialog(mw, report) diag.show() - mw.taskman.run_in_background(mw.col.backend.get_empty_cards, on_done) + mw.taskman.run_in_background(mw.col.get_empty_cards, on_done) class EmptyCardsDialog(QDialog): diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 9c676ffe7..2cf6170ba 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -3,9 +3,9 @@ import aqt from anki.consts import * +from anki.errors import TemplateError from anki.lang import without_unicode_isolation from anki.models import NoteType -from anki.rsbackend import TemplateError from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.schema_change_tracker import ChangeTracker diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 2653e11de..173d7a4d8 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -26,11 +26,11 @@ import aqt.stats import aqt.toolbar import aqt.webview from anki import hooks +from anki._backend import RustBackend as _RustBackend from anki.collection import Collection, SearchTerm from anki.decks import Deck from anki.hooks import runHook from anki.lang import without_unicode_isolation -from anki.rsbackend import RustBackend from anki.sound import AVTag, SoundOrVideoTag from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from aqt import gui_hooks @@ -100,7 +100,7 @@ class AnkiQt(QMainWindow): self, app: QApplication, profileManager: ProfileManagerType, - backend: RustBackend, + backend: _RustBackend, opts: Namespace, args: List[Any], ) -> None: diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 50bf66f0e..79a560632 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -9,8 +9,10 @@ from concurrent.futures import Future from typing import Iterable, List, Optional, Sequence, TypeVar import aqt -from anki.collection import SearchTerm -from anki.rsbackend import TR, Interrupted, ProgressKind, pb +from anki.collection import ProgressKind, SearchTerm +from anki.errors import Interrupted +from anki.lang import TR +from anki.media import CheckMediaOut from aqt.qt import * from aqt.utils import ( askUser, @@ -74,7 +76,7 @@ class MediaChecker: self.mw.taskman.run_on_main(lambda: self.mw.progress.update(progress.val)) - def _check(self) -> pb.CheckMediaOut: + def _check(self) -> CheckMediaOut: "Run the check on a background thread." return self.mw.col.media.check() @@ -87,7 +89,7 @@ class MediaChecker: if isinstance(exc, Interrupted): return - output: pb.CheckMediaOut = future.result() + output: CheckMediaOut = future.result() report = output.report # show report and offer to delete diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index b37282afc..1da2565ae 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -18,11 +18,10 @@ import flask_cors # type: ignore from flask import Response, request from waitress.server import create_server -import anki.backend_pb2 as pb import aqt from anki import hooks -from anki.rsbackend import from_json_bytes -from anki.utils import devMode +from anki.collection import GraphPreferences +from anki.utils import devMode, from_json_bytes from aqt.qt import * from aqt.utils import aqt_data_folder @@ -253,22 +252,21 @@ def _redirectWebExports(path): def graph_data() -> bytes: args = from_json_bytes(request.data) - return aqt.mw.col.backend.graphs(search=args["search"], days=args["days"]) + return aqt.mw.col.graph_data(search=args["search"], days=args["days"]) def graph_preferences() -> bytes: - return aqt.mw.col.backend.get_graph_preferences() + return aqt.mw.col.get_graph_preferences() def set_graph_preferences() -> None: - input = pb.GraphPreferences() - input.ParseFromString(request.data) - aqt.mw.col.backend.set_graph_preferences(input=input) + prefs = GraphPreferences() + prefs.ParseFromString(request.data) + aqt.mw.col.set_graph_preferences(prefs) def congrats_info() -> bytes: - info = aqt.mw.col.backend.congrats_info() - return info.SerializeToString() + return aqt.mw.col.congrats_info() post_handlers = { diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 07925f132..3e60a3e9e 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -9,13 +9,9 @@ from dataclasses import dataclass from typing import Callable, List, Optional, Union import aqt -from anki.rsbackend import ( - TR, - Interrupted, - MediaSyncProgress, - NetworkError, - ProgressKind, -) +from anki.collection import MediaSyncProgress, ProgressKind +from anki.errors import Interrupted, NetworkError +from anki.lang import TR from anki.types import assert_exhaustive from anki.utils import intTime from aqt import gui_hooks diff --git a/qt/aqt/models.py b/qt/aqt/models.py index 1d238a496..bc398a5ba 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -6,11 +6,9 @@ from typing import Any, List, Optional, Sequence import aqt.clayout from anki import stdmodels -from anki.backend_pb2 import NoteTypeNameIDUseCount from anki.lang import without_unicode_isolation -from anki.models import NoteType +from anki.models import NoteType, NoteTypeNameIDUseCount from anki.notes import Note -from anki.rsbackend import pb from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.utils import ( @@ -51,7 +49,7 @@ class Models(QDialog): self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE), ) - self.models: List[pb.NoteTypeNameIDUseCount] = [] + self.models: List[NoteTypeNameIDUseCount] = [] self.setupModels() restoreGeom(self, "models") self.exec_() @@ -111,7 +109,7 @@ class Models(QDialog): self.saveAndRefresh(nt) def saveAndRefresh(self, nt: NoteType) -> None: - def save() -> Sequence[pb.NoteTypeNameIDUseCount]: + def save() -> Sequence[NoteTypeNameIDUseCount]: self.mm.save(nt) return self.col.models.all_use_counts() @@ -161,7 +159,7 @@ class Models(QDialog): nt = self.current_notetype() - def save() -> Sequence[pb.NoteTypeNameIDUseCount]: + def save() -> Sequence[NoteTypeNameIDUseCount]: self.mm.rem(nt) return self.col.models.all_use_counts() diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 93065846d..af548c5c3 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -20,7 +20,7 @@ import aqt.sound from anki import Collection from anki.db import DB from anki.lang import without_unicode_isolation -from anki.rsbackend import SyncAuth +from anki.sync import SyncAuth from anki.utils import intTime, isMac, isWin from aqt import appHelpSite from aqt.qt import * diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 6806107b1..c1c3749ad 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -9,9 +9,10 @@ from enum import Enum from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast import aqt -from anki.collection import ConfigBoolKey, InvalidInput, SearchTerm -from anki.errors import DeckRenameError -from anki.rsbackend import DeckTreeNode, TagTreeNode +from anki.collection import ConfigBoolKey, SearchTerm +from anki.decks import DeckTreeNode +from anki.errors import DeckRenameError, InvalidInput +from anki.tags import TagTreeNode from aqt import gui_hooks from aqt.main import ResetReason from aqt.models import Models @@ -524,7 +525,7 @@ class SidebarTreeView(QTreeView): newhead = head + node.name + "::" render(item, node.children, newhead) - tree = self.col.backend.tag_tree() + tree = self.col.tags.tree() root = self._section_root( root=root, name=TR.BROWSING_SIDEBAR_TAGS, diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 691fde1f4..c71ae132c 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -8,18 +8,10 @@ import os from typing import Callable, Tuple import aqt -from anki.lang import without_unicode_isolation -from anki.rsbackend import ( - TR, - FullSyncProgress, - Interrupted, - NormalSyncProgress, - ProgressKind, - SyncError, - SyncErrorKind, - SyncOutput, - SyncStatus, -) +from anki.collection import FullSyncProgress, NormalSyncProgress, ProgressKind +from anki.errors import Interrupted, SyncError +from anki.lang import TR, without_unicode_isolation +from anki.sync import SyncOutput, SyncStatus from anki.utils import platDesc from aqt.qt import ( QDialog, @@ -69,7 +61,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]) def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception): if isinstance(err, SyncError): - if err.kind() == SyncErrorKind.AUTH_FAILED: + if err.is_auth_error(): mw.pm.clear_sync_auth() elif isinstance(err, Interrupted): # no message to show @@ -249,7 +241,7 @@ def sync_login( try: auth = fut.result() except SyncError as e: - if e.kind() == SyncErrorKind.AUTH_FAILED: + if e.is_auth_error(): showWarning(str(e)) sync_login(mw, on_success, username, password) else: diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 8214966e2..8bc71904d 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import Any, Dict, Optional import aqt -from anki.rsbackend import SyncStatus +from anki.sync import SyncStatus from aqt import gui_hooks from aqt.qt import * from aqt.sync import get_sync_status diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 373d2bc46..ac511e9a4 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -14,13 +14,14 @@ from markdown import markdown import anki import aqt -from anki.rsbackend import TR, InvalidInput # pylint: disable=unused-import +from anki.errors import InvalidInput +from anki.lang import TR # pylint: disable=unused-import from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild from aqt.qt import * from aqt.theme import theme_manager if TYPE_CHECKING: - from anki.rsbackend import TRValue + from anki.lang import TRValue TextFormat = Union[Literal["plain", "rich"]] diff --git a/qt/tests/test_i18n.py b/qt/tests/test_i18n.py index c11180032..41f9c26e4 100644 --- a/qt/tests/test_i18n.py +++ b/qt/tests/test_i18n.py @@ -1,5 +1,5 @@ import anki.lang -from anki.rsbackend import TR +from anki.lang import TR def test_no_collection_i18n():