simplify errors

- use a flat enum instead of oneof messages, most of which were empty
- tidy up the Python side
This commit is contained in:
Damien Elmes 2021-04-03 16:00:15 +10:00
parent fe6888f9a4
commit 41c5a25dc8
9 changed files with 169 additions and 187 deletions

View file

@ -9,7 +9,8 @@ ignored-classes=
AnswerCardIn, AnswerCardIn,
UnburyCardsInCurrentDeckIn, UnburyCardsInCurrentDeckIn,
BuryOrSuspendCardsIn, BuryOrSuspendCardsIn,
NoteIsDuplicateOrEmptyOut NoteIsDuplicateOrEmptyOut,
BackendError
[REPORTS] [REPORTS]
output-format=colorized output-format=colorized

View file

@ -3,24 +3,39 @@
from __future__ import annotations from __future__ import annotations
import os
import sys import sys
import traceback import traceback
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
from weakref import ref from weakref import ref
from markdown import markdown
import anki.buildinfo import anki.buildinfo
from anki._backend.generated import RustBackendGenerated from anki._backend.generated import RustBackendGenerated
from anki.dbproxy import Row as DBRow from anki.dbproxy import Row as DBRow
from anki.dbproxy import ValueForDB from anki.dbproxy import ValueForDB
from anki.errors import backend_exception_to_pylib
from anki.utils import from_json_bytes, to_json_bytes from anki.utils import from_json_bytes, to_json_bytes
from ..errors import (
BackendIOError,
DBError,
ExistsError,
FilteredDeckError,
Interrupted,
InvalidInput,
LocalizedError,
NetworkError,
NotFoundError,
SearchError,
SyncError,
SyncErrorKind,
TemplateError,
UndoEmpty,
)
from . import backend_pb2 as pb from . import backend_pb2 as pb
from . import rsbridge from . import rsbridge
from .fluent import GeneratedTranslations, LegacyTranslationEnum from .fluent import GeneratedTranslations, LegacyTranslationEnum
# pylint: disable=c-extension-no-member
assert rsbridge.buildhash() == anki.buildinfo.buildhash assert rsbridge.buildhash() == anki.buildinfo.buildhash
@ -147,3 +162,57 @@ class Translations(GeneratedTranslations):
return self.backend().translate( return self.backend().translate(
module_index=module, message_index=message, **args module_index=module, message_index=message, **args
) )
def backend_exception_to_pylib(err: pb.BackendError) -> Exception:
kind = pb.BackendError
val = err.kind
if val == kind.INTERRUPTED:
return Interrupted()
elif val == kind.NETWORK_ERROR:
return NetworkError(err.localized)
elif val == kind.SYNC_AUTH_ERROR:
return SyncError(err.localized, SyncErrorKind.AUTH)
elif val == kind.SYNC_OTHER_ERROR:
return SyncError(err.localized, SyncErrorKind.OTHER)
elif val == kind.IO_ERROR:
return BackendIOError(err.localized)
elif val == kind.DB_ERROR:
return DBError(err.localized)
elif val == kind.TEMPLATE_PARSE:
return TemplateError(err.localized)
elif val == kind.INVALID_INPUT:
return InvalidInput(err.localized)
elif val == kind.JSON_ERROR:
return LocalizedError(err.localized)
elif val == kind.NOT_FOUND_ERROR:
return NotFoundError()
elif val == kind.EXISTS:
return ExistsError()
elif val == kind.FILTERED_DECK_ERROR:
return FilteredDeckError(err.localized)
elif val == kind.PROTO_ERROR:
return LocalizedError(err.localized)
elif val == kind.SEARCH_ERROR:
return SearchError(markdown(err.localized))
elif val == kind.UNDO_EMPTY:
return UndoEmpty()
else:
# sadly we can't do exhaustiveness checking on protobuf enums
# assert_exhaustive(val)
return LocalizedError(err.localized)

View file

@ -40,7 +40,7 @@ from anki.config import Config, ConfigManager
from anki.consts import * from anki.consts import *
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
from anki.decks import DeckId, DeckManager from anki.decks import DeckId, DeckManager
from anki.errors import AnkiError, DBError from anki.errors import AbortSchemaModification, DBError
from anki.lang import FormatTimeSpan from anki.lang import FormatTimeSpan
from anki.media import MediaManager, media_paths_from_col_path from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager, NotetypeDict, NotetypeId from anki.models import ModelManager, NotetypeDict, NotetypeId
@ -290,7 +290,7 @@ class Collection:
"Mark schema modified. Call this first so user can abort if necessary." "Mark schema modified. Call this first so user can abort if necessary."
if not self.schemaChanged(): if not self.schemaChanged():
if check and not hooks.schema_will_change(proceed=True): if check and not hooks.schema_will_change(proceed=True):
raise AnkiError("abortSchemaMod") raise AbortSchemaModification()
self.db.execute("update col set scm=?", intTime(1000)) self.db.execute("update col set scm=?", intTime(1000))
self.save() self.save()

View file

@ -3,41 +3,48 @@
from __future__ import annotations from __future__ import annotations
from markdown import markdown from enum import Enum
import anki._backend.backend_pb2 as _pb
from anki.types import assert_exhaustive
class StringError(Exception): class LocalizedError(Exception):
"An error with a localized description."
def __init__(self, localized: str) -> None:
self._localized = localized
super().__init__()
def __str__(self) -> str: def __str__(self) -> str:
return self.args[0] # pylint: disable=unsubscriptable-object return self._localized
class Interrupted(Exception): class Interrupted(Exception):
pass pass
class NetworkError(StringError): class NetworkError(LocalizedError):
pass pass
class SyncError(StringError): class SyncErrorKind(Enum):
# pylint: disable=no-member AUTH = 1
def is_auth_error(self) -> bool: OTHER = 2
return self.args[1] == _pb.SyncError.SyncErrorKind.AUTH_FAILED
class IOError(StringError): class SyncError(LocalizedError):
def __init__(self, localized: str, kind: SyncErrorKind):
self.kind = kind
super().__init__(localized)
class BackendIOError(LocalizedError):
pass pass
class DBError(StringError): class DBError(LocalizedError):
pass pass
class TemplateError(StringError): class TemplateError(LocalizedError):
pass pass
@ -53,67 +60,22 @@ class UndoEmpty(Exception):
pass pass
class DeckRenameError(Exception): class FilteredDeckError(LocalizedError):
"""Legacy error, use FilteredDeckError instead."""
def __init__(self, description: str, *args: object) -> None:
super().__init__(description, *args)
self.description = description
class FilteredDeckError(StringError, DeckRenameError):
pass pass
class InvalidInput(StringError): class InvalidInput(LocalizedError):
pass pass
class SearchError(StringError): class SearchError(LocalizedError):
pass pass
def backend_exception_to_pylib(err: _pb.BackendError) -> Exception: class AbortSchemaModification(Exception):
val = err.WhichOneof("value") pass
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 == "filtered_deck_error":
return FilteredDeckError(err.localized)
elif val == "proto_error":
return StringError(err.localized)
elif val == "search_error":
return SearchError(markdown(err.localized))
elif val == "undo_empty":
return UndoEmpty()
else:
assert_exhaustive(val)
return StringError(err.localized)
# FIXME: this is only used with "abortSchemaMod", but currently some # legacy
# add-ons depend on it DeckRenameError = FilteredDeckError
class AnkiError(Exception): AnkiError = AbortSchemaModification
def __init__(self, type: str) -> None:
super().__init__()
self.type = type
def __str__(self) -> str:
return self.type

View file

@ -71,7 +71,7 @@ class ErrorHandler(QObject):
error = html.escape(self.pool) error = html.escape(self.pool)
self.pool = "" self.pool = ""
self.mw.progress.clear() self.mw.progress.clear()
if "abortSchemaMod" in error: if "AbortSchemaModification" in error:
return return
if "DeprecationWarning" in error: if "DeprecationWarning" in error:
return return

View file

@ -9,7 +9,7 @@ from concurrent.futures import Future
from typing import Callable, Tuple from typing import Callable, Tuple
import aqt import aqt
from anki.errors import Interrupted, SyncError from anki.errors import Interrupted, SyncError, SyncErrorKind
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.sync import SyncOutput, SyncStatus from anki.sync import SyncOutput, SyncStatus
from anki.utils import platDesc from anki.utils import platDesc
@ -62,7 +62,7 @@ def get_sync_status(
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None: def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None:
if isinstance(err, SyncError): if isinstance(err, SyncError):
if err.is_auth_error(): if err.kind is SyncErrorKind.AUTH:
mw.pm.clear_sync_auth() mw.pm.clear_sync_auth()
elif isinstance(err, Interrupted): elif isinstance(err, Interrupted):
# no message to show # no message to show
@ -247,7 +247,7 @@ def sync_login(
try: try:
auth = fut.result() auth = fut.result()
except SyncError as e: except SyncError as e:
if e.is_auth_error(): if e.kind is SyncErrorKind.AUTH:
showWarning(str(e)) showWarning(str(e))
sync_login(mw, on_success, username, password) sync_login(mw, on_success, username, password)
else: else:

View file

@ -553,53 +553,28 @@ message I18nBackendInit {
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
message BackendError { message BackendError {
enum Kind {
INVALID_INPUT = 0;
UNDO_EMPTY = 1;
INTERRUPTED = 2;
TEMPLATE_PARSE = 3;
IO_ERROR = 4;
DB_ERROR = 5;
NETWORK_ERROR = 6;
SYNC_AUTH_ERROR = 7;
SYNC_OTHER_ERROR = 8;
JSON_ERROR = 9;
PROTO_ERROR = 10;
NOT_FOUND_ERROR = 11;
EXISTS = 12;
FILTERED_DECK_ERROR = 13;
SEARCH_ERROR = 14;
}
// localized error description suitable for displaying to the user // localized error description suitable for displaying to the user
string localized = 1; string localized = 1;
// error specifics // the error subtype
oneof value { Kind kind = 2;
Empty invalid_input = 2;
Empty template_parse = 3;
Empty io_error = 4;
Empty db_error = 5;
NetworkError network_error = 6;
SyncError sync_error = 7;
// user interrupted operation
Empty interrupted = 8;
string json_error = 9;
string proto_error = 10;
Empty not_found_error = 11;
Empty exists = 12;
Empty filtered_deck_error = 13;
Empty search_error = 14;
Empty undo_empty = 15;
}
}
message NetworkError {
enum NetworkErrorKind {
OTHER = 0;
OFFLINE = 1;
TIMEOUT = 2;
PROXY_AUTH = 3;
}
NetworkErrorKind kind = 1;
}
message SyncError {
enum SyncErrorKind {
OTHER = 0;
CONFLICT = 1;
SERVER_ERROR = 2;
CLIENT_TOO_OLD = 3;
AUTH_FAILED = 4;
SERVER_MESSAGE = 5;
MEDIA_CHECK_REQUIRED = 6;
RESYNC_REQUIRED = 7;
CLOCK_INCORRECT = 8;
DATABASE_CHECK_REQUIRED = 9;
SYNC_NOT_STARTED = 10;
}
SyncErrorKind kind = 1;
} }
// Progress // Progress

View file

@ -3,72 +3,49 @@
use crate::{ use crate::{
backend_proto as pb, backend_proto as pb,
error::{AnkiError, NetworkErrorKind, SyncErrorKind}, error::{AnkiError, SyncErrorKind},
prelude::*, prelude::*,
}; };
/// Convert an Anki error to a protobuf error. use pb::backend_error::Kind;
pub(super) fn anki_error_to_proto_error(err: AnkiError, tr: &I18n) -> pb::BackendError {
use pb::backend_error::Value as V;
let localized = err.localized_description(tr);
let value = match err {
AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}),
AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}),
AnkiError::IoError { .. } => V::IoError(pb::Empty {}),
AnkiError::DbError { .. } => V::DbError(pb::Empty {}),
AnkiError::NetworkError(err) => V::NetworkError(pb::NetworkError {
kind: err.kind.into(),
}),
AnkiError::SyncError(err) => V::SyncError(pb::SyncError {
kind: err.kind.into(),
}),
AnkiError::Interrupted => V::Interrupted(pb::Empty {}),
AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}),
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
AnkiError::JsonError(info) => V::JsonError(info),
AnkiError::ProtoError(info) => V::ProtoError(info),
AnkiError::NotFound => V::NotFoundError(pb::Empty {}),
AnkiError::Existing => V::Exists(pb::Empty {}),
AnkiError::FilteredDeckError(_) => V::FilteredDeckError(pb::Empty {}),
AnkiError::SearchError(_) => V::SearchError(pb::Empty {}),
AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}),
AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}),
AnkiError::InvalidRegex(_) => V::InvalidInput(pb::Empty {}),
AnkiError::UndoEmpty => V::UndoEmpty(pb::Empty {}),
};
pb::BackendError { impl AnkiError {
value: Some(value), pub(super) fn into_protobuf(self, tr: &I18n) -> pb::BackendError {
localized, let localized = self.localized_description(tr);
let kind = match self {
AnkiError::InvalidInput(_) => Kind::InvalidInput,
AnkiError::TemplateError(_) => Kind::TemplateParse,
AnkiError::IoError(_) => Kind::IoError,
AnkiError::DbError(_) => Kind::DbError,
AnkiError::NetworkError(_) => Kind::NetworkError,
AnkiError::SyncError(err) => err.kind.into(),
AnkiError::Interrupted => Kind::Interrupted,
AnkiError::CollectionNotOpen => Kind::InvalidInput,
AnkiError::CollectionAlreadyOpen => Kind::InvalidInput,
AnkiError::JsonError(_) => Kind::JsonError,
AnkiError::ProtoError(_) => Kind::ProtoError,
AnkiError::NotFound => Kind::NotFoundError,
AnkiError::Existing => Kind::Exists,
AnkiError::FilteredDeckError(_) => Kind::FilteredDeckError,
AnkiError::SearchError(_) => Kind::SearchError,
AnkiError::TemplateSaveError(_) => Kind::TemplateParse,
AnkiError::ParseNumError => Kind::InvalidInput,
AnkiError::InvalidRegex(_) => Kind::InvalidInput,
AnkiError::UndoEmpty => Kind::UndoEmpty,
};
pb::BackendError {
kind: kind as i32,
localized,
}
} }
} }
impl std::convert::From<NetworkErrorKind> for i32 { impl From<SyncErrorKind> for Kind {
fn from(e: NetworkErrorKind) -> Self { fn from(err: SyncErrorKind) -> Self {
use pb::network_error::NetworkErrorKind as V; match err {
(match e { SyncErrorKind::AuthFailed => Kind::SyncAuthError,
NetworkErrorKind::Offline => V::Offline, _ => Kind::SyncOtherError,
NetworkErrorKind::Timeout => V::Timeout, }
NetworkErrorKind::ProxyAuth => V::ProxyAuth,
NetworkErrorKind::Other => V::Other,
}) as i32
}
}
impl std::convert::From<SyncErrorKind> for i32 {
fn from(e: SyncErrorKind) -> Self {
use pb::sync_error::SyncErrorKind as V;
(match e {
SyncErrorKind::Conflict => V::Conflict,
SyncErrorKind::ServerError => V::ServerError,
SyncErrorKind::ClientTooOld => V::ClientTooOld,
SyncErrorKind::AuthFailed => V::AuthFailed,
SyncErrorKind::ServerMessage => V::ServerMessage,
SyncErrorKind::ResyncRequired => V::ResyncRequired,
SyncErrorKind::DatabaseCheckRequired => V::DatabaseCheckRequired,
SyncErrorKind::Other => V::Other,
SyncErrorKind::ClockIncorrect => V::ClockIncorrect,
SyncErrorKind::SyncNotStarted => V::SyncNotStarted,
}) as i32
} }
} }

View file

@ -61,8 +61,6 @@ use std::{
}; };
use tokio::runtime::{self, Runtime}; use tokio::runtime::{self, Runtime};
use self::error::anki_error_to_proto_error;
pub struct Backend { pub struct Backend {
col: Arc<Mutex<Option<Collection>>>, col: Arc<Mutex<Option<Collection>>>,
tr: I18n, tr: I18n,
@ -137,7 +135,7 @@ impl Backend {
pb::ServiceIndex::Cards => CardsService::run_method(self, method, input), pb::ServiceIndex::Cards => CardsService::run_method(self, method, input),
}) })
.map_err(|err| { .map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.tr); let backend_err = err.into_protobuf(&self.tr);
let mut bytes = Vec::new(); let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap(); backend_err.encode(&mut bytes).unwrap();
bytes bytes
@ -146,7 +144,7 @@ impl Backend {
pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> { pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> {
self.db_command(input).map_err(|err| { self.db_command(input).map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.tr); let backend_err = err.into_protobuf(&self.tr);
let mut bytes = Vec::new(); let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap(); backend_err.encode(&mut bytes).unwrap();
bytes bytes