A pytest plugin for testing Anki add-ons
Project description
pytest-anki
pytest-anki is a pytest plugin that allows developers to write tests for their Anki add-ons.
At its core lies the anki_session fixture that provides add-on authors with the ability to create and control headless Anki sessions to test their add-ons in:
from pytest_anki import AnkiSession
def test_addon_registers_deck(anki_session: AnkiSession):
my_addon = anki_session.load_addon("my_addon")
with anki_session.load_profile()
with anki_session.deck_installed(deck_path) as deck_id:
assert deck_id in my_addon.deck_ids
anki_session comes with a comprehensive API that allows developers to programmatically manipulate Anki, set up and reproduce specific configurations, simulate user interactions, and much more.
The goal is to provide add-on authors with a one-stop-shop for their functional testing needs, while also enabling them to QA their add-ons against a battery of different Anki versions, catching incompatibilities as they arise.
Quickstart
Install pytest-anki with your Qt backend and Anki version:
pip install pytest-anki2[qt6,anki-2509]
Add a minimal test file:
# test_my_addon.py
from pytest_anki import AnkiSession
def test_addon_loads(anki_session: AnkiSession):
anki_session.load_addon("my_addon")
assert hasattr(anki_session.mw, "my_addon")
def test_with_profile(anki_session: AnkiSession):
with anki_session.profile_loaded():
assert anki_session.collection is not None
Run with pytest:
pytest test_my_addon.py
If you use pyproject.toml, add the plugin config:
[tool.pytest.ini_options]
qt_api = "pyqt6"
See the Usage section below for the full API reference.
Platform Support
pytest-anki is tested on Linux (Ubuntu 24.04) and macOS in CI. Linux runs the full matrix of Qt5/Qt6 and Anki versions; macOS runs a single latest-config entry.
The full test suite requires a Qt6 WebEngine ABI compatible with Ubuntu 24.04 (as used in CI). On other Linux distributions, use system Qt packages (qt6-system extra) and rely on CI for test validation. On macOS, PyPI Qt6 wheels work natively.
Installation
Requirements
- Python 3.9+ (3.13 and 3.14 supported in CI)
- Anki 2.1.54+ (installed automatically)
- Qt5 or Qt6 (auto-detected at runtime; see below)
Choose your Qt backend
pytest-anki supports both PyQt5 and PyQt6, auto-detected at import time. Choose the approach that matches your system.
Option A: Ubuntu / Debian (PyPI wheels, recommended)
PyPI wheels for PyQt6, PyQt6-WebEngine, and their bundled Qt6 runtimes are built against Ubuntu's ABI and work out of the box:
pip install pytest-anki2[qt6-pypi]
With uv:
uv add --dev pytest-anki2[qt6-pypi]
Install optional selenium support for web debugging:
pip install pytest-anki2[qt6-pypi,selenium]
Option B: Arch Linux (system packages)
Use your distro's pre-compiled PyQt6 packages — they link against your system's Qt6 libraries and avoid ABI incompatibilities:
sudo pacman -S python-pyqt6-webengine
pip install pytest-anki2[qt6-system]
With uv:
sudo pacman -S python-pyqt6-webengine
uv add --dev pytest-anki2[qt6-system]
Option C: Fedora (system packages)
sudo dnf install python3-pyqt6-webengine
pip install pytest-anki2[qt6-system]
With uv:
sudo dnf install python3-pyqt6-webengine
uv add --dev pytest-anki2[qt6-system]
Option D: Any Linux with system Qt5 (fallback)
If your system provides Qt5 + PyQt5:
pip install pytest-anki2[qt5]
What's the difference between the Qt6 extras?
| Extra | Installs | Best for |
|---|---|---|
qt6 |
PyQt6 + PyQt6-WebEngine (bindings only) |
Default — use if unsure |
qt6-system |
Same as qt6 (alias) |
Systems with Qt6 + WebEngine installed via native packages |
qt6-pypi |
+ PyQt6-Qt6 + PyQt6-WebEngine-Qt6 (bundled runtimes) |
Ubuntu / Debian where PyPI wheels work natively |
Optional extras
| Extra | What it gives you |
|---|---|
selenium / web |
Web debugging via ChromeDriver |
recommended-plugins |
pytest-xvfb, pytest-xdist (with native forking) |
Qt binding selection
pytest-anki auto-detects your installed Qt bindings at import time (PyQt6 preferred over PyQt5). To override, set the QT_API environment variable:
QT_API value |
Binding selected |
|---|---|
pyqt6 or qt6 |
PyQt6 |
pyqt5 or qt5 |
PyQt5 |
QT_API=pyqt5 pytest tests/
This is useful when both PyQt5 and PyQt6 are installed, or when you want to test against a specific binding in CI.
Usage
Basic Use
The plugin registers a single anki_session fixture that launches a headless Anki instance:
from pytest_anki import AnkiSession
def test_my_addon(anki_session: AnkiSession):
# Anki is running — interact with anki_session.mw, .app, etc.
pass
The anki_session fixture yields an AnkiSession with these key attributes:
| Attribute | Type | Description |
|---|---|---|
mw |
AnkiQt |
Anki's main window |
app |
AnkiApp |
QApplication instance |
collection |
Collection |
Anki collection (after profile is loaded) |
user |
str |
Profile name (default: "User 1") |
base |
str |
Path to Anki's base directory |
qtbot |
QtBot |
pytest-qt fixture for Qt signal testing |
Profiles & collection:
def test_profile_loading(anki_session: AnkiSession):
with anki_session.profile_loaded():
assert anki_session.collection
# mw.col.conf, mw.pm.profile, etc. are available
Deck management:
def test_deck_install(anki_session: AnkiSession):
with anki_session.profile_loaded():
with anki_session.deck_installed("path/to/deck.apkg") as deck_id:
assert deck_id in [d.id for d in anki_session.collection.decks.all_names_and_ids()]
Loading add-ons:
def test_load_addon(anki_session: AnkiSession):
anki_session.load_addon("my_addon_package")
assert hasattr(anki_session.mw, "my_addon_package")
Add-on config:
def test_addon_config(anki_session: AnkiSession):
with anki_session.addon_config_created(
package_name="my_addon",
default_config={"key": "default"},
user_config={"key": "overridden"},
) as paths:
pass # config written to addons21/my_addon/config.json and meta.json
Pre-setting Anki state:
from pytest_anki import AnkiStateUpdate
def test_preset_state(anki_session: AnkiSession):
anki_session.update_anki_state(AnkiStateUpdate(
colconf_storage={"my_key": True},
profile_storage={"my_key": True},
))
Running tasks in the Qt event loop:
def test_threaded_task(anki_session: AnkiSession):
result = anki_session.run_in_thread_and_wait(
lambda: 42, timeout=5000
)
assert result == 42
Configuring the Anki Session
Customize the session via indirect parametrization:
import pytest
@pytest.mark.parametrize("anki_session", [dict(
load_profile=True,
profile_name="CustomUser",
lang="de_DE",
packed_addons=["path/to/addon.ankiaddon"],
unpacked_addons=[("my_addon", "path/to/addon/source")],
addon_configs=[("my_addon", {"key": "value"})],
preset_anki_state=AnkiStateUpdate(meta_storage={"key": True}),
enable_web_debugging=False,
skip_loading_addons=False,
)], indirect=True)
def test_configured_session(anki_session: AnkiSession):
assert anki_session.mw.pm.name == "CustomUser"
| Parameter | Type | Default | Description |
|---|---|---|---|
base_path |
str |
system tempdir | Directory for Anki base folder |
profile_name |
str |
"User 1" |
User profile name |
lang |
str |
"en_US" |
Profile language |
load_profile |
bool |
False |
Pre-load profile/collection |
preset_anki_state |
AnkiStateUpdate |
None |
Pre-configure col/prof/meta storage |
packed_addons |
List[Path] |
None |
.ankiaddon packages to install |
unpacked_addons |
List[Tuple[str, Path]] |
None |
Source folders to install as add-ons |
addon_configs |
List[Tuple[str, dict]] |
None |
Config key/value pairs for add-ons |
enable_web_debugging |
bool |
False |
Enable remote devtools |
skip_loading_addons |
bool |
False |
Install but don't auto-load add-ons |
Web debugging
When enable_web_debugging=True, you can drive Anki's web views via Selenium:
from pytest_anki import AnkiWebViewType
def test_web_view(anki_session: AnkiSession):
with anki_session.profile_loaded():
anki_session.run_with_chrome_driver(
lambda driver: driver.find_element("tag name", "body"),
target_web_view=AnkiWebViewType.main_webview,
)
Migration Guide (v1 → v2)
v2.0.0 is a major rewrite. Here's what changed:
Breaking changes
| v1 | v2 |
|---|---|
pytest-anki2[qt5] / pytest-anki2[qt6] |
pytest-anki2[qt5] / pytest-anki2[qt6] / pytest-anki2[qt6-system] / pytest-anki2[qt6-pypi] |
| Poetry-based build | uv + hatchling |
| Python 3.7+ | Python 3.9+ |
pytest-forked |
xdist native forking (pytest-xdist>=3.0) |
| Manual forking required | Auto-forked by default (opt-out via --anki-no-fork) |
| PyQt5 only | PyQt6 auto-detected, PyQt5 fallback |
| Selenium always installed | Selenium optional ([selenium] extra) |
What to update
- Switch to uv (or keep pip —
pip install pytest-anki2[...]still works). - Specify an Anki version extra:
pip install pytest-anki2[anki-2509,qt6]. - Remove
pytest-forkedfrom your dependencies — xdist handles forking. - If you used
@pytest.mark.forkedexplicitly, it's now automatic; use--anki-no-forkto disable. - If you relied on
pytest-ankipulling in selenium, addseleniumextra explicitly.
What stayed the same
- The
anki_sessionfixture API is backward-compatible. AnkiSession,AnkiStateUpdate,AnkiWebViewTypeare atpytest_anki.- Indirect parametrization via
@pytest.mark.parametrize("anki_session", [...], indirect=True)works as before.
Additional Notes
When to use pytest-anki
Running your test in an Anki environment is expensive and introduces an additional layer of confounding factors. If you can mock your Anki runtime dependencies away, then that should always be your first tool of choice.
Where anki_session comes in handy is further towards the upper levels of the test pyramid, i.e. functional tests, end-to-end tests, and UI tests. Additionally the plugin can provide you with a convenient way to automate testing for incompatibilities with Anki and other add-ons.
The importance of forking your tests
Since v2.0.0, all tests using this plugin are automatically marked as forked (via pytest_collection_modifyitems). This is because, while the plugin does attempt to tear down Anki sessions as cleanly as possible on exit, this process is never quite perfect, especially for add-ons that monkey-patch Anki.
With unforked test runs, factors like that can lead to unexpected behavior, or worse still, your tests crashing. Forking a new subprocess for each test bypasses these limitations.
To opt out of automatic forking, set anki_force_fork = false in your pyproject.toml:
[tool.pytest.ini_options]
anki_force_fork = false
Or pass --anki-no-fork on the command line:
pytest --anki-no-fork tests/
Disabling forking can speed up test suites that share a single Anki process, but may cause instability if add-ons mutate global state.
Automated Testing
pytest-anki is designed to work well with continuous integration systems such as GitHub actions. For an example see pytest-anki's own GitHub workflows.
Troubleshooting
Qt6 ABI incompatibility
PyPI wheels for PyQt6-WebEngine ship Qt6 libraries compiled against Ubuntu's ABI. On Arch, Fedora, or other non-Ubuntu distros:
# Use system Qt packages
sudo pacman -S python-pyqt6-webengine # Arch
sudo dnf install python3-pyqt6-webengine # Fedora
pip install pytest-anki2[qt6-system]
If you cannot run the full suite locally, CI (GitHub Actions) is the authoritative validation. You can always run lint and type checks:
make lint
make check
pytest hangs when using xvfb
Blocking non-dismissable prompts from your add-on code can cause hangs. Bypass xvfb temporarily:
pytest --no-xvfb tests/
ImportError: No Qt bindings found
Install a Qt backend:
pip install pytest-anki2[qt6]
# or for PyQt5:
pip install pytest-anki2[qt5]
If you have Qt installed but the wrong one is selected, set QT_API:
QT_API=pyqt5 pytest tests/
Tests fail with "Failed to import add-on"
Ensure your add-on is in Anki's addons21 directory, or use unpacked_addons to point to a source folder during session setup:
@pytest.mark.parametrize("anki_session", [dict(
unpacked_addons=[("my_addon", "/path/to/source")],
)], indirect=True)
def test_with_addon(anki_session: AnkiSession):
anki_session.load_addon("my_addon")
Contributing
Contributions are welcome! To set up pytest-anki for development, please first make sure you have Python 3.9+ and uv installed, then run the following steps:
$ git clone https://github.com/Alexander-Nilsson/pytest-anki.git
$ cd pytest-anki
$ make install
Before submitting any changes, please make sure that pytest-anki's checks and tests pass:
make lint
make check
make test # requires Ubuntu-compatible Qt6 ABI; CI runs the full matrix
This project uses ruff to enforce a consistent code style. To auto-format your code you can use:
make format
License and Credits
pytest-anki is
Copyright © 2019-2025 Aristotelis P. (Glutanimate) and contributors
Copyright © 2017-2019 Michal Krassowski
Copyright © 2017-2021 Ankitects Pty Ltd and contributors
All credits for the original idea for creating a context manager to test Anki add-ons with go to Michal. pytest-anki would not exist without his anki_testing project.
I would also like to extend a heartfelt thanks to AMBOSS for their major part in supporting the development of this plugin! Most of the recent feature additions leading up to v1.0.0 of the plugin were implemented as part of my work on the AMBOSS add-on.
pytest-anki is free and open-source software. Its source-code is released under the GNU AGPLv3 license, extended by a number of additional terms. For more information please see the license file that accompanies this program.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. Please see the license file for more details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_anki2-2.0.0.tar.gz.
File metadata
- Download URL: pytest_anki2-2.0.0.tar.gz
- Upload date:
- Size: 21.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d257ddc7f9a3044f7d792d8e2eb38d1d3c00eed679a864226f24f4b2a06c510
|
|
| MD5 |
c07508b670143e86dcfaefe786a6662d
|
|
| BLAKE2b-256 |
319bedca23db35bd1e54c95ec6b8bd22ca73cd5729b8ce0093975b9d366fc7f4
|
Provenance
The following attestation bundles were made for pytest_anki2-2.0.0.tar.gz:
Publisher:
release.yml on Alexander-Nilsson/pytest-anki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_anki2-2.0.0.tar.gz -
Subject digest:
6d257ddc7f9a3044f7d792d8e2eb38d1d3c00eed679a864226f24f4b2a06c510 - Sigstore transparency entry: 1754637290
- Sigstore integration time:
-
Permalink:
Alexander-Nilsson/pytest-anki@9b5d41fd110f4ea3075ed3c5ed61d2624d88cf29 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/Alexander-Nilsson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9b5d41fd110f4ea3075ed3c5ed61d2624d88cf29 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytest_anki2-2.0.0-py3-none-any.whl.
File metadata
- Download URL: pytest_anki2-2.0.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63b292b0911ba9d99f6ce9c831bfeee5120d13e4e66d4cfc5d682e99537ffff9
|
|
| MD5 |
8595554f3141f4b43d305ad78a7cc2d5
|
|
| BLAKE2b-256 |
a49e89108c0461b0d393b169abc0a1dc52951b95dfe53890eb2af7473830098f
|
Provenance
The following attestation bundles were made for pytest_anki2-2.0.0-py3-none-any.whl:
Publisher:
release.yml on Alexander-Nilsson/pytest-anki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_anki2-2.0.0-py3-none-any.whl -
Subject digest:
63b292b0911ba9d99f6ce9c831bfeee5120d13e4e66d4cfc5d682e99537ffff9 - Sigstore transparency entry: 1754637329
- Sigstore integration time:
-
Permalink:
Alexander-Nilsson/pytest-anki@9b5d41fd110f4ea3075ed3c5ed61d2624d88cf29 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/Alexander-Nilsson
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9b5d41fd110f4ea3075ed3c5ed61d2624d88cf29 -
Trigger Event:
push
-
Statement type: