start work on more clearly defining backend/protobuf boundaries

- anki._backend stores the protobuf files and rsbackend.py code
- pylib modules import protobuf messages directly from the
_pb2 files, and explicitly export any will be returned or consumed
by public pylib functions, so that calling code can import from pylib
- the "rsbackend" no longer imports and re-exports protobuf messages
- pylib can just consume them directly.
- move errors to errors.py

Still todo:

- rsbridge
- finishing the work on rsbackend, and check what we need to add
back to the original file location to avoid breaking add-ons
This commit is contained in:
Damien Elmes 2021-01-31 15:55:08 +10:00
parent cd9767be80
commit 9d853bbb03
60 changed files with 420 additions and 367 deletions

View File

@ -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

View File

@ -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 = [
"..",

View File

@ -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__"],
)

View File

@ -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(

View File

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/backend_pb2.pyi

View File

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/fluent_pb2.pyi

View File

@ -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:

View File

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/generated.py

View File

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/backend_pb2.pyi

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/fluent_pb2.pyi

View File

@ -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),

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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=[

View File

@ -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

View File

@ -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))

View File

@ -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,

View File

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/rsbackend_gen.py

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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),

View File

@ -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
##############################################################################

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"],

View File

@ -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

View File

@ -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

View File

@ -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 *

View File

@ -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

View File

@ -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

View File

@ -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 (

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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 = {

View File

@ -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

View File

@ -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()

View File

@ -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 *

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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"]]

View File

@ -1,5 +1,5 @@
import anki.lang
from anki.rsbackend import TR
from anki.lang import TR
def test_no_collection_i18n():