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:
RumovZ 2022-04-28 17:05:18 +02:00
parent 702f47c522
commit 19664a0e47
6 changed files with 135 additions and 172 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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