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 re
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from typing import Sequence, Type from typing import Sequence, Type
import aqt.forms import aqt.forms
import aqt.main 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.decks import DeckId, DeckNameId
from anki.notes import NoteId from anki.notes import NoteId
from aqt import gui_hooks from aqt import gui_hooks
from aqt.errors import show_exception from aqt.errors import show_exception
from aqt.operations import QueryOpWithBackendProgress from aqt.operations import QueryOp
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
checkInvalidFilename, checkInvalidFilename,
@ -180,7 +179,7 @@ class ColpkgExporter(Exporter):
@staticmethod @staticmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None: def export(mw: aqt.main.AnkiQt, options: Options) -> None:
def on_success(_future: Future) -> None: def on_success(_: None) -> None:
mw.reopen() mw.reopen()
tooltip(tr.exporting_collection_exported(), parent=mw) tooltip(tr.exporting_collection_exported(), parent=mw)
@ -189,14 +188,15 @@ class ColpkgExporter(Exporter):
show_exception(parent=mw, exception=exception) show_exception(parent=mw, exception=exception)
gui_hooks.collection_will_temporarily_close(mw.col) gui_hooks.collection_will_temporarily_close(mw.col)
QueryOpWithBackendProgress( QueryOp(
parent=mw, parent=mw,
op=lambda col: col.export_collection_package( op=lambda col: col.export_collection_package(
options.out_path, include_media=options.include_media, legacy=False options.out_path, include_media=options.include_media, legacy=False
), ),
success=on_success, success=on_success,
key="exporting", ).with_backend_progress(export_progress_label).failure(
).failure(on_failure).run_in_background() on_failure
).run_in_background()
class ApkgExporter(Exporter): class ApkgExporter(Exporter):
@ -211,7 +211,7 @@ class ApkgExporter(Exporter):
@staticmethod @staticmethod
def export(mw: aqt.main.AnkiQt, options: Options) -> None: def export(mw: aqt.main.AnkiQt, options: Options) -> None:
QueryOpWithBackendProgress( QueryOp(
parent=mw, parent=mw,
op=lambda col: col.export_anki_package( op=lambda col: col.export_anki_package(
out_path=options.out_path, out_path=options.out_path,
@ -219,8 +219,13 @@ class ApkgExporter(Exporter):
with_scheduling=options.include_scheduling, with_scheduling=options.include_scheduling,
with_media=options.include_media, with_media=options.include_media,
), ),
success=lambda fut: tooltip( success=lambda count: tooltip(
tr.exporting_note_exported(count=fut), parent=mw tr.exporting_note_exported(count=count), parent=mw
), ),
key="exporting", ).with_backend_progress(export_progress_label).run_in_background()
).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 __future__ import annotations
from concurrent.futures import Future from typing import Sequence
import aqt.main import aqt.main
from anki.collection import Collection, ImportLogWithChanges, Progress
from anki.errors import Interrupted from anki.errors import Interrupted
from aqt.operations import ( from aqt.operations import CollectionOp, QueryOp
ClosedCollectionOpWithBackendProgress,
CollectionOpWithBackendProgress,
)
from aqt.qt import * from aqt.qt import *
from aqt.utils import askUser, getFile, showInfo, showText, showWarning, tooltip, tr 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 import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
def on_success(_future: Future) -> None: def on_success() -> None:
mw.loadCollection() mw.loadCollection()
tooltip(tr.importing_importing_complete()) 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): if not isinstance(err, Interrupted):
showWarning(str(err)) showWarning(str(err))
import_collection_package_op(mw, file).success(on_success).failure( QueryOp(
on_failure parent=mw,
).run_in_background() 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( def import_collection_package_op(
mw: aqt.main.AnkiQt, path: str mw: aqt.main.AnkiQt, path: str, success: Callable[[], None]
) -> ClosedCollectionOpWithBackendProgress: ) -> QueryOp[None]:
def op() -> None: def op(_: Collection) -> None:
col_path = mw.pm.collectionPath() col_path = mw.pm.collectionPath()
media_folder = os.path.join(mw.pm.profileFolder(), "collection.media") media_folder = os.path.join(mw.pm.profileFolder(), "collection.media")
mw.backend.import_collection_package( mw.backend.import_collection_package(
col_path=col_path, backup_path=path, media_folder=media_folder col_path=col_path, backup_path=path, media_folder=media_folder
) )
return ClosedCollectionOpWithBackendProgress( return QueryOp(parent=mw, op=op, success=lambda _: success()).with_backend_progress(
parent=mw, import_progress_label
op=op,
key="importing",
) )
def import_anki_package(mw: aqt.main.AnkiQt, path: str) -> None: def import_anki_package(mw: aqt.main.AnkiQt, path: str) -> None:
CollectionOpWithBackendProgress( CollectionOp(
parent=mw, parent=mw,
op=lambda col: col.import_anki_package(path), op=lambda col: col.import_anki_package(path),
key="importing", ).with_backend_progress(import_progress_label).success(
).success(show_import_log).run_in_background() show_import_log
).run_in_background()
def show_import_log(future: Future) -> None: def show_import_log(log_with_changes: ImportLogWithChanges) -> None:
log = future.log # type: ignore log = log_with_changes.log # type: ignore
total = len(log.conflicting) + len(log.updated) + len(log.new) + len(log.duplicate) total = len(log.conflicting) + len(log.updated) + len(log.new) + len(log.duplicate)
text = f"""{tr.importing_notes_found_in_file(val=total)} 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) 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) 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 self.restoring_backup = True
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been()) showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
import_collection_package_op(self, path).success( import_collection_package_op(
lambda _: self.onOpenProfile() self, path, success=self.onOpenProfile
).run_in_background() ).run_in_background()
def _on_downgrade(self) -> None: def _on_downgrade(self) -> None:

View file

@ -3,9 +3,8 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC
from concurrent.futures._base import Future 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
import aqt.gui_hooks import aqt.gui_hooks
@ -20,8 +19,7 @@ from anki.collection import (
Progress, Progress,
) )
from aqt.errors import show_exception from aqt.errors import show_exception
from aqt.qt import QTimer, QWidget, qconnect from aqt.qt import QWidget
from aqt.utils import tr
class HasChangesProperty(Protocol): class HasChangesProperty(Protocol):
@ -70,6 +68,7 @@ class CollectionOp(Generic[ResultWithChanges]):
_success: Callable[[ResultWithChanges], Any] | None = None _success: Callable[[ResultWithChanges], Any] | None = None
_failure: Callable[[Exception], 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]): def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
self._parent = parent self._parent = parent
@ -87,6 +86,12 @@ class CollectionOp(Generic[ResultWithChanges]):
self._failure = failure self._failure = failure
return self 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: def run_in_background(self, *, initiator: object | None = None) -> None:
from aqt import mw from aqt import mw
@ -128,7 +133,12 @@ class CollectionOp(Generic[ResultWithChanges]):
op: Callable[[], ResultWithChanges], op: Callable[[], ResultWithChanges],
on_done: Callable[[Future], None], on_done: Callable[[Future], None],
) -> 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( def _finish_op(
self, mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None self, mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None
@ -185,6 +195,7 @@ class QueryOp(Generic[T]):
_failure: Callable[[Exception], Any] | None = None _failure: Callable[[Exception], Any] | None = None
_progress: bool | str = False _progress: bool | str = False
_label_from_progress: Callable[[Progress], str | None] | None = None
def __init__( def __init__(
self, self,
@ -209,6 +220,12 @@ class QueryOp(Generic[T]):
self._progress = label or True self._progress = label or True
return self 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: def run_in_background(self) -> None:
from aqt import mw from aqt import mw
@ -218,26 +235,11 @@ class QueryOp(Generic[T]):
def wrapped_op() -> T: def wrapped_op() -> T:
assert mw 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) return self._op(mw.col)
def wrapped_done(future: Future) -> None: def wrapped_done(future: Future) -> None:
assert mw assert mw
if self._progress:
mw.progress.finish()
mw._decrease_background_ops() mw._decrease_background_ops()
# did something go wrong? # did something go wrong?
if exception := future.exception(): if exception := future.exception():
@ -261,120 +263,16 @@ class QueryOp(Generic[T]):
op: Callable[[], T], op: Callable[[], T],
on_done: Callable[[Future], None], on_done: Callable[[Future], None],
) -> None: ) -> None:
mw.taskman.run_in_background(op, on_done) label = self._progress if isinstance(self._progress, str) else None
if self._label_from_progress:
mw.taskman.with_backend_progress(
class OpWithBackendProgress(ABC): op,
"""Periodically queries the backend for progress updates, and enables abortion. self._label_from_progress,
on_done=on_done,
Requires a key for a value on the `Progress` proto message.""" start_label=label,
parent=self._parent,
def __init__( )
self, elif self._progress:
*args: Any, mw.taskman.with_progress(op, on_done, label=label, parent=self._parent)
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()
else: else:
super()._run(mw, op, on_done) mw.taskman.run_in_background(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."""

View file

@ -6,6 +6,7 @@ import time
import aqt.forms import aqt.forms
from anki._legacy import print_deprecation_warning from anki._legacy import print_deprecation_warning
from anki.collection import Progress
from aqt.qt import * from aqt.qt import *
from aqt.utils import disable_help_button, tr from aqt.utils import disable_help_button, tr
@ -23,6 +24,7 @@ class ProgressManager:
self._busy_cursor_timer: QTimer | None = None self._busy_cursor_timer: QTimer | None = None
self._win: ProgressDialog | None = None self._win: ProgressDialog | None = None
self._levels = 0 self._levels = 0
self._backend_timer: QTimer | None = None
# Safer timers # Safer timers
########################################################################## ##########################################################################
@ -166,6 +168,32 @@ class ProgressManager:
qconnect(self._show_timer.timeout, self._on_show_timer) qconnect(self._show_timer.timeout, self._on_show_timer)
return self._win 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( def update(
self, self,
label: str | None = None, label: str | None = None,
@ -204,6 +232,9 @@ class ProgressManager:
if self._show_timer: if self._show_timer:
self._show_timer.stop() self._show_timer.stop()
self._show_timer = None self._show_timer = None
if self._backend_timer:
self._backend_timer.deleteLater()
self._backend_timer = None
def clear(self) -> None: def clear(self) -> None:
"Restore the interface after an error." "Restore the interface after an error."

View file

@ -15,6 +15,7 @@ from threading import Lock
from typing import Any, Callable from typing import Any, Callable
import aqt import aqt
from anki.collection import Progress
from aqt.qt import * from aqt.qt import *
Closure = Callable[[], None] Closure = Callable[[], None]
@ -89,6 +90,27 @@ class TaskManager(QObject):
self.run_in_background(task, wrapped_done) 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: def _on_closures_pending(self) -> None:
"""Run any pending closures. This runs in the main thread.""" """Run any pending closures. This runs in the main thread."""
with self._closures_lock: with self._closures_lock: