Anki/qt/aqt/update.py
Damien Elmes bb1b289690 Add some helpers to allow add-ons to install packages into the venv
While something we probably don't want to encourage much of, this
may enable some previously-unshared add-ons.

https://forums.ankiweb.net/t/bundling-numpy-in-an-add-on/62669/5

The 'uv add' command is transaction, so if an add-on tries to inject
incompatible dependencies into the environment, the venv will be
left as-is. And each Anki upgrade/downgrade resets the requirements,
so the requested packages shouldn't cause errors down the line.

Sample add-on:

import subprocess
from aqt import mw
from aqt.operations import QueryOp
from aqt.qt import QAction
from aqt.utils import showInfo

def ensure_spacy(col):
    print("trying to import spacy")
    try:
        import spacy
        print("successful import")
        return
    except Exception as e:
        print("error importing:", e)

    print("attempting add")
    try:
        from aqt.package import add_python_requirements as add
    except Exception as e:
        raise Exception(f"package unavailable, can't install: {e}")
    # be explicit about version, or Anki beta users will get
    # a beta wheel that may break
    (success, output) = add(["spacy==3.8.7", "https://github.com/explosion/spacy-models/releases/download/ko_core_news_sm-3.8.0/ko_core_news_sm-3.8.0-py3-none-any.whl"])
    if not success:
        raise Exception(f"adding failed: {output}")

    print("success")

    # alterantively:
    # from aqt.package import venv_binary
    # subprocess.run([venv_binary("spacy"), "download", "ko_core_news_sm"], check=True)
    # print("model added")

    # large packages will freeze for a while on first import on macOS
    import spacy
    print("spacy import successful")

def activate_spacy():
    def on_success(res):
        mw.progress.single_shot(1000, lambda: showInfo("Spacy installed"))

    QueryOp(parent=mw, op=ensure_spacy, success=on_success).with_progress("Installing spacy...").run_in_background()

action = QAction("Activate Spacy", mw)
action.triggered.connect(activate_spacy)
mw.form.menuTools.addAction(action)
2025-07-04 14:23:04 +07:00

91 lines
3 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import aqt
from anki.buildinfo import buildhash
from anki.collection import CheckForUpdateResponse, Collection
from anki.utils import dev_mode, int_time, int_version, plat_desc
from aqt.operations import QueryOp
from aqt.package import (
launcher_executable as _launcher_executable,
)
from aqt.package import (
update_and_restart as _update_and_restart,
)
from aqt.qt import *
from aqt.utils import openLink, show_warning, showText, tr
def check_for_update() -> None:
from aqt import mw
def do_check(_col: Collection) -> CheckForUpdateResponse:
return mw.backend.check_for_update(
version=int_version(),
buildhash=buildhash,
os=plat_desc(),
install_id=mw.pm.meta["id"],
last_message_id=max(0, mw.pm.meta["lastMsg"]),
)
def on_done(resp: CheckForUpdateResponse) -> None:
# is clock off?
if not dev_mode:
diff = abs(resp.current_time - int_time())
if diff > 300:
diff_text = tr.qt_misc_second(count=diff)
warn = (
tr.qt_misc_in_order_to_ensure_your_collection(val="%s") % diff_text
)
show_warning(
warn,
parent=mw,
textFormat=Qt.TextFormat.RichText,
callback=mw.app.closeAllWindows,
)
return
# should we show a message?
if msg := resp.message:
showText(msg, parent=mw, type="html")
mw.pm.meta["lastMsg"] = resp.last_message_id
# has Anki been updated?
if ver := resp.new_version:
if mw.pm.meta.get("suppressUpdate", None) != ver:
prompt_to_update(mw, ver)
def on_fail(exc: Exception) -> None:
print(f"update check failed: {exc}")
QueryOp(parent=mw, op=do_check, success=on_done).failure(
on_fail
).without_collection().run_in_background()
def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None:
msg = (
tr.qt_misc_anki_updatedanki_has_been_released(val=ver)
+ tr.qt_misc_would_you_like_to_download_it()
)
msgbox = QMessageBox(mw)
msgbox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setIcon(QMessageBox.Icon.Information)
msgbox.setText(msg)
button = QPushButton(tr.qt_misc_ignore_this_update())
msgbox.addButton(button, QMessageBox.ButtonRole.RejectRole)
msgbox.setDefaultButton(QMessageBox.StandardButton.Yes)
ret = msgbox.exec()
if msgbox.clickedButton() == button:
# ignore this update
mw.pm.meta["suppressUpdate"] = ver
elif ret == QMessageBox.StandardButton.Yes:
if _launcher_executable():
_update_and_restart()
else:
openLink(aqt.appWebsiteDownloadSection)