mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 15:02:21 -04:00
Remove OpWithBackendProgress and ClosedCollectionOp
Backend progress logic is now in ProgressManager. QueryOp can be used for running on closed collection. Also fix aborting of colpkg exports, which slipped through in #1817.
This commit is contained in:
parent
702f47c522
commit
19664a0e47
6 changed files with 135 additions and 172 deletions
|
@ -7,18 +7,17 @@ import os
|
|||
import re
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from concurrent.futures import Future
|
||||
from dataclasses import dataclass
|
||||
from typing import Sequence, Type
|
||||
|
||||
import aqt.forms
|
||||
import aqt.main
|
||||
from anki.collection import DeckIdLimit, ExportLimit, NoteIdsLimit
|
||||
from anki.collection import DeckIdLimit, ExportLimit, NoteIdsLimit, Progress
|
||||
from anki.decks import DeckId, DeckNameId
|
||||
from anki.notes import NoteId
|
||||
from aqt import gui_hooks
|
||||
from aqt.errors import show_exception
|
||||
from aqt.operations import QueryOpWithBackendProgress
|
||||
from aqt.operations import QueryOp
|
||||
from aqt.qt import *
|
||||
from aqt.utils import (
|
||||
checkInvalidFilename,
|
||||
|
@ -180,7 +179,7 @@ class ColpkgExporter(Exporter):
|
|||
|
||||
@staticmethod
|
||||
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
||||
def on_success(_future: Future) -> None:
|
||||
def on_success(_: None) -> None:
|
||||
mw.reopen()
|
||||
tooltip(tr.exporting_collection_exported(), parent=mw)
|
||||
|
||||
|
@ -189,14 +188,15 @@ class ColpkgExporter(Exporter):
|
|||
show_exception(parent=mw, exception=exception)
|
||||
|
||||
gui_hooks.collection_will_temporarily_close(mw.col)
|
||||
QueryOpWithBackendProgress(
|
||||
QueryOp(
|
||||
parent=mw,
|
||||
op=lambda col: col.export_collection_package(
|
||||
options.out_path, include_media=options.include_media, legacy=False
|
||||
),
|
||||
success=on_success,
|
||||
key="exporting",
|
||||
).failure(on_failure).run_in_background()
|
||||
).with_backend_progress(export_progress_label).failure(
|
||||
on_failure
|
||||
).run_in_background()
|
||||
|
||||
|
||||
class ApkgExporter(Exporter):
|
||||
|
@ -211,7 +211,7 @@ class ApkgExporter(Exporter):
|
|||
|
||||
@staticmethod
|
||||
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
||||
QueryOpWithBackendProgress(
|
||||
QueryOp(
|
||||
parent=mw,
|
||||
op=lambda col: col.export_anki_package(
|
||||
out_path=options.out_path,
|
||||
|
@ -219,8 +219,13 @@ class ApkgExporter(Exporter):
|
|||
with_scheduling=options.include_scheduling,
|
||||
with_media=options.include_media,
|
||||
),
|
||||
success=lambda fut: tooltip(
|
||||
tr.exporting_note_exported(count=fut), parent=mw
|
||||
success=lambda count: tooltip(
|
||||
tr.exporting_note_exported(count=count), parent=mw
|
||||
),
|
||||
key="exporting",
|
||||
).run_in_background()
|
||||
).with_backend_progress(export_progress_label).run_in_background()
|
||||
|
||||
|
||||
def export_progress_label(progress: Progress) -> str | None:
|
||||
if not progress.HasField("exporting"):
|
||||
return None
|
||||
return tr.exporting_exported_media_file(count=progress.exporting)
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future
|
||||
from typing import Sequence
|
||||
|
||||
import aqt.main
|
||||
from anki.collection import Collection, ImportLogWithChanges, Progress
|
||||
from anki.errors import Interrupted
|
||||
from aqt.operations import (
|
||||
ClosedCollectionOpWithBackendProgress,
|
||||
CollectionOpWithBackendProgress,
|
||||
)
|
||||
from aqt.operations import CollectionOp, QueryOp
|
||||
from aqt.qt import *
|
||||
from aqt.utils import askUser, getFile, showInfo, showText, showWarning, tooltip, tr
|
||||
|
||||
|
@ -62,7 +60,7 @@ def maybe_import_collection_package(mw: aqt.main.AnkiQt, path: str) -> None:
|
|||
|
||||
|
||||
def import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
|
||||
def on_success(_future: Future) -> None:
|
||||
def on_success() -> None:
|
||||
mw.loadCollection()
|
||||
tooltip(tr.importing_importing_complete())
|
||||
|
||||
|
@ -71,38 +69,43 @@ def import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
|
|||
if not isinstance(err, Interrupted):
|
||||
showWarning(str(err))
|
||||
|
||||
import_collection_package_op(mw, file).success(on_success).failure(
|
||||
on_failure
|
||||
).run_in_background()
|
||||
QueryOp(
|
||||
parent=mw,
|
||||
op=lambda _: mw.create_backup_now(),
|
||||
success=lambda _: mw.unloadCollection(
|
||||
lambda: import_collection_package_op(mw, file, on_success)
|
||||
.failure(on_failure)
|
||||
.run_in_background()
|
||||
),
|
||||
).with_progress().run_in_background()
|
||||
|
||||
|
||||
def import_collection_package_op(
|
||||
mw: aqt.main.AnkiQt, path: str
|
||||
) -> ClosedCollectionOpWithBackendProgress:
|
||||
def op() -> None:
|
||||
mw: aqt.main.AnkiQt, path: str, success: Callable[[], None]
|
||||
) -> QueryOp[None]:
|
||||
def op(_: Collection) -> None:
|
||||
col_path = mw.pm.collectionPath()
|
||||
media_folder = os.path.join(mw.pm.profileFolder(), "collection.media")
|
||||
mw.backend.import_collection_package(
|
||||
col_path=col_path, backup_path=path, media_folder=media_folder
|
||||
)
|
||||
|
||||
return ClosedCollectionOpWithBackendProgress(
|
||||
parent=mw,
|
||||
op=op,
|
||||
key="importing",
|
||||
return QueryOp(parent=mw, op=op, success=lambda _: success()).with_backend_progress(
|
||||
import_progress_label
|
||||
)
|
||||
|
||||
|
||||
def import_anki_package(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||
CollectionOpWithBackendProgress(
|
||||
CollectionOp(
|
||||
parent=mw,
|
||||
op=lambda col: col.import_anki_package(path),
|
||||
key="importing",
|
||||
).success(show_import_log).run_in_background()
|
||||
).with_backend_progress(import_progress_label).success(
|
||||
show_import_log
|
||||
).run_in_background()
|
||||
|
||||
|
||||
def show_import_log(future: Future) -> None:
|
||||
log = future.log # type: ignore
|
||||
def show_import_log(log_with_changes: ImportLogWithChanges) -> None:
|
||||
log = log_with_changes.log # type: ignore
|
||||
total = len(log.conflicting) + len(log.updated) + len(log.new) + len(log.duplicate)
|
||||
|
||||
text = f"""{tr.importing_notes_found_in_file(val=total)}
|
||||
|
@ -120,5 +123,9 @@ def show_import_log(future: Future) -> None:
|
|||
showText(text, plain_text_edit=True)
|
||||
|
||||
|
||||
def log_rows(rows: list, action: str) -> str:
|
||||
def log_rows(rows: Sequence, action: str) -> str:
|
||||
return "\n".join(f"[{action}] {', '.join(note.fields)}" for note in rows)
|
||||
|
||||
|
||||
def import_progress_label(progress: Progress) -> str | None:
|
||||
return progress.importing if progress.HasField("importing") else None
|
||||
|
|
|
@ -407,8 +407,8 @@ class AnkiQt(QMainWindow):
|
|||
self.restoring_backup = True
|
||||
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
|
||||
|
||||
import_collection_package_op(self, path).success(
|
||||
lambda _: self.onOpenProfile()
|
||||
import_collection_package_op(
|
||||
self, path, success=self.onOpenProfile
|
||||
).run_in_background()
|
||||
|
||||
def _on_downgrade(self) -> None:
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from concurrent.futures._base import Future
|
||||
from typing import Any, Callable, Generic, Literal, Protocol, TypeVar, Union
|
||||
from typing import Any, Callable, Generic, Protocol, TypeVar, Union
|
||||
|
||||
import aqt
|
||||
import aqt.gui_hooks
|
||||
|
@ -20,8 +19,7 @@ from anki.collection import (
|
|||
Progress,
|
||||
)
|
||||
from aqt.errors import show_exception
|
||||
from aqt.qt import QTimer, QWidget, qconnect
|
||||
from aqt.utils import tr
|
||||
from aqt.qt import QWidget
|
||||
|
||||
|
||||
class HasChangesProperty(Protocol):
|
||||
|
@ -70,6 +68,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
|
||||
_success: Callable[[ResultWithChanges], Any] | None = None
|
||||
_failure: Callable[[Exception], Any] | None = None
|
||||
_label_from_progress: Callable[[Progress], str | None] | None = None
|
||||
|
||||
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
||||
self._parent = parent
|
||||
|
@ -87,6 +86,12 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
self._failure = failure
|
||||
return self
|
||||
|
||||
def with_backend_progress(
|
||||
self, label_from_progress: Callable[[Progress], str | None] | None
|
||||
) -> CollectionOp[ResultWithChanges]:
|
||||
self._label_from_progress = label_from_progress
|
||||
return self
|
||||
|
||||
def run_in_background(self, *, initiator: object | None = None) -> None:
|
||||
from aqt import mw
|
||||
|
||||
|
@ -128,7 +133,12 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
op: Callable[[], ResultWithChanges],
|
||||
on_done: Callable[[Future], None],
|
||||
) -> None:
|
||||
mw.taskman.with_progress(op, on_done)
|
||||
if self._label_from_progress:
|
||||
mw.taskman.with_backend_progress(
|
||||
op, self._label_from_progress, on_done=on_done, parent=self._parent
|
||||
)
|
||||
else:
|
||||
mw.taskman.with_progress(op, on_done, parent=self._parent)
|
||||
|
||||
def _finish_op(
|
||||
self, mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None
|
||||
|
@ -185,6 +195,7 @@ class QueryOp(Generic[T]):
|
|||
|
||||
_failure: Callable[[Exception], Any] | None = None
|
||||
_progress: bool | str = False
|
||||
_label_from_progress: Callable[[Progress], str | None] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -209,6 +220,12 @@ class QueryOp(Generic[T]):
|
|||
self._progress = label or True
|
||||
return self
|
||||
|
||||
def with_backend_progress(
|
||||
self, label_from_progress: Callable[[Progress], str | None] | None
|
||||
) -> QueryOp[T]:
|
||||
self._label_from_progress = label_from_progress
|
||||
return self
|
||||
|
||||
def run_in_background(self) -> None:
|
||||
from aqt import mw
|
||||
|
||||
|
@ -218,26 +235,11 @@ class QueryOp(Generic[T]):
|
|||
|
||||
def wrapped_op() -> T:
|
||||
assert mw
|
||||
if self._progress:
|
||||
label: str | None
|
||||
if isinstance(self._progress, str):
|
||||
label = self._progress
|
||||
else:
|
||||
label = None
|
||||
|
||||
def start_progress() -> None:
|
||||
assert mw
|
||||
mw.progress.start(label=label)
|
||||
|
||||
mw.taskman.run_on_main(start_progress)
|
||||
return self._op(mw.col)
|
||||
|
||||
def wrapped_done(future: Future) -> None:
|
||||
assert mw
|
||||
|
||||
if self._progress:
|
||||
mw.progress.finish()
|
||||
|
||||
mw._decrease_background_ops()
|
||||
# did something go wrong?
|
||||
if exception := future.exception():
|
||||
|
@ -261,120 +263,16 @@ class QueryOp(Generic[T]):
|
|||
op: Callable[[], T],
|
||||
on_done: Callable[[Future], None],
|
||||
) -> None:
|
||||
mw.taskman.run_in_background(op, on_done)
|
||||
|
||||
|
||||
class OpWithBackendProgress(ABC):
|
||||
"""Periodically queries the backend for progress updates, and enables abortion.
|
||||
|
||||
Requires a key for a value on the `Progress` proto message."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
key: Literal["importing", "exporting"],
|
||||
**kwargs: Any,
|
||||
):
|
||||
self._key = key
|
||||
self._timer = QTimer()
|
||||
self._timer.setSingleShot(False)
|
||||
self._timer.setInterval(100)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
mw: aqt.main.AnkiQt,
|
||||
op: Callable,
|
||||
on_done: Callable[[Future], None],
|
||||
) -> None:
|
||||
if not (dialog := mw.progress.start(immediate=True, parent=mw)):
|
||||
print("Progress dialog already running; aborting will not work")
|
||||
|
||||
def on_progress() -> None:
|
||||
assert mw
|
||||
|
||||
progress = mw.backend.latest_progress()
|
||||
if not (label := label_from_progress(progress, self._key)):
|
||||
return
|
||||
if dialog and dialog.wantCancel:
|
||||
mw.backend.set_wants_abort()
|
||||
mw.taskman.run_on_main(lambda: mw.progress.update(label=label))
|
||||
|
||||
def wrapped_on_done(future: Future) -> None:
|
||||
self._timer.deleteLater()
|
||||
assert mw
|
||||
mw.progress.finish()
|
||||
on_done(future)
|
||||
|
||||
qconnect(self._timer.timeout, on_progress)
|
||||
self._timer.start()
|
||||
mw.taskman.run_in_background(task=op, on_done=wrapped_on_done)
|
||||
|
||||
|
||||
def label_from_progress(
|
||||
progress: Progress,
|
||||
key: Literal["importing", "exporting"],
|
||||
) -> str | None:
|
||||
if not progress.HasField(key):
|
||||
return None
|
||||
if key == "importing":
|
||||
return progress.importing
|
||||
if key == "exporting":
|
||||
return tr.exporting_exported_media_file(count=progress.exporting)
|
||||
|
||||
|
||||
class CollectionOpWithBackendProgress(OpWithBackendProgress, CollectionOp):
|
||||
pass
|
||||
|
||||
|
||||
class QueryOpWithBackendProgress(OpWithBackendProgress, QueryOp):
|
||||
def with_progress(self, *_args: Any) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ClosedCollectionOp(CollectionOp):
|
||||
"""For CollectionOps that need to be run on a closed collection.
|
||||
|
||||
If a collection is open, backs it up and unloads it, before running the op.
|
||||
Reloads it, if that has not been done by a callback, yet.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget,
|
||||
op: Callable[[], ResultWithChanges],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(parent, lambda _: op(), *args, **kwargs)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
mw: aqt.main.AnkiQt,
|
||||
op: Callable[[], ResultWithChanges],
|
||||
on_done: Callable[[Future], None],
|
||||
) -> None:
|
||||
if mw.col:
|
||||
QueryOp(
|
||||
parent=mw,
|
||||
op=lambda _: mw.create_backup_now(),
|
||||
success=lambda _: mw.unloadCollection(
|
||||
lambda: super(ClosedCollectionOp, self)._run(mw, op, on_done)
|
||||
),
|
||||
).with_progress().run_in_background()
|
||||
label = self._progress if isinstance(self._progress, str) else None
|
||||
if self._label_from_progress:
|
||||
mw.taskman.with_backend_progress(
|
||||
op,
|
||||
self._label_from_progress,
|
||||
on_done=on_done,
|
||||
start_label=label,
|
||||
parent=self._parent,
|
||||
)
|
||||
elif self._progress:
|
||||
mw.taskman.with_progress(op, on_done, label=label, parent=self._parent)
|
||||
else:
|
||||
super()._run(mw, op, on_done)
|
||||
|
||||
def _finish_op(
|
||||
self,
|
||||
_mw: aqt.main.AnkiQt,
|
||||
_result: ResultWithChanges,
|
||||
_initiator: object | None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class ClosedCollectionOpWithBackendProgress(
|
||||
ClosedCollectionOp, CollectionOpWithBackendProgress
|
||||
):
|
||||
"""See ClosedCollectionOp and CollectionOpWithBackendProgress."""
|
||||
mw.taskman.run_in_background(op, on_done)
|
||||
|
|
|
@ -6,6 +6,7 @@ import time
|
|||
|
||||
import aqt.forms
|
||||
from anki._legacy import print_deprecation_warning
|
||||
from anki.collection import Progress
|
||||
from aqt.qt import *
|
||||
from aqt.utils import disable_help_button, tr
|
||||
|
||||
|
@ -23,6 +24,7 @@ class ProgressManager:
|
|||
self._busy_cursor_timer: QTimer | None = None
|
||||
self._win: ProgressDialog | None = None
|
||||
self._levels = 0
|
||||
self._backend_timer: QTimer | None = None
|
||||
|
||||
# Safer timers
|
||||
##########################################################################
|
||||
|
@ -166,6 +168,32 @@ class ProgressManager:
|
|||
qconnect(self._show_timer.timeout, self._on_show_timer)
|
||||
return self._win
|
||||
|
||||
def start_with_backend_updates(
|
||||
self,
|
||||
label_from_progress: Callable[[Progress], str | None],
|
||||
start_label: str | None = None,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
self._backend_timer = QTimer()
|
||||
self._backend_timer.setSingleShot(False)
|
||||
self._backend_timer.setInterval(100)
|
||||
|
||||
if not (dialog := self.start(immediate=True, label=start_label, parent=parent)):
|
||||
print("Progress dialog already running; aborting will not work")
|
||||
|
||||
def on_progress() -> None:
|
||||
assert self.mw
|
||||
|
||||
progress = self.mw.backend.latest_progress()
|
||||
if not (label := label_from_progress(progress)):
|
||||
return
|
||||
if dialog and dialog.wantCancel:
|
||||
self.mw.backend.set_wants_abort()
|
||||
self.update(label=label)
|
||||
|
||||
qconnect(self._backend_timer.timeout, on_progress)
|
||||
self._backend_timer.start()
|
||||
|
||||
def update(
|
||||
self,
|
||||
label: str | None = None,
|
||||
|
@ -204,6 +232,9 @@ class ProgressManager:
|
|||
if self._show_timer:
|
||||
self._show_timer.stop()
|
||||
self._show_timer = None
|
||||
if self._backend_timer:
|
||||
self._backend_timer.deleteLater()
|
||||
self._backend_timer = None
|
||||
|
||||
def clear(self) -> None:
|
||||
"Restore the interface after an error."
|
||||
|
|
|
@ -15,6 +15,7 @@ from threading import Lock
|
|||
from typing import Any, Callable
|
||||
|
||||
import aqt
|
||||
from anki.collection import Progress
|
||||
from aqt.qt import *
|
||||
|
||||
Closure = Callable[[], None]
|
||||
|
@ -89,6 +90,27 @@ class TaskManager(QObject):
|
|||
|
||||
self.run_in_background(task, wrapped_done)
|
||||
|
||||
def with_backend_progress(
|
||||
self,
|
||||
task: Callable,
|
||||
label_from_progress: Callable[[Progress], str | None],
|
||||
on_done: Callable[[Future], None] | None = None,
|
||||
parent: QWidget | None = None,
|
||||
start_label: str | None = None,
|
||||
) -> None:
|
||||
self.mw.progress.start_with_backend_updates(
|
||||
label_from_progress,
|
||||
parent=parent,
|
||||
start_label=start_label,
|
||||
)
|
||||
|
||||
def wrapped_done(fut: Future) -> None:
|
||||
self.mw.progress.finish()
|
||||
if on_done:
|
||||
on_done(fut)
|
||||
|
||||
self.run_in_background(task, wrapped_done)
|
||||
|
||||
def _on_closures_pending(self) -> None:
|
||||
"""Run any pending closures. This runs in the main thread."""
|
||||
with self._closures_lock:
|
||||
|
|
Loading…
Reference in a new issue