From e025a44ea3e545bdced9f3a361de38c05218c55a Mon Sep 17 00:00:00 2001 From: oakkitten Date: Tue, 12 Apr 2022 19:50:36 +0100 Subject: [PATCH 1/6] Tests: make tests pass for Anki 2.1.50 (Qt5) * require Python 3.10 * prevent Anki from doing backups, again * restore cwd that deck exporter changes for some wild reason --- .github/workflows/main.yml | 9 ++++++++- .gitignore | 1 + plugin/__init__.py | 4 +--- tests/conftest.py | 26 +++++++++++++++++++------- tests/test_misc.py | 16 ++++++++++++++++ tox.ini | 9 ++++++--- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 826f9c1..72bf0ce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,15 @@ jobs: sudo apt-get update sudo apt-get install -y pyqt5-dev-tools xvfb - - name: Setup Python + - name: Setup Python 3.8 uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Setup Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 - name: Install tox run: pip install tox diff --git a/.gitignore b/.gitignore index 34ac317..fc4b3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ AnkiConnect.zip meta.json .idea/ +.tox/ diff --git a/plugin/__init__.py b/plugin/__init__.py index 6b50f4d..eceaa77 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1425,9 +1425,7 @@ def openNewWindow(): if savedMid: deck['mid'] = savedMid - addCards.editor.note = ankiNote - addCards.editor.loadNote() - addCards.editor.updateTags() + addCards.editor.set_note(ankiNote) addCards.activateWindow() diff --git a/tests/conftest.py b/tests/conftest.py index 9fbbcea..c33f006 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import aqt.operations.note import pytest +import anki.collection from PyQt5 import QtTest from _pytest.monkeypatch import MonkeyPatch # noqa from pytest_anki._launch import anki_running, temporary_user # noqa @@ -77,22 +78,33 @@ def close(self): yield +@contextmanager +def anki_patched_to_prevent_backups(): + with MonkeyPatch().context() as monkey: + if ac._anki21_version < 50: + monkey.setitem(aqt.profiles.profileConf, "numBackups", 0) + else: + monkey.setattr(anki.collection.Collection, "create_backup", + lambda *args, **kwargs: True) + yield + + @contextmanager def empty_anki_session_started(): with waitress_patched_to_prevent_it_from_dying(): - with anki_running( - qtbot=None, # noqa - enable_web_debugging=False, - profile_name="test_user", - ) as session: - yield session + with anki_patched_to_prevent_backups(): + with anki_running( + qtbot=None, # noqa + enable_web_debugging=False, + profile_name="test_user", + ) as session: + yield session @contextmanager def profile_created_and_loaded(session): with temporary_user(session.base, "test_user", "en_US"): with session.profile_loaded(): - aqt.mw.pm.profile["numBackups"] = 0 # don't try to do backups yield session diff --git a/tests/test_misc.py b/tests/test_misc.py index b5feaa7..16c8e5a 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,4 +1,7 @@ +import os + import aqt +import pytest from conftest import ac, anki_connect_config_loaded, \ set_up_test_deck_and_test_model_and_two_notes, \ @@ -33,6 +36,19 @@ def test_loadProfile(self, session_with_profile_loaded): class TestExportImport: + # since Anki 2.1.50, exporting media for some wild reason + # will change the current working directory, which then gets removed. + # see `exporting.py`, ctrl-f `os.chdir(self.mediaDir)` + @pytest.fixture(autouse=True) + def current_working_directory_preserved(self): + cwd = os.getcwd() + yield + + try: + os.getcwd() + except FileNotFoundError: + os.chdir(cwd) + def test_exportPackage(self, session_with_profile_loaded, setup): filename = session_with_profile_loaded.base + "/export.apkg" ac.exportPackage(deck="test_deck", path=filename) diff --git a/tox.ini b/tox.ini index 96e56f5..b615d04 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,7 @@ minversion = 3.24 skipsdist = true skip_install = true -envlist = py38-anki{45,46,47,48,49} +envlist = py38-anki{45,46,47,48,49},py39-anki50qt5 [testenv] commands = @@ -58,7 +58,7 @@ allowlist_externals = deps = pytest==7.1.1 pytest-forked==1.4.0 - pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@17d19043 + pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@97708344 anki45: anki==2.1.45 anki45: aqt==2.1.45 @@ -73,4 +73,7 @@ deps = anki48: aqt==2.1.48 anki49: anki==2.1.49 - anki49: aqt==2.1.49 \ No newline at end of file + anki49: aqt==2.1.49 + + anki50qt5: anki==2.1.50 + anki50qt5: aqt[qt5]==2.1.50 From 901d92b067efe61bf4d702c3de161481e7d243ea Mon Sep 17 00:00:00 2001 From: oakkitten Date: Tue, 12 Apr 2022 23:09:04 +0100 Subject: [PATCH 2/6] Tests: make tests pass for Anki 2.1.50 (Qt6) * Import things from `aqt.qt` not `PyQt5`/`PyQt6` * When testing with Qt6, disable Anki's Qt5 compatibility mode * Depend on `pytest-anki` that is also Qt version agnostic --- plugin/__init__.py | 10 ++-------- plugin/edit.py | 2 +- tests/conftest.py | 6 +++++- tox.ini | 10 +++++++--- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/plugin/__init__.py b/plugin/__init__.py index eceaa77..b947a06 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -20,26 +20,20 @@ import json import os import os.path -import random import re -import string import time import unicodedata -from PyQt5 import QtCore -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QMessageBox, QCheckBox - import anki import anki.exporting import anki.storage import aqt from anki.cards import Card from anki.consts import MODEL_CLOZE - from anki.exporting import AnkiPackageExporter from anki.importing import AnkiPackageImporter from anki.notes import Note +from aqt.qt import Qt, QTimer, QMessageBox, QCheckBox from .edit import Edit @@ -391,7 +385,7 @@ def requestPermission(self, origin, allowed): msg.setStandardButtons(QMessageBox.Yes|QMessageBox.No) msg.setDefaultButton(QMessageBox.No) msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg)) - msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + msg.setWindowFlags(Qt.WindowStaysOnTopHint) pressedButton = msg.exec_() if pressedButton == QMessageBox.Yes: diff --git a/plugin/edit.py b/plugin/edit.py index 1e063af..066df0e 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -184,7 +184,7 @@ class Edit(aqt.editcurrent.EditCurrent): # upon a request to open the dialog via `aqt.dialogs.open()`, # the manager will call either the constructor or the `reopen` method def __init__(self, note): - QDialog.__init__(self, None, Qt.Window) + QDialog.__init__(self, None, Qt.WindowType.Window) aqt.mw.garbage_collect_on_dialog_finish(self) self.form = aqt.forms.editcurrent.Ui_Dialog() self.form.setupUi(self) diff --git a/tests/conftest.py b/tests/conftest.py index c33f006..cb26734 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ import aqt.operations.note import pytest import anki.collection -from PyQt5 import QtTest from _pytest.monkeypatch import MonkeyPatch # noqa from pytest_anki._launch import anki_running, temporary_user # noqa from waitress import wasyncore @@ -15,6 +14,11 @@ from plugin.edit import Edit from plugin.util import DEFAULT_CONFIG +try: + from PyQt6 import QtTest +except ImportError: + from PyQt5 import QtTest + ac = AnkiConnect() diff --git a/tox.ini b/tox.ini index b615d04..c8d3a41 100644 --- a/tox.ini +++ b/tox.ini @@ -40,25 +40,26 @@ # LIBGL_ALWAYS_INDIRECT=1 # QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" # QT_DEBUG_PLUGINS=1 -# ANKIDEV=true +# ANKIDEV=1 [tox] minversion = 3.24 skipsdist = true skip_install = true -envlist = py38-anki{45,46,47,48,49},py39-anki50qt5 +envlist = py38-anki{45,46,47,48,49},py39-anki{50qt5,50qt6} [testenv] commands = xvfb-run python -m pytest {posargs} setenv = HOME={envdir}/home + anki50qt6: DISABLE_QT5_COMPAT=1 allowlist_externals = xvfb-run deps = pytest==7.1.1 pytest-forked==1.4.0 - pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@97708344 + pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@a0d27aa5 anki45: anki==2.1.45 anki45: aqt==2.1.45 @@ -77,3 +78,6 @@ deps = anki50qt5: anki==2.1.50 anki50qt5: aqt[qt5]==2.1.50 + + anki50qt6: anki==2.1.50 + anki50qt6: aqt[qt6]==2.1.50 From 056e722187551ba91fc7e06b654e75284004ecca Mon Sep 17 00:00:00 2001 From: oakkitten Date: Tue, 12 Apr 2022 23:22:51 +0100 Subject: [PATCH 3/6] Edit dialog: fix editor buttons on Anki 2.1.50 Also add a few tests for the buttons to make sure that they get actually added and enabled/disabled. --- plugin/edit.py | 104 +++++++++++++++++++++++++++++++-------------- tests/test_edit.py | 83 +++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 33 deletions(-) diff --git a/plugin/edit.py b/plugin/edit.py index 066df0e..2ae9a89 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -1,5 +1,6 @@ import aqt import aqt.editor +import aqt.browser.previewer from aqt import gui_hooks from aqt.qt import QDialog, Qt, QKeySequence, QShortcut from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip @@ -24,6 +25,8 @@ DOMAIN_PREFIX = "foosoft.ankiconnect." +anki_version = tuple(int(segment) for segment in aqt.appVersion.split(".")) + def get_note_by_note_id(note_id): return aqt.mw.col.get_note(note_id) @@ -225,8 +228,6 @@ def on_operation_did_execute(self, changes, handler): if changes.note_text and handler is not self.editor: self.reload_notes_after_user_action_elsewhere() - # adjusting buttons right after initializing doesn't have any effect; - # this seems to do the trick def editor_did_load_note(self, _editor): self.enable_disable_next_and_previous_buttons() @@ -304,29 +305,66 @@ def setup_editor_buttons(self): gui_hooks.editor_did_init.append(self.add_preview_button) gui_hooks.editor_did_init_buttons.append(self.add_right_hand_side_buttons) - self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self) + # on Anki 2.1.50, browser mode makes the Preview button visible + extra_kwargs = {} if anki_version < (2, 1, 50) else { + "editor_mode": aqt.editor.EditorMode.BROWSER + } + + self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self, + **extra_kwargs) gui_hooks.editor_did_init_buttons.remove(self.add_right_hand_side_buttons) gui_hooks.editor_did_init.remove(self.add_preview_button) - # taken from `setupEditor` of browser.py - # PreviewButton calls pycmd `preview`, which is hardcoded. - # copying _links is needed so that opening Anki's browser does not - # screw them up as they are apparently shared between instances?! + # * on Anki < 2.1.50, make the button via js (`setupEditor` of browser.py); + # also, make a copy of _links so that opening Anki's browser does not + # screw them up as they are apparently shared between instances?! + # the last part seems to have been fixed in Anki 2.1.50 + # * on Anki 2.1.50, the button is created by setting editor mode, + # see above; so we only need to add the link. def add_preview_button(self, editor): QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.show_preview) - editor._links = editor._links.copy() - editor._links["preview"] = self.show_preview - editor.web.eval(""" - $editorToolbar.then(({notetypeButtons}) => - notetypeButtons.appendButton( - {component: editorToolbar.PreviewButton, id: 'preview'} - ) - ); - """) - + if anki_version < (2, 1, 50): + editor._links = editor._links.copy() + editor.web.eval(""" + $editorToolbar.then(({notetypeButtons}) => + notetypeButtons.appendButton( + {component: editorToolbar.PreviewButton, id: 'preview'} + ) + ); + """) + + editor._links["preview"] = lambda _editor: self.show_preview() and None + + # * on Anki < 2.1.50, button style is okay-ish from get-go, + # except when disabled; adding class `btn` fixes that; + # * on Anki 2.1.50, buttons have weird font size and are square'; + # the style below makes them in line with left-hand side buttons def add_right_hand_side_buttons(self, buttons, editor): + if anki_version < (2, 1, 50): + extra_button_class = "btn" + else: + extra_button_class = "anki-connect-button" + editor.web.eval(""" + (function(){ + const style = document.createElement("style"); + style.innerHTML = ` + .anki-connect-button { + white-space: nowrap; + width: auto; + padding: 0 2px; + font-size: var(--base-font-size); + } + .anki-connect-button:disabled { + pointer-events: none; + opacity: .4; + } + `; + document.head.appendChild(style); + })(); + """) + def add(cmd, function, label, tip, keys): button_html = editor.addButton( icon=None, @@ -338,30 +376,34 @@ def add(cmd, function, label, tip, keys): keys=keys, ) - # adding class `btn` properly styles buttons when disabled - button_html = button_html.replace('class="', 'class="btn ') + button_html = button_html.replace('class="', + f'class="{extra_button_class} ') buttons.append(button_html) add("browse", self.show_browser, "Browse", "Browse", "Ctrl+F") add("previous", self.show_previous, "<", "Previous", "Alt+Left") add("next", self.show_next, ">", "Next", "Alt+Right") + def run_javascript_after_toolbar_ready(self, js): + js = f"setTimeout(function() {{ {js} }}, 1)" + if anki_version < (2, 1, 50): + js = f'$editorToolbar.then(({{ toolbar }}) => {js})' + else: + js = f'require("anki/ui").loaded.then(() => {js})' + self.editor.web.eval(js) + def enable_disable_next_and_previous_buttons(self): def to_js(boolean): return "true" if boolean else "false" - disable_previous = to_js(not(history.has_note_to_left_of(self.note))) - disable_next = to_js(not(history.has_note_to_right_of(self.note))) - - self.editor.web.eval(f""" - $editorToolbar.then(({{ toolbar }}) => {{ - setTimeout(function() {{ - document.getElementById("{DOMAIN_PREFIX}previous") - .disabled = {disable_previous}; - document.getElementById("{DOMAIN_PREFIX}next") - .disabled = {disable_next}; - }}, 1); - }}); + disable_previous = not(history.has_note_to_left_of(self.note)) + disable_next = not(history.has_note_to_right_of(self.note)) + + self.run_javascript_after_toolbar_ready(f""" + document.getElementById("{DOMAIN_PREFIX}previous") + .disabled = {to_js(disable_previous)}; + document.getElementById("{DOMAIN_PREFIX}next") + .disabled = {to_js(disable_next)}; """) ########################################################################## diff --git a/tests/test_edit.py b/tests/test_edit.py index 6886fa8..71b1615 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -1,8 +1,63 @@ +from dataclasses import dataclass +from unittest.mock import MagicMock + import aqt.operations.note import pytest -from conftest import get_dialog_instance -from plugin.edit import Edit, DecentPreviewer, history +from conftest import get_dialog_instance, wait_until +from plugin.edit import Edit, DecentPreviewer, history, DOMAIN_PREFIX + + +NOTHING = object() + + +class Value: + def __init__(self): + self.value = NOTHING + + def set(self, value): + self.value = value + + def has_been_set(self): + return self.value is not NOTHING + + +@dataclass +class JavascriptDialogButtonManipulator: + dialog: ... + + def eval_js(self, js): + evaluation_result = Value() + self.dialog.editor.web.evalWithCallback(js, evaluation_result.set) + wait_until(evaluation_result.has_been_set) + return evaluation_result.value + + def wait_until_toolbar_buttons_are_ready(self): + ready_flag = Value() + self.dialog.editor._links["set_ready_flag"] = ready_flag.set # noqa + self.dialog.run_javascript_after_toolbar_ready("pycmd('set_ready_flag');") + wait_until(ready_flag.has_been_set) + + # preview button doesn't have an id, so find by label + def click_preview_button(self): + self.eval_js(""" + document.evaluate("//button[text()='Preview']", document) + .iterateNext() + .click() + """) + + def click_button(self, button_id): + self.eval_js(f""" + document.getElementById("{DOMAIN_PREFIX}{button_id}").click() + """) + + def is_button_disabled(self, button_id): + return self.eval_js(f""" + document.getElementById("{DOMAIN_PREFIX}{button_id}").disabled + """) + + +############################################################################## def test_edit_dialog_opens(setup): @@ -99,6 +154,30 @@ def test_navigation(self, dialog, next_button_presses, current_card, assert dialog._should_enable_next() is next_enabled +class TestButtons: + @pytest.fixture + def manipulator(self, setup): + dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id) + return JavascriptDialogButtonManipulator(dialog) + + def test_preview_button_can_be_clicked(self, manipulator, monkeypatch): + monkeypatch.setattr(manipulator.dialog, "show_preview", MagicMock()) + manipulator.wait_until_toolbar_buttons_are_ready() + manipulator.click_preview_button() + wait_until(lambda: manipulator.dialog.show_preview.call_count == 1) + + def test_addon_buttons_can_be_clicked(self, manipulator): + manipulator.wait_until_toolbar_buttons_are_ready() + manipulator.click_button(button_id="browse") + wait_until(lambda: get_dialog_instance("Browser") is not None) + + def test_addon_buttons_get_disabled_enabled(self, setup, manipulator): + Edit.open_dialog_and_show_note_with_id(setup.note2_id) + manipulator.wait_until_toolbar_buttons_are_ready() + assert manipulator.is_button_disabled("previous") is False + assert manipulator.is_button_disabled("next") is True + + class TestHistory: @pytest.fixture(autouse=True) def cleanup(self): From 700c6ae218c6aa7557fcaae526a70405b1790a54 Mon Sep 17 00:00:00 2001 From: oakkitten Date: Thu, 14 Apr 2022 00:42:13 +0100 Subject: [PATCH 4/6] Explicitly require Anki >= 2.1.45 Consolidate Anki version checks across files --- plugin/__init__.py | 28 +++++++++++----------------- plugin/edit.py | 4 ++-- tests/conftest.py | 4 ++-- tests/test_decks.py | 4 ---- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/plugin/__init__.py b/plugin/__init__.py index b947a06..ed4c008 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -13,6 +13,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import aqt + +anki_version = tuple(int(segment) for segment in aqt.appVersion.split(".")) + +if anki_version < (2, 1, 45): + raise Exception("Minimum Anki version supported: 2.1.45") + import base64 import glob import hashlib @@ -27,21 +34,15 @@ import anki import anki.exporting import anki.storage -import aqt from anki.cards import Card from anki.consts import MODEL_CLOZE from anki.exporting import AnkiPackageExporter from anki.importing import AnkiPackageImporter from anki.notes import Note +from anki.errors import NotFoundError from aqt.qt import Qt, QTimer, QMessageBox, QCheckBox from .edit import Edit - -try: - from anki.rsbackend import NotFoundError -except: - NotFoundError = Exception - from . import web, util @@ -50,8 +51,6 @@ # class AnkiConnect: - _anki21_version = int(aqt.appVersion.split('.')[-1]) - def __init__(self): self.log = None self.timer = None @@ -78,11 +77,7 @@ def startWebServer(self): ) def save_model(self, models, ankiModel): - if self._anki21_version < 45: - models.save(ankiModel, True) - models.flush() - else: - models.update_dict(ankiModel) + models.update_dict(ankiModel) def logEvent(self, name, data): if self.log is not None: @@ -541,9 +536,8 @@ def deleteDecks(self, decks, cardsToo=False): # however, since 62c23c6816adf912776b9378c008a52bb50b2e8d (2.1.45) # passing cardsToo to `rem` (long deprecated) won't raise an error! # this is dangerous, so let's raise our own exception - if self._anki21_version >= 28: - raise Exception("Since Anki 2.1.28 it's not possible " - "to delete decks without deleting cards as well") + raise Exception("Since Anki 2.1.28 it's not possible " + "to delete decks without deleting cards as well") try: self.startEditing() decks = filter(lambda d: d in self.deckNames(), decks) diff --git a/plugin/edit.py b/plugin/edit.py index 2ae9a89..a8c5ac9 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -8,6 +8,8 @@ from anki.consts import QUEUE_TYPE_SUSPENDED from anki.utils import ids2str +from . import anki_version + # Edit dialog. Like Edit Current, but: # * has a Preview button to preview the cards for the note @@ -25,8 +27,6 @@ DOMAIN_PREFIX = "foosoft.ankiconnect." -anki_version = tuple(int(segment) for segment in aqt.appVersion.split(".")) - def get_note_by_note_id(note_id): return aqt.mw.col.get_note(note_id) diff --git a/tests/conftest.py b/tests/conftest.py index cb26734..208abe2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from pytest_anki._launch import anki_running, temporary_user # noqa from waitress import wasyncore -from plugin import AnkiConnect +from plugin import AnkiConnect, anki_version from plugin.edit import Edit from plugin.util import DEFAULT_CONFIG @@ -85,7 +85,7 @@ def close(self): @contextmanager def anki_patched_to_prevent_backups(): with MonkeyPatch().context() as monkey: - if ac._anki21_version < 50: + if anki_version < (2, 1, 50): monkey.setitem(aqt.profiles.profileConf, "numBackups", 0) else: monkey.setattr(anki.collection.Collection, "create_backup", diff --git a/tests/test_decks.py b/tests/test_decks.py index 04e6107..c388b03 100755 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -30,10 +30,6 @@ def test_deleteDeck(setup): assert {*before} - {*after} == {"test_deck"} -@pytest.mark.skipif( - condition=ac._anki21_version < 28, - reason=f"Not applicable to Anki < 2.1.28" -) def test_deleteDeck_must_be_called_with_cardsToo_set_to_True_on_later_api(setup): with pytest.raises(Exception): ac.deleteDecks(decks=["test_deck"]) From ca90ef95fcd23bc5193fd688788b04d820bcdc41 Mon Sep 17 00:00:00 2001 From: oakkitten Date: Fri, 15 Apr 2022 21:49:46 +0100 Subject: [PATCH 5/6] Tests: set `HOME` via the command `env` Environmental variables set in `setenv` also affect pip invocations. `HOME` determines where pip is placing its cache. Setting HOME to anything virtual env-specific means that every virtual environment is created with its own cache. This wastes time, data and disk space. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index c8d3a41..a3def46 100644 --- a/tox.ini +++ b/tox.ini @@ -50,11 +50,11 @@ envlist = py38-anki{45,46,47,48,49},py39-anki{50qt5,50qt6} [testenv] commands = - xvfb-run python -m pytest {posargs} + env HOME={envtmpdir}/home xvfb-run python -m pytest {posargs} setenv = - HOME={envdir}/home anki50qt6: DISABLE_QT5_COMPAT=1 allowlist_externals = + env xvfb-run deps = pytest==7.1.1 From 8a84db971add35895cda0754a0659326f5d749b8 Mon Sep 17 00:00:00 2001 From: oakkitten Date: Wed, 20 Apr 2022 18:30:20 +0100 Subject: [PATCH 6/6] Patch Anki 2.1.50 on Windows to have valid stdout Something in Anki is setting stdout to Null. This is a problem because Anki itself is writing to it, particularly when printing warnings about the deprecated methods that we are using. This patches Anki using the same method that the upcoming Anki 2.1.51 is using. --- plugin/__init__.py | 4 ++++ plugin/util.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/plugin/__init__.py b/plugin/__init__.py index ed4c008..898f81b 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -27,6 +27,7 @@ import json import os import os.path +import platform import re import time import unicodedata @@ -1619,6 +1620,9 @@ def importPackage(self, path): # when run inside Anki, `__name__` would be either numeric, # or, if installed via `link.sh`, `AnkiConnectDev` if __name__ != "plugin": + if platform.system() == "Windows" and anki_version == (2, 1, 50): + util.patch_anki_2_1_50_having_null_stdout_on_windows() + Edit.register_with_anki() ac = AnkiConnect() diff --git a/plugin/util.py b/plugin/util.py index cc3c157..3ae5eb1 100644 --- a/plugin/util.py +++ b/plugin/util.py @@ -14,6 +14,7 @@ # along with this program. If not, see . import os +import sys import anki import anki.sync @@ -83,3 +84,10 @@ def setting(key): return aqt.mw.addonManager.getConfig(__name__).get(key, DEFAULT_CONFIG[key]) except: raise Exception('setting {} not found'.format(key)) + + +# see https://github.com/FooSoft/anki-connect/issues/308 +# fixed in https://github.com/ankitects/anki/commit/0b2a226d +def patch_anki_2_1_50_having_null_stdout_on_windows(): + if sys.stdout is None: + sys.stdout = open(os.devnull, "w", encoding="utf8")