From cd411927cc31a8080ab8c132a708594f2893d352 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 16:13:49 +0700 Subject: [PATCH 001/197] Split libankihelper into a separate module It's rarely updated, and the old approach resulted in a 'proper' aqt build only being done on a Mac. --- build/configure/src/aqt.rs | 22 ------------- qt/aqt/_macos_helper.py | 49 +++------------------------- qt/mac/anki_mac_helper/__init__.py | 51 ++++++++++++++++++++++++++++++ qt/mac/anki_mac_helper/py.typed | 0 qt/mac/build.sh | 20 ++++++++++++ qt/mac/helper_build.py | 24 +++++++++++--- qt/mac/pyproject.toml | 17 ++++++++++ qt/pyproject.toml | 1 + uv.lock | 12 ++++++- 9 files changed, 125 insertions(+), 71 deletions(-) create mode 100644 qt/mac/anki_mac_helper/__init__.py create mode 100644 qt/mac/anki_mac_helper/py.typed create mode 100755 qt/mac/build.sh create mode 100644 qt/mac/pyproject.toml diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 60316fcd2..6be5adb07 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -27,7 +27,6 @@ pub fn build_and_check_aqt(build: &mut Build) -> Result<()> { build_forms(build)?; build_generated_sources(build)?; build_data_folder(build)?; - build_macos_helper(build)?; build_wheel(build)?; check_python(build)?; Ok(()) @@ -337,27 +336,6 @@ impl BuildAction for BuildThemedIcon<'_> { } } -fn build_macos_helper(build: &mut Build) -> Result<()> { - if cfg!(target_os = "macos") { - build.add_action( - "qt:aqt:data:lib:libankihelper", - RunCommand { - command: ":pyenv:bin", - args: "$script $out $in", - inputs: hashmap! { - "script" => inputs!["qt/mac/helper_build.py"], - "in" => inputs![glob!["qt/mac/*.swift"]], - "" => inputs!["out/env"], - }, - outputs: hashmap! { - "out" => vec!["qt/_aqt/data/lib/libankihelper.dylib"], - }, - }, - )?; - } - Ok(()) -} - fn build_wheel(build: &mut Build) -> Result<()> { build.add_action( "wheels:aqt", diff --git a/qt/aqt/_macos_helper.py b/qt/aqt/_macos_helper.py index 9328b1c4a..634d94756 100644 --- a/qt/aqt/_macos_helper.py +++ b/qt/aqt/_macos_helper.py @@ -3,50 +3,11 @@ from __future__ import annotations -import os import sys -from collections.abc import Callable -from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p -import aqt -import aqt.utils - - -class _MacOSHelper: - def __init__(self) -> None: - path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib") - - self._dll = CDLL(path) - self._dll.system_is_dark.restype = c_bool - - def system_is_dark(self) -> bool: - return self._dll.system_is_dark() - - def set_darkmode_enabled(self, enabled: bool) -> bool: - return self._dll.set_darkmode_enabled(enabled) - - def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: - global _on_audio_error - _on_audio_error = on_error - self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) - - def end_wav_record(self) -> None: - "On completion, file should be saved if no error has arrived." - self._dll.end_wav_record() - - -# this must not be overwritten or deallocated -@CFUNCTYPE(None, c_char_p) # type: ignore -def _audio_error_callback(msg: str) -> None: - if handler := _on_audio_error: - handler(msg) - - -_on_audio_error: Callable[[str], None] | None = None - -macos_helper: _MacOSHelper | None = None if sys.platform == "darwin": - try: - macos_helper = _MacOSHelper() - except Exception as e: - print("macos_helper:", e) + from anki_mac_helper import ( # pylint:disable=unused-import,import-error + macos_helper, + ) +else: + macos_helper = None diff --git a/qt/mac/anki_mac_helper/__init__.py b/qt/mac/anki_mac_helper/__init__.py new file mode 100644 index 000000000..a0adb469e --- /dev/null +++ b/qt/mac/anki_mac_helper/__init__.py @@ -0,0 +1,51 @@ +# 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 sys +from collections.abc import Callable +from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p +from pathlib import Path + + +class _MacOSHelper: + def __init__(self) -> None: + # Look for the dylib in the same directory as this module + module_dir = Path(__file__).parent + path = module_dir / "libankihelper.dylib" + + self._dll = CDLL(str(path)) + self._dll.system_is_dark.restype = c_bool + + def system_is_dark(self) -> bool: + return self._dll.system_is_dark() + + def set_darkmode_enabled(self, enabled: bool) -> bool: + return self._dll.set_darkmode_enabled(enabled) + + def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: + global _on_audio_error + _on_audio_error = on_error + self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) + + def end_wav_record(self) -> None: + "On completion, file should be saved if no error has arrived." + self._dll.end_wav_record() + + +# this must not be overwritten or deallocated +@CFUNCTYPE(None, c_char_p) # type: ignore +def _audio_error_callback(msg: str) -> None: + if handler := _on_audio_error: + handler(msg) + + +_on_audio_error: Callable[[str], None] | None = None + +macos_helper: _MacOSHelper | None = None +if sys.platform == "darwin": + try: + macos_helper = _MacOSHelper() + except Exception as e: + print("macos_helper:", e) diff --git a/qt/mac/anki_mac_helper/py.typed b/qt/mac/anki_mac_helper/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/qt/mac/build.sh b/qt/mac/build.sh new file mode 100755 index 000000000..4c14a13f4 --- /dev/null +++ b/qt/mac/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +set -e + +# Get the project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJ_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Build the dylib first +echo "Building macOS helper dylib..." +"$PROJ_ROOT/out/pyenv/bin/python" "$SCRIPT_DIR/helper_build.py" + +# Create the wheel using uv +echo "Creating wheel..." +cd "$SCRIPT_DIR" +"$PROJ_ROOT/out/extracted/uv/uv" build --wheel + +echo "Build complete!" diff --git a/qt/mac/helper_build.py b/qt/mac/helper_build.py index 0638c9249..aaf997669 100644 --- a/qt/mac/helper_build.py +++ b/qt/mac/helper_build.py @@ -7,8 +7,16 @@ import subprocess import sys from pathlib import Path -out_dylib, *src_files = sys.argv[1:] -out_dir = Path(out_dylib).parent.resolve() +# If no arguments provided, build for the anki_mac_helper package +if len(sys.argv) == 1: + script_dir = Path(__file__).parent + out_dylib = script_dir / "anki_mac_helper" / "libankihelper.dylib" + src_files = list(script_dir.glob("*.swift")) +else: + out_dylib, *src_files = sys.argv[1:] + +out_dylib = Path(out_dylib) +out_dir = out_dylib.parent.resolve() src_dir = Path(src_files[0]).parent.resolve() # Build for both architectures @@ -29,12 +37,20 @@ for arch in architectures: "ankihelper", "-O", ] - args.extend(src_dir / Path(file).name for file in src_files) + if isinstance(src_files[0], Path): + args.extend(src_files) + else: + args.extend(src_dir / Path(file).name for file in src_files) args.extend(["-o", str(temp_out)]) subprocess.run(args, check=True, cwd=out_dir) +# Ensure output directory exists +out_dir.mkdir(parents=True, exist_ok=True) + # Create universal binary -lipo_args = ["lipo", "-create", "-output", out_dylib] + [str(f) for f in temp_files] +lipo_args = ["lipo", "-create", "-output", str(out_dylib)] + [ + str(f) for f in temp_files +] subprocess.run(lipo_args, check=True) # Clean up temporary files diff --git a/qt/mac/pyproject.toml b/qt/mac/pyproject.toml new file mode 100644 index 000000000..93f4e939b --- /dev/null +++ b/qt/mac/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "anki-mac-helper" +version = "0.1.0" +description = "Small support library for Anki on Macs" +requires-python = ">=3.9" +license = { text = "AGPL-3.0-or-later" } +authors = [ + { name = "Anki Team" }, +] +urls = { Homepage = "https://github.com/ankitects/anki" } + +[tool.hatch.build.targets.wheel] +packages = ["anki_mac_helper"] diff --git a/qt/pyproject.toml b/qt/pyproject.toml index ab16afc3d..d8b4c4306 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "waitress>=2.0.0", "psutil; sys.platform == 'win32'", "pywin32; sys.platform == 'win32'", + "anki-mac-helper; sys.platform == 'darwin'", "pip-system-certs!=5.1", "mock", "types-decorator", diff --git a/uv.lock b/uv.lock index d735e0772..51b16f8ab 100644 --- a/uv.lock +++ b/uv.lock @@ -141,11 +141,20 @@ dev = [ { name = "wheel" }, ] +[[package]] +name = "anki-mac-helper" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/9f/c4d3e635ddbd2c6c24ff5454e96900fd2061b9abbb0198b9283446780d08/anki_mac_helper-0.1.0-py3-none-any.whl", hash = "sha256:ed449aba27ea3bc7999054afa10dacf08ef856ed7af46526d9c8599d8179a618", size = 40637, upload-time = "2025-06-19T14:38:07.672Z" }, +] + [[package]] name = "aqt" version = "0.1.2" source = { editable = "qt" } dependencies = [ + { name = "anki-mac-helper", marker = "sys_platform == 'darwin' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, { name = "beautifulsoup4" }, { name = "flask" }, { name = "flask-cors" }, @@ -201,6 +210,7 @@ qt67 = [ [package.metadata] requires-dist = [ { name = "anki-audio", marker = "(sys_platform == 'darwin' and extra == 'audio') or (sys_platform == 'win32' and extra == 'audio')", specifier = "==0.1.0" }, + { name = "anki-mac-helper", marker = "sys_platform == 'darwin'" }, { name = "beautifulsoup4" }, { name = "flask" }, { name = "flask-cors" }, @@ -572,7 +582,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ From 8e20973c52e185d6e87e0ee4792c024fcc419b8f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 16:11:17 +0700 Subject: [PATCH 002/197] Drop remaining qt5 code --- build/configure/src/aqt.rs | 1 - docs/linux.md | 15 +- qt/aqt/__init__.py | 23 +- qt/aqt/browser/table/model.py | 4 +- qt/aqt/browser/table/table.py | 11 +- qt/aqt/forms/about.py | 9 +- qt/aqt/forms/addcards.py | 9 +- qt/aqt/forms/addfield.py | 9 +- qt/aqt/forms/addmodel.py | 9 +- qt/aqt/forms/addonconf.py | 9 +- qt/aqt/forms/addons.py | 9 +- qt/aqt/forms/browser.py | 9 +- qt/aqt/forms/browserdisp.py | 9 +- qt/aqt/forms/browseropts.py | 9 +- qt/aqt/forms/changemap.py | 9 +- qt/aqt/forms/changemodel.py | 9 +- qt/aqt/forms/clayout_top.py | 9 +- qt/aqt/forms/customstudy.py | 9 +- qt/aqt/forms/dconf.py | 9 +- qt/aqt/forms/debug.py | 9 +- qt/aqt/forms/editcurrent.py | 9 +- qt/aqt/forms/edithtml.py | 9 +- qt/aqt/forms/emptycards.py | 9 +- qt/aqt/forms/exporting.py | 9 +- qt/aqt/forms/fields.py | 9 +- qt/aqt/forms/filtered_deck.py | 9 +- qt/aqt/forms/finddupes.py | 9 +- qt/aqt/forms/findreplace.py | 9 +- qt/aqt/forms/forget.py | 9 +- qt/aqt/forms/getaddons.py | 9 +- qt/aqt/forms/importing.py | 9 +- qt/aqt/forms/main.py | 9 +- qt/aqt/forms/modelopts.py | 9 +- qt/aqt/forms/models.py | 9 +- qt/aqt/forms/preferences.py | 9 +- qt/aqt/forms/preview.py | 9 +- qt/aqt/forms/profiles.py | 9 +- qt/aqt/forms/progress.py | 9 +- qt/aqt/forms/reposition.py | 9 +- qt/aqt/forms/setgroup.py | 9 +- qt/aqt/forms/setlang.py | 9 +- qt/aqt/forms/stats.py | 9 +- qt/aqt/forms/studydeck.py | 9 +- qt/aqt/forms/synclog.py | 9 +- qt/aqt/forms/taglimit.py | 9 +- qt/aqt/forms/template.py | 9 +- qt/aqt/forms/widgets.py | 9 +- qt/aqt/profiles.py | 7 +- qt/aqt/progress.py | 3 +- qt/aqt/qt/__init__.py | 25 +-- qt/aqt/qt/qt5.py | 22 -- qt/aqt/qt/qt5_audio.py | 99 -------- qt/aqt/qt/qt5_compat.py | 411 ---------------------------------- qt/aqt/sound.py | 19 +- qt/tools/build_ui.py | 26 +-- 55 files changed, 71 insertions(+), 973 deletions(-) delete mode 100644 qt/aqt/qt/qt5.py delete mode 100644 qt/aqt/qt/qt5_audio.py delete mode 100644 qt/aqt/qt/qt5_compat.py diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 6be5adb07..0a9b32270 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -38,7 +38,6 @@ fn build_forms(build: &mut Build) -> Result<()> { let mut py_files = vec![]; for path in ui_files.resolve() { let outpath = outdir.join(path.file_name().unwrap()).into_string(); - py_files.push(outpath.replace(".ui", "_qt5.py")); py_files.push(outpath.replace(".ui", "_qt6.py")); } build.add_action( diff --git a/docs/linux.md b/docs/linux.md index 27e3ceeda..55794e074 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -51,13 +51,8 @@ Anki requires a recent glibc. If you are using a distro that uses musl, Anki will not work. -If your glibc version is 2.35+ on AMD64 or 2.39+ on ARM64, you can skip the rest of this section. - -If your system has an older glibc, you won't be able to use the PyQt wheels that are -available in pip/PyPy, and will need to use your system-installed PyQt instead. -Your distro will also need to have Python 3.9 or later. - -After installing the system libraries (eg: +You can use your system's Qt libraries if they are Qt 6.2 or later, if +you wish. After installing the system libraries (eg: 'sudo apt install python3-pyqt6.qt{quick,webengine} python3-venv pyqt6-dev-tools'), find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where @@ -68,12 +63,6 @@ export PYTHONPATH=/usr/lib/python3/dist-packages export PYTHON_BINARY=/usr/bin/python3 ``` -There are a few things to be aware of: - -- You should use ./run and not tools/run-qt5\*, even if your system libraries are Qt5. -- If your system libraries are Qt5, when creating an aqt wheel, the wheel will not work - on Qt6 environments. - ## Packaging considerations Python, node and protoc are downloaded as part of the build. You can optionally define diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 6645e3599..740dcbc9f 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -284,7 +284,7 @@ def setupLangAndBackend( class NativeEventFilter(QAbstractNativeEventFilter): def nativeEventFilter( self, eventType: Any, message: Any - ) -> tuple[bool, sip.voidptr | None]: + ) -> tuple[bool, Any | None]: if eventType == "windows_generic_MSG": import ctypes.wintypes @@ -376,6 +376,8 @@ class AnkiApp(QApplication): def onRecv(self) -> None: sock = self._srv.nextPendingConnection() + if sock is None: + return if not sock.waitForReadyRead(self.TMOUT): sys.stderr.write(sock.errorString()) return @@ -406,14 +408,12 @@ class AnkiApp(QApplication): QRadioButton, QMenu, QSlider, - # classes with PyQt5 compatibility proxy - without_qt5_compat_wrapper(QToolButton), - without_qt5_compat_wrapper(QTabBar), + QToolButton, + QTabBar, ) if evt.type() in [QEvent.Type.Enter, QEvent.Type.HoverEnter]: if (isinstance(src, pointer_classes) and src.isEnabled()) or ( - isinstance(src, without_qt5_compat_wrapper(QComboBox)) - and not src.isEditable() + isinstance(src, QComboBox) and not src.isEditable() ): self.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor)) else: @@ -525,15 +525,12 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None: QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL) elif driver in (VideoDriver.Software, VideoDriver.ANGLE): if is_win: - # on Windows, this appears to be sufficient on Qt5/Qt6. + # on Windows, this appears to be sufficient # On Qt6, ANGLE is excluded by the enum. os.environ["QT_OPENGL"] = driver.value elif is_mac: QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) elif is_lin: - # Qt5 only - os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1" - # Required on Qt6 if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ: os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" if qtmajor > 5: @@ -663,12 +660,6 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None: if is_win and "QT_QPA_PLATFORM" not in os.environ: os.environ["QT_QPA_PLATFORM"] = "windows:altgr" - # Disable sandbox on Qt5 PyPi/packaged builds, as it causes blank screens on modern - # glibc versions. We check for specific patch versions, because distros may have - # fixed the issue in their own Qt builds. - if is_lin and qtfullversion in ([5, 15, 2], [5, 14, 1]): - os.environ["QTWEBENGINE_DISABLE_SANDBOX"] = "1" - # create the app QCoreApplication.setApplicationName("Anki") QGuiApplication.setDesktopFileName("anki") diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 5b42c0ca3..e8d3bb7b6 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -325,15 +325,13 @@ class DataModel(QAbstractTableModel): return 0 return self.len_columns() - _QFont = without_qt5_compat_wrapper(QFont) - def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: if not index.isValid(): return QVariant() if role == Qt.ItemDataRole.FontRole: if not self.column_at(index).uses_cell_font: return QVariant() - qfont = self._QFont() + qfont = QFont() row = self.get_row(index) qfont.setFamily(row.font_name) qfont.setPixelSize(row.font_size) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index f3d543d93..fb921822b 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -382,10 +382,7 @@ class Table: hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._restore_header() qconnect(hh.customContextMenuRequested, self._on_header_context) - if qtmajor == 5: - qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed_qt5) - else: - qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) + qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) qconnect(hh.sectionMoved, self._on_column_moved) # Slots @@ -495,12 +492,6 @@ class Table: if checked: self._scroll_to_column(self._model.len_columns() - 1) - def _on_sort_column_changed_qt5(self, section: int, order: int) -> None: - self._on_sort_column_changed( - section, - Qt.SortOrder.AscendingOrder if not order else Qt.SortOrder.DescendingOrder, - ) - def _on_sort_column_changed(self, section: int, order: Qt.SortOrder) -> None: column = self._model.column_at_section(section) sorting = column.sorting_notes if self.is_notes_mode() else column.sorting_cards diff --git a/qt/aqt/forms/about.py b/qt/aqt/forms/about.py index 4faf97fb0..fe66f7da3 100644 --- a/qt/aqt/forms/about.py +++ b/qt/aqt/forms/about.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.about_qt6 import * -else: - from _aqt.forms.about_qt5 import * # type: ignore +from _aqt.forms.about_qt6 import * diff --git a/qt/aqt/forms/addcards.py b/qt/aqt/forms/addcards.py index ae2debe3e..8c501695e 100644 --- a/qt/aqt/forms/addcards.py +++ b/qt/aqt/forms/addcards.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addcards_qt6 import * -else: - from _aqt.forms.addcards_qt5 import * # type: ignore +from _aqt.forms.addcards_qt6 import * diff --git a/qt/aqt/forms/addfield.py b/qt/aqt/forms/addfield.py index 57c697b4a..a2f9eed74 100644 --- a/qt/aqt/forms/addfield.py +++ b/qt/aqt/forms/addfield.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addfield_qt6 import * -else: - from _aqt.forms.addfield_qt5 import * # type: ignore +from _aqt.forms.addfield_qt6 import * diff --git a/qt/aqt/forms/addmodel.py b/qt/aqt/forms/addmodel.py index 9a7d06b7e..0af313a45 100644 --- a/qt/aqt/forms/addmodel.py +++ b/qt/aqt/forms/addmodel.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addmodel_qt6 import * -else: - from _aqt.forms.addmodel_qt5 import * # type: ignore +from _aqt.forms.addmodel_qt6 import * diff --git a/qt/aqt/forms/addonconf.py b/qt/aqt/forms/addonconf.py index cca92b7b9..d78ebb82a 100644 --- a/qt/aqt/forms/addonconf.py +++ b/qt/aqt/forms/addonconf.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addonconf_qt6 import * -else: - from _aqt.forms.addonconf_qt5 import * # type: ignore +from _aqt.forms.addonconf_qt6 import * diff --git a/qt/aqt/forms/addons.py b/qt/aqt/forms/addons.py index fa00be08b..46d7532b4 100644 --- a/qt/aqt/forms/addons.py +++ b/qt/aqt/forms/addons.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addons_qt6 import * -else: - from _aqt.forms.addons_qt5 import * # type: ignore +from _aqt.forms.addons_qt6 import * diff --git a/qt/aqt/forms/browser.py b/qt/aqt/forms/browser.py index 403f780c5..70214ba4c 100644 --- a/qt/aqt/forms/browser.py +++ b/qt/aqt/forms/browser.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browser_qt6 import * -else: - from _aqt.forms.browser_qt5 import * # type: ignore +from _aqt.forms.browser_qt6 import * diff --git a/qt/aqt/forms/browserdisp.py b/qt/aqt/forms/browserdisp.py index 712e5a400..fc745a703 100644 --- a/qt/aqt/forms/browserdisp.py +++ b/qt/aqt/forms/browserdisp.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browserdisp_qt6 import * -else: - from _aqt.forms.browserdisp_qt5 import * # type: ignore +from _aqt.forms.browserdisp_qt6 import * diff --git a/qt/aqt/forms/browseropts.py b/qt/aqt/forms/browseropts.py index 68602c85c..1ae696033 100644 --- a/qt/aqt/forms/browseropts.py +++ b/qt/aqt/forms/browseropts.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browseropts_qt6 import * -else: - from _aqt.forms.browseropts_qt5 import * # type: ignore +from _aqt.forms.browseropts_qt6 import * diff --git a/qt/aqt/forms/changemap.py b/qt/aqt/forms/changemap.py index 6028b0d49..b48b49a83 100644 --- a/qt/aqt/forms/changemap.py +++ b/qt/aqt/forms/changemap.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.changemap_qt6 import * -else: - from _aqt.forms.changemap_qt5 import * # type: ignore +from _aqt.forms.changemap_qt6 import * diff --git a/qt/aqt/forms/changemodel.py b/qt/aqt/forms/changemodel.py index 73f7f6095..cd1931af8 100644 --- a/qt/aqt/forms/changemodel.py +++ b/qt/aqt/forms/changemodel.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.changemodel_qt6 import * -else: - from _aqt.forms.changemodel_qt5 import * # type: ignore +from _aqt.forms.changemodel_qt6 import * diff --git a/qt/aqt/forms/clayout_top.py b/qt/aqt/forms/clayout_top.py index 24f78be11..1a76c882a 100644 --- a/qt/aqt/forms/clayout_top.py +++ b/qt/aqt/forms/clayout_top.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.clayout_top_qt6 import * -else: - from _aqt.forms.clayout_top_qt5 import * # type: ignore +from _aqt.forms.clayout_top_qt6 import * diff --git a/qt/aqt/forms/customstudy.py b/qt/aqt/forms/customstudy.py index 393638b2c..3bfad32ac 100644 --- a/qt/aqt/forms/customstudy.py +++ b/qt/aqt/forms/customstudy.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.customstudy_qt6 import * -else: - from _aqt.forms.customstudy_qt5 import * # type: ignore +from _aqt.forms.customstudy_qt6 import * diff --git a/qt/aqt/forms/dconf.py b/qt/aqt/forms/dconf.py index e28db5c31..f39de7077 100644 --- a/qt/aqt/forms/dconf.py +++ b/qt/aqt/forms/dconf.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.dconf_qt6 import * -else: - from _aqt.forms.dconf_qt5 import * # type: ignore +from _aqt.forms.dconf_qt6 import * diff --git a/qt/aqt/forms/debug.py b/qt/aqt/forms/debug.py index 928ba7795..0880c49fc 100644 --- a/qt/aqt/forms/debug.py +++ b/qt/aqt/forms/debug.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.debug_qt6 import * -else: - from _aqt.forms.debug_qt5 import * # type: ignore +from _aqt.forms.debug_qt6 import * diff --git a/qt/aqt/forms/editcurrent.py b/qt/aqt/forms/editcurrent.py index 1281faafe..cfa9ab1d9 100644 --- a/qt/aqt/forms/editcurrent.py +++ b/qt/aqt/forms/editcurrent.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.editcurrent_qt6 import * -else: - from _aqt.forms.editcurrent_qt5 import * # type: ignore +from _aqt.forms.editcurrent_qt6 import * diff --git a/qt/aqt/forms/edithtml.py b/qt/aqt/forms/edithtml.py index 029977705..61b9e0fd2 100644 --- a/qt/aqt/forms/edithtml.py +++ b/qt/aqt/forms/edithtml.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.edithtml_qt6 import * -else: - from _aqt.forms.edithtml_qt5 import * # type: ignore +from _aqt.forms.edithtml_qt6 import * diff --git a/qt/aqt/forms/emptycards.py b/qt/aqt/forms/emptycards.py index 046c7eb3a..1cae290fd 100644 --- a/qt/aqt/forms/emptycards.py +++ b/qt/aqt/forms/emptycards.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.emptycards_qt6 import * -else: - from _aqt.forms.emptycards_qt5 import * # type: ignore +from _aqt.forms.emptycards_qt6 import * diff --git a/qt/aqt/forms/exporting.py b/qt/aqt/forms/exporting.py index 559e50ecd..d09e9cdd9 100644 --- a/qt/aqt/forms/exporting.py +++ b/qt/aqt/forms/exporting.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.exporting_qt6 import * -else: - from _aqt.forms.exporting_qt5 import * # type: ignore +from _aqt.forms.exporting_qt6 import * diff --git a/qt/aqt/forms/fields.py b/qt/aqt/forms/fields.py index fa379be67..cf7a39f75 100644 --- a/qt/aqt/forms/fields.py +++ b/qt/aqt/forms/fields.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.fields_qt6 import * -else: - from _aqt.forms.fields_qt5 import * # type: ignore +from _aqt.forms.fields_qt6 import * diff --git a/qt/aqt/forms/filtered_deck.py b/qt/aqt/forms/filtered_deck.py index 9b9589046..59870f5a0 100644 --- a/qt/aqt/forms/filtered_deck.py +++ b/qt/aqt/forms/filtered_deck.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.filtered_deck_qt6 import * -else: - from _aqt.forms.filtered_deck_qt5 import * # type: ignore +from _aqt.forms.filtered_deck_qt6 import * diff --git a/qt/aqt/forms/finddupes.py b/qt/aqt/forms/finddupes.py index 7bca9c4cd..43ac30549 100644 --- a/qt/aqt/forms/finddupes.py +++ b/qt/aqt/forms/finddupes.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.finddupes_qt6 import * -else: - from _aqt.forms.finddupes_qt5 import * # type: ignore +from _aqt.forms.finddupes_qt6 import * diff --git a/qt/aqt/forms/findreplace.py b/qt/aqt/forms/findreplace.py index 8f82e58fe..65d1f3555 100644 --- a/qt/aqt/forms/findreplace.py +++ b/qt/aqt/forms/findreplace.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.findreplace_qt6 import * -else: - from _aqt.forms.findreplace_qt5 import * # type: ignore +from _aqt.forms.findreplace_qt6 import * diff --git a/qt/aqt/forms/forget.py b/qt/aqt/forms/forget.py index 97425aed8..0d17803df 100644 --- a/qt/aqt/forms/forget.py +++ b/qt/aqt/forms/forget.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.forget_qt6 import * -else: - from _aqt.forms.forget_qt5 import * # type: ignore +from _aqt.forms.forget_qt6 import * diff --git a/qt/aqt/forms/getaddons.py b/qt/aqt/forms/getaddons.py index c47ed27a8..ecb6c23dd 100644 --- a/qt/aqt/forms/getaddons.py +++ b/qt/aqt/forms/getaddons.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.getaddons_qt6 import * -else: - from _aqt.forms.getaddons_qt5 import * # type: ignore +from _aqt.forms.getaddons_qt6 import * diff --git a/qt/aqt/forms/importing.py b/qt/aqt/forms/importing.py index f60b74a4e..39ade97c2 100644 --- a/qt/aqt/forms/importing.py +++ b/qt/aqt/forms/importing.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.importing_qt6 import * -else: - from _aqt.forms.importing_qt5 import * # type: ignore +from _aqt.forms.importing_qt6 import * diff --git a/qt/aqt/forms/main.py b/qt/aqt/forms/main.py index 068804a2d..7ec7107b3 100644 --- a/qt/aqt/forms/main.py +++ b/qt/aqt/forms/main.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.main_qt6 import * -else: - from _aqt.forms.main_qt5 import * # type: ignore +from _aqt.forms.main_qt6 import * diff --git a/qt/aqt/forms/modelopts.py b/qt/aqt/forms/modelopts.py index 0e4770c92..811b1fb7b 100644 --- a/qt/aqt/forms/modelopts.py +++ b/qt/aqt/forms/modelopts.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.modelopts_qt6 import * -else: - from _aqt.forms.modelopts_qt5 import * # type: ignore +from _aqt.forms.modelopts_qt6 import * diff --git a/qt/aqt/forms/models.py b/qt/aqt/forms/models.py index fb0b64e0a..43c75c62a 100644 --- a/qt/aqt/forms/models.py +++ b/qt/aqt/forms/models.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.models_qt6 import * -else: - from _aqt.forms.models_qt5 import * # type: ignore +from _aqt.forms.models_qt6 import * diff --git a/qt/aqt/forms/preferences.py b/qt/aqt/forms/preferences.py index de9fdc989..6fdb0bfd3 100644 --- a/qt/aqt/forms/preferences.py +++ b/qt/aqt/forms/preferences.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.preferences_qt6 import * -else: - from _aqt.forms.preferences_qt5 import * # type: ignore +from _aqt.forms.preferences_qt6 import * diff --git a/qt/aqt/forms/preview.py b/qt/aqt/forms/preview.py index ca938a396..bf735bd39 100644 --- a/qt/aqt/forms/preview.py +++ b/qt/aqt/forms/preview.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.preview_qt6 import * -else: - from _aqt.forms.preview_qt5 import * # type: ignore +from _aqt.forms.preview_qt6 import * diff --git a/qt/aqt/forms/profiles.py b/qt/aqt/forms/profiles.py index c7bcc10e1..7d5b8d6e0 100644 --- a/qt/aqt/forms/profiles.py +++ b/qt/aqt/forms/profiles.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.profiles_qt6 import * -else: - from _aqt.forms.profiles_qt5 import * # type: ignore +from _aqt.forms.profiles_qt6 import * diff --git a/qt/aqt/forms/progress.py b/qt/aqt/forms/progress.py index 47a57ce49..7a2a332d5 100644 --- a/qt/aqt/forms/progress.py +++ b/qt/aqt/forms/progress.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.progress_qt6 import * -else: - from _aqt.forms.progress_qt5 import * # type: ignore +from _aqt.forms.progress_qt6 import * diff --git a/qt/aqt/forms/reposition.py b/qt/aqt/forms/reposition.py index 646abf7c4..cfad6b55a 100644 --- a/qt/aqt/forms/reposition.py +++ b/qt/aqt/forms/reposition.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.reposition_qt6 import * -else: - from _aqt.forms.reposition_qt5 import * # type: ignore +from _aqt.forms.reposition_qt6 import * diff --git a/qt/aqt/forms/setgroup.py b/qt/aqt/forms/setgroup.py index 649e4f75a..617ef3b96 100644 --- a/qt/aqt/forms/setgroup.py +++ b/qt/aqt/forms/setgroup.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.setgroup_qt6 import * -else: - from _aqt.forms.setgroup_qt5 import * # type: ignore +from _aqt.forms.setgroup_qt6 import * diff --git a/qt/aqt/forms/setlang.py b/qt/aqt/forms/setlang.py index bb715ff92..efe14343b 100644 --- a/qt/aqt/forms/setlang.py +++ b/qt/aqt/forms/setlang.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.setlang_qt6 import * -else: - from _aqt.forms.setlang_qt5 import * # type: ignore +from _aqt.forms.setlang_qt6 import * diff --git a/qt/aqt/forms/stats.py b/qt/aqt/forms/stats.py index 212c03345..12b161f4e 100644 --- a/qt/aqt/forms/stats.py +++ b/qt/aqt/forms/stats.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.stats_qt6 import * -else: - from _aqt.forms.stats_qt5 import * # type: ignore +from _aqt.forms.stats_qt6 import * diff --git a/qt/aqt/forms/studydeck.py b/qt/aqt/forms/studydeck.py index b95bc7e87..497ab01cf 100644 --- a/qt/aqt/forms/studydeck.py +++ b/qt/aqt/forms/studydeck.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.studydeck_qt6 import * -else: - from _aqt.forms.studydeck_qt5 import * # type: ignore +from _aqt.forms.studydeck_qt6 import * diff --git a/qt/aqt/forms/synclog.py b/qt/aqt/forms/synclog.py index 97fefe300..ddd08456b 100644 --- a/qt/aqt/forms/synclog.py +++ b/qt/aqt/forms/synclog.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.synclog_qt6 import * -else: - from _aqt.forms.synclog_qt5 import * # type: ignore +from _aqt.forms.synclog_qt6 import * diff --git a/qt/aqt/forms/taglimit.py b/qt/aqt/forms/taglimit.py index 7a4763016..88262c657 100644 --- a/qt/aqt/forms/taglimit.py +++ b/qt/aqt/forms/taglimit.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.taglimit_qt6 import * -else: - from _aqt.forms.taglimit_qt5 import * # type: ignore +from _aqt.forms.taglimit_qt6 import * diff --git a/qt/aqt/forms/template.py b/qt/aqt/forms/template.py index 84f3d2a05..7540d72e0 100644 --- a/qt/aqt/forms/template.py +++ b/qt/aqt/forms/template.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.template_qt6 import * -else: - from _aqt.forms.template_qt5 import * # type: ignore +from _aqt.forms.template_qt6 import * diff --git a/qt/aqt/forms/widgets.py b/qt/aqt/forms/widgets.py index b91f7ae26..07dc11c6c 100644 --- a/qt/aqt/forms/widgets.py +++ b/qt/aqt/forms/widgets.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.widgets_qt6 import * -else: - from _aqt.forms.widgets_qt5 import * # type: ignore +from _aqt.forms.widgets_qt6 import * diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 273e6df3a..6597f6705 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -189,11 +189,8 @@ class ProfileManager: # return the bytes directly return args[0] elif name == "_unpickle_enum": - if qtmajor == 5: - return sip._unpickle_enum(module, klass, args) # type: ignore - else: - # old style enums can't be unpickled - return None + # old style enums can't be unpickled + return None else: return sip._unpickle_type(module, klass, args) # type: ignore diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index fbb0a7470..8c45c44ee 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -300,8 +300,7 @@ class ProgressManager: def _closeWin(self) -> None: # if the parent window has been deleted, the progress dialog may have # already been dropped; delete it if it hasn't been - if not sip.isdeleted(self._win): - assert self._win is not None + if self._win and not sip.isdeleted(self._win): self._win.cancel() self._win = None self._shown = 0 diff --git a/qt/aqt/qt/__init__.py b/qt/aqt/qt/__init__.py index ea1b4bd46..11670e90c 100644 --- a/qt/aqt/qt/__init__.py +++ b/qt/aqt/qt/__init__.py @@ -11,20 +11,12 @@ import traceback from collections.abc import Callable from typing import TypeVar, Union -try: - import PyQt6 -except Exception: - from .qt5 import * # type: ignore -else: - if os.getenv("ENABLE_QT5_COMPAT"): - print("Running with temporary Qt5 compatibility shims.") - from . import qt5_compat # needs to be imported first - from .qt6 import * +from anki._legacy import deprecated +# legacy code depends on these re-exports from anki.utils import is_mac, is_win -# fix buggy ubuntu12.04 display of language selector -os.environ["LIBOVERLAY_SCROLLBAR"] = "0" +from .qt6 import * def debug() -> None: @@ -52,7 +44,7 @@ qtminor = _version.minorVersion() qtpoint = _version.microVersion() qtfullversion = _version.segments() -if qtmajor < 5 or (qtmajor == 5 and qtminor < 14): +if qtmajor == 6 and qtminor < 2: raise Exception("Anki does not support your Qt version.") @@ -64,11 +56,6 @@ def qconnect(signal: Callable | pyqtSignal | pyqtBoundSignal, func: Callable) -> _T = TypeVar("_T") +@deprecated(info="no longer required, and now a no-op") def without_qt5_compat_wrapper(cls: _T) -> _T: - """Remove Qt5 compat wrapper from Qt class, if active. - - Only needed for a few Qt APIs that deal with QVariants.""" - if fn := getattr(cls, "_without_compat_wrapper", None): - return fn() - else: - return cls + return cls diff --git a/qt/aqt/qt/qt5.py b/qt/aqt/qt/qt5.py deleted file mode 100644 index 0a45dffb9..000000000 --- a/qt/aqt/qt/qt5.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# make sure not to optimize imports on this file -# pylint: skip-file - -""" -PyQt5 imports -""" - -from PyQt5.QtCore import * # type: ignore -from PyQt5.QtGui import * # type: ignore -from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy # type: ignore -from PyQt5.QtWebChannel import QWebChannel # type: ignore -from PyQt5.QtWebEngineCore import * # type: ignore -from PyQt5.QtWebEngineWidgets import * # type: ignore -from PyQt5.QtWidgets import * # type: ignore - -try: - from PyQt5 import sip # type: ignore -except ImportError: - import sip # type: ignore diff --git a/qt/aqt/qt/qt5_audio.py b/qt/aqt/qt/qt5_audio.py deleted file mode 100644 index cc8426a6e..000000000 --- a/qt/aqt/qt/qt5_audio.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# pylint: skip-file - -""" -PyQt5-only audio code -""" - -import wave -from collections.abc import Callable -from concurrent.futures import Future -from typing import cast - -import aqt - -from . import * # isort:skip -from ..sound import Recorder # isort:skip -from ..utils import showWarning # isort:skip - - -class QtAudioInputRecorder(Recorder): - def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: - super().__init__(output_path) - - self.mw = mw - self._parent = parent - - from PyQt5.QtMultimedia import ( # type: ignore - QAudioDeviceInfo, - QAudioFormat, - QAudioInput, - ) - - format = QAudioFormat() - format.setChannelCount(1) - format.setSampleRate(44100) - format.setSampleSize(16) - format.setCodec("audio/pcm") - format.setByteOrder(QAudioFormat.LittleEndian) - format.setSampleType(QAudioFormat.SignedInt) - - device = QAudioDeviceInfo.defaultInputDevice() - if not device.isFormatSupported(format): - format = device.nearestFormat(format) - print("format changed") - print("channels", format.channelCount()) - print("rate", format.sampleRate()) - print("size", format.sampleSize()) - self._format = format - - self._audio_input = QAudioInput(device, format, parent) - - def start(self, on_done: Callable[[], None]) -> None: - self._iodevice = self._audio_input.start() - self._buffer = bytearray() - qconnect(self._iodevice.readyRead, self._on_read_ready) - super().start(on_done) - - def _on_read_ready(self) -> None: - self._buffer.extend(cast(bytes, self._iodevice.readAll())) - - def stop(self, on_done: Callable[[str], None]) -> None: - def on_stop_timer() -> None: - # read anything remaining in buffer & stop - self._on_read_ready() - self._audio_input.stop() - - if err := self._audio_input.error(): - showWarning(f"recording failed: {err}") - return - - def write_file() -> None: - # swallow the first 300ms to allow audio device to quiesce - wait = int(44100 * self.STARTUP_DELAY) - if len(self._buffer) <= wait: - return - self._buffer = self._buffer[wait:] - - # write out the wave file - wf = wave.open(self.output_path, "wb") - wf.setnchannels(self._format.channelCount()) - wf.setsampwidth(self._format.sampleSize() // 8) - wf.setframerate(self._format.sampleRate()) - wf.writeframes(self._buffer) - wf.close() - - def and_then(fut: Future) -> None: - fut.result() - Recorder.stop(self, on_done) - - self.mw.taskman.run_in_background(write_file, and_then) - - # schedule the stop for half a second in the future, - # to avoid truncating the end of the recording - self._stop_timer = t = QTimer(self._parent) - t.timeout.connect(on_stop_timer) # type: ignore - t.setSingleShot(True) - t.start(500) diff --git a/qt/aqt/qt/qt5_compat.py b/qt/aqt/qt/qt5_compat.py deleted file mode 100644 index ef281b87c..000000000 --- a/qt/aqt/qt/qt5_compat.py +++ /dev/null @@ -1,411 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# type: ignore -# pylint: disable=unused-import - -""" -Patches and aliases that provide a PyQt5 → PyQt6 compatibility shim for add-ons -""" - -import sys -import types -import typing - -import PyQt6.QtCore -import PyQt6.QtDBus -import PyQt6.QtGui -import PyQt6.QtNetwork -import PyQt6.QtPrintSupport -import PyQt6.QtWebChannel -import PyQt6.QtWebEngineCore -import PyQt6.QtWebEngineWidgets -import PyQt6.QtWidgets - -from anki._legacy import print_deprecation_warning - -# Globally alias PyQt5 to PyQt6 -# ######################################################################### - -sys.modules["PyQt5"] = PyQt6 -# Need to alias QtCore explicitly as sip otherwise complains about repeat registration -sys.modules["PyQt5.QtCore"] = PyQt6.QtCore -# Need to alias QtWidgets and QtGui explicitly to facilitate patches -sys.modules["PyQt5.QtGui"] = PyQt6.QtGui -sys.modules["PyQt5.QtWidgets"] = PyQt6.QtWidgets -# Needed to maintain import order between QtWebEngineWidgets and QCoreApplication: -sys.modules["PyQt5.QtWebEngineWidgets"] = PyQt6.QtWebEngineWidgets -# Register other aliased top-level Qt modules just to be safe: -sys.modules["PyQt5.QtWebEngineCore"] = PyQt6.QtWebEngineCore -sys.modules["PyQt5.QtWebChannel"] = PyQt6.QtWebChannel -sys.modules["PyQt5.QtNetwork"] = PyQt6.QtNetwork -# Alias sip -sys.modules["sip"] = PyQt6.sip - -# Restore QWebEnginePage.view() -# ######################################################################## - -from PyQt6.QtWebEngineCore import QWebEnginePage -from PyQt6.QtWebEngineWidgets import QWebEngineView - - -def qwebenginepage_view(page: QWebEnginePage) -> QWebEnginePage: - print_deprecation_warning( - "'QWebEnginePage.view()' is deprecated. " - "Please use 'QWebEngineView.forPage(page)'" - ) - return QWebEngineView.forPage(page) - - -PyQt6.QtWebEngineCore.QWebEnginePage.view = qwebenginepage_view - -# Alias removed exec_ methods to exec -# ######################################################################## - -from PyQt6.QtCore import QCoreApplication, QEventLoop, QThread -from PyQt6.QtGui import QDrag, QGuiApplication -from PyQt6.QtWidgets import QApplication, QDialog, QMenu - - -# This helper function is needed as aliasing exec_ to exec directly will cause -# an unbound method error, even when wrapped with types.MethodType -def qt_exec_(object, *args, **kwargs): - class_name = object.__class__.__name__ - print_deprecation_warning( - f"'{class_name}.exec_()' is deprecated. Please use '{class_name}.exec()'" - ) - return object.exec(*args, **kwargs) - - -QCoreApplication.exec_ = qt_exec_ -QEventLoop.exec_ = qt_exec_ -QThread.exec_ = qt_exec_ -QDrag.exec_ = qt_exec_ -QGuiApplication.exec_ = qt_exec_ -QApplication.exec_ = qt_exec_ -QDialog.exec_ = qt_exec_ -QMenu.exec_ = qt_exec_ - -# Graciously handle removed Qt resource system -# ######################################################################## - -# Given that add-ons mostly use the Qt resource system to equip UI elements with -# icons – which oftentimes are not essential to the core UX –, printing a warning -# instead of preventing the add-on from loading seems appropriate. - - -def qt_resource_system_call(*args, **kwargs): - print_deprecation_warning( - "The Qt resource system no longer works on PyQt6. " - "Use QDir.addSearchPath() or mw.addonManager.setWebExports() instead." - ) - - -PyQt6.QtCore.qRegisterResourceData = qt_resource_system_call -PyQt6.QtCore.qUnregisterResourceData = qt_resource_system_call - -# Patch unscoped enums back in, aliasing them to scoped enums -# ######################################################################## - -PyQt6.QtWidgets.QDockWidget.AllDockWidgetFeatures = ( - PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable - | PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable - | PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable -) - -# when we subclass QIcon, icons fail to show when returned by getData() -# in a tableview/treeview, so we need to manually alias these -PyQt6.QtGui.QIcon.Active = PyQt6.QtGui.QIcon.Mode.Active -PyQt6.QtGui.QIcon.Disabled = PyQt6.QtGui.QIcon.Mode.Disabled -PyQt6.QtGui.QIcon.Normal = PyQt6.QtGui.QIcon.Mode.Normal -PyQt6.QtGui.QIcon.Selected = PyQt6.QtGui.QIcon.Mode.Selected -PyQt6.QtGui.QIcon.Off = PyQt6.QtGui.QIcon.State.Off -PyQt6.QtGui.QIcon.On = PyQt6.QtGui.QIcon.State.On - -# This is the subset of enums used in all public Anki add-ons as of 2021-10-19. -# Please note that this list is likely to be incomplete as the process used to -# find them probably missed dynamically constructed enums. -# Also, as mostly only public Anki add-ons were taken into consideration, -# some enums in other add-ons might not be included. In those cases please -# consider filing a PR to extend the assignments below. - -# Important: These patches are not meant to provide compatibility for all -# add-ons going forward, but simply to maintain support with already -# existing add-ons. Add-on authors should take heed to use scoped enums -# in any future code changes. - -# (module, [(type_name, enums)]) -_enum_map = ( - ( - PyQt6.QtCore, - [ - ("QEvent", ("Type",)), - ("QEventLoop", ("ProcessEventsFlag",)), - ("QIODevice", ("OpenModeFlag",)), - ("QItemSelectionModel", ("SelectionFlag",)), - ("QLocale", ("Country", "Language")), - ("QMetaType", ("Type",)), - ("QProcess", ("ProcessState", "ProcessChannel")), - ("QStandardPaths", ("StandardLocation",)), - ( - "Qt", - ( - "AlignmentFlag", - "ApplicationAttribute", - "ArrowType", - "AspectRatioMode", - "BrushStyle", - "CaseSensitivity", - "CheckState", - "ConnectionType", - "ContextMenuPolicy", - "CursorShape", - "DateFormat", - "DayOfWeek", - "DockWidgetArea", - "FindChildOption", - "FocusPolicy", - "FocusReason", - "GlobalColor", - "HighDpiScaleFactorRoundingPolicy", - "ImageConversionFlag", - "InputMethodHint", - "ItemDataRole", - "ItemFlag", - "KeyboardModifier", - "LayoutDirection", - "MatchFlag", - "Modifier", - "MouseButton", - "Orientation", - "PenCapStyle", - "PenJoinStyle", - "PenStyle", - "ScrollBarPolicy", - "ShortcutContext", - "SortOrder", - "TextElideMode", - "TextFlag", - "TextFormat", - "TextInteractionFlag", - "ToolBarArea", - "ToolButtonStyle", - "TransformationMode", - "WidgetAttribute", - "WindowModality", - "WindowState", - "WindowType", - "Key", - ), - ), - ("QThread", ("Priority",)), - ], - ), - (PyQt6.QtDBus, [("QDBus", ("CallMode",))]), - ( - PyQt6.QtGui, - [ - ("QAction", ("MenuRole", "ActionEvent")), - ("QClipboard", ("Mode",)), - ("QColor", ("NameFormat",)), - ("QFont", ("Style", "Weight", "StyleHint")), - ("QFontDatabase", ("WritingSystem", "SystemFont")), - ("QImage", ("Format",)), - ("QKeySequence", ("SequenceFormat", "StandardKey")), - ("QMovie", ("CacheMode",)), - ("QPageLayout", ("Orientation",)), - ("QPageSize", ("PageSizeId",)), - ("QPainter", ("RenderHint",)), - ("QPalette", ("ColorRole", "ColorGroup")), - ("QTextCharFormat", ("UnderlineStyle",)), - ("QTextCursor", ("MoveOperation", "MoveMode", "SelectionType")), - ("QTextFormat", ("Property",)), - ("QTextOption", ("WrapMode",)), - ("QValidator", ("State",)), - ], - ), - (PyQt6.QtNetwork, [("QHostAddress", ("SpecialAddress",))]), - (PyQt6.QtPrintSupport, [("QPrinter", ("Unit",))]), - ( - PyQt6.QtWebEngineCore, - [ - ("QWebEnginePage", ("WebWindowType", "FindFlag", "WebAction")), - ("QWebEngineProfile", ("PersistentCookiesPolicy", "HttpCacheType")), - ("QWebEngineScript", ("ScriptWorldId", "InjectionPoint")), - ("QWebEngineSettings", ("FontSize", "WebAttribute")), - ], - ), - ( - PyQt6.QtWidgets, - [ - ( - "QAbstractItemView", - ( - "CursorAction", - "DropIndicatorPosition", - "ScrollMode", - "EditTrigger", - "SelectionMode", - "SelectionBehavior", - "DragDropMode", - "ScrollHint", - ), - ), - ("QAbstractScrollArea", ("SizeAdjustPolicy",)), - ("QAbstractSpinBox", ("ButtonSymbols",)), - ("QBoxLayout", ("Direction",)), - ("QColorDialog", ("ColorDialogOption",)), - ("QComboBox", ("SizeAdjustPolicy", "InsertPolicy")), - ("QCompleter", ("CompletionMode",)), - ("QDateTimeEdit", ("Section",)), - ("QDialog", ("DialogCode",)), - ("QDialogButtonBox", ("StandardButton", "ButtonRole")), - ("QDockWidget", ("DockWidgetFeature",)), - ("QFileDialog", ("Option", "FileMode", "AcceptMode", "DialogLabel")), - ("QFormLayout", ("FieldGrowthPolicy", "ItemRole")), - ("QFrame", ("Shape", "Shadow")), - ("QGraphicsItem", ("GraphicsItemFlag",)), - ("QGraphicsPixmapItem", ("ShapeMode",)), - ("QGraphicsView", ("ViewportAnchor", "DragMode")), - ("QHeaderView", ("ResizeMode",)), - ("QLayout", ("SizeConstraint",)), - ("QLineEdit", ("EchoMode",)), - ( - "QListView", - ("Flow", "BrowserLayout", "ResizeMode", "Movement", "ViewMode"), - ), - ("QListWidgetItem", ("ItemType",)), - ("QMessageBox", ("StandardButton", "Icon", "ButtonRole")), - ("QPlainTextEdit", ("LineWrapMode",)), - ("QProgressBar", ("Direction",)), - ("QRubberBand", ("Shape",)), - ("QSizePolicy", ("ControlType", "Policy")), - ("QSlider", ("TickPosition",)), - ( - "QStyle", - ( - "SubElement", - "ComplexControl", - "StandardPixmap", - "ControlElement", - "PixelMetric", - "StateFlag", - "SubControl", - ), - ), - ("QSystemTrayIcon", ("MessageIcon", "ActivationReason")), - ("QTabBar", ("ButtonPosition",)), - ("QTabWidget", ("TabShape", "TabPosition")), - ("QTextEdit", ("LineWrapMode",)), - ("QToolButton", ("ToolButtonPopupMode",)), - ("QWizard", ("WizardStyle", "WizardOption")), - ], - ), -) - -_renamed_enum_cases = { - "QComboBox": { - "AdjustToMinimumContentsLength": "AdjustToMinimumContentsLengthWithIcon" - }, - "QDialogButtonBox": {"No": "NoButton"}, - "QPainter": {"HighQualityAntialiasing": "Antialiasing"}, - "QPalette": {"Background": "Window", "Foreground": "WindowText"}, - "Qt": {"MatchRegExp": "MatchRegularExpression", "MidButton": "MiddleButton"}, -} - - -# This works by wrapping each enum-containing Qt class (eg QAction) in a proxy. -# When an attribute is missing from the underlying Qt class, __getattr__ is -# called, and we try fetching the attribute from each of the declared enums -# for that module. If a match is found, a deprecation warning is printed. -# -# Looping through enumerations is not particularly efficient on a large type like -# Qt, but we only pay the cost when an attribute is not found. In the worst case, -# it's about 50ms per 1000 failed lookups on the Qt module. - - -def _instrument_type( - module: types.ModuleType, type_name: str, enums: list[str] -) -> None: - type = getattr(module, type_name) - renamed_attrs = _renamed_enum_cases.get(type_name, {}) - - class QtClassProxyType(type.__class__): - def __getattr__(cls, provided_name): # pylint: disable=no-self-argument - # we know this is not an enum - if provided_name == "__pyqtSignature__": - raise AttributeError - - name = renamed_attrs.get(provided_name) or provided_name - - for enum_name in enums: - enum = getattr(type, enum_name) - try: - val = getattr(enum, name) - except AttributeError: - continue - - print_deprecation_warning( - f"'{type_name}.{provided_name}' will stop working. Please use '{type_name}.{enum_name}.{name}' instead." - ) - return val - - return getattr(type, name) - - class QtClassProxy( - type, metaclass=QtClassProxyType - ): # pylint: disable=invalid-metaclass - @staticmethod - def _without_compat_wrapper(): - return type - - setattr(module, type_name, QtClassProxy) - - -for module, type_to_enum_list in _enum_map: - for type_name, enums in type_to_enum_list: - _instrument_type(module, type_name, enums) - -# Alias classes shifted between QtWidgets and QtGui -########################################################################## - -PyQt6.QtWidgets.QAction = PyQt6.QtGui.QAction -PyQt6.QtWidgets.QActionGroup = PyQt6.QtGui.QActionGroup -PyQt6.QtWidgets.QShortcut = PyQt6.QtGui.QShortcut - -# Alias classes shifted between QtWebEngineWidgets and QtWebEngineCore -########################################################################## - -PyQt6.QtWebEngineWidgets.QWebEnginePage = PyQt6.QtWebEngineCore.QWebEnginePage -PyQt6.QtWebEngineWidgets.QWebEngineHistory = PyQt6.QtWebEngineCore.QWebEngineHistory -PyQt6.QtWebEngineWidgets.QWebEngineProfile = PyQt6.QtWebEngineCore.QWebEngineProfile -PyQt6.QtWebEngineWidgets.QWebEngineScript = PyQt6.QtWebEngineCore.QWebEngineScript -PyQt6.QtWebEngineWidgets.QWebEngineScriptCollection = ( - PyQt6.QtWebEngineCore.QWebEngineScriptCollection -) -PyQt6.QtWebEngineWidgets.QWebEngineClientCertificateSelection = ( - PyQt6.QtWebEngineCore.QWebEngineClientCertificateSelection -) -PyQt6.QtWebEngineWidgets.QWebEngineSettings = PyQt6.QtWebEngineCore.QWebEngineSettings -PyQt6.QtWebEngineWidgets.QWebEngineFullScreenRequest = ( - PyQt6.QtWebEngineCore.QWebEngineFullScreenRequest -) -PyQt6.QtWebEngineWidgets.QWebEngineContextMenuData = ( - PyQt6.QtWebEngineCore.QWebEngineContextMenuRequest -) -PyQt6.QtWebEngineWidgets.QWebEngineDownloadItem = ( - PyQt6.QtWebEngineCore.QWebEngineDownloadRequest -) - -# Aliases for other miscellaneous class changes -########################################################################## - -PyQt6.QtCore.QRegExp = PyQt6.QtCore.QRegularExpression - - -# Mock the removed PyQt5.Qt module -########################################################################## - -sys.modules["PyQt5.Qt"] = sys.modules["aqt.qt"] -# support 'from PyQt5 import Qt', as it's an alias to PyQt6 -PyQt6.Qt = sys.modules["aqt.qt"] diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 5753ab234..8ff49024f 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -772,19 +772,14 @@ class RecordDialog(QDialog): saveGeom(self, "audioRecorder2") def _start_recording(self) -> None: - if qtmajor > 5: - if macos_helper and platform.machine() == "arm64": - self._recorder = NativeMacRecorder( - namedtmp("rec.wav"), - ) - else: - self._recorder = QtAudioInputRecorder( - namedtmp("rec.wav"), self.mw, self._parent - ) + if macos_helper and platform.machine() == "arm64": + self._recorder = NativeMacRecorder( + namedtmp("rec.wav"), + ) else: - from aqt.qt.qt5_audio import QtAudioInputRecorder as Qt5Recorder - - self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent) + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) self._recorder.start(self._start_timer) def _start_timer(self) -> None: diff --git a/qt/tools/build_ui.py b/qt/tools/build_ui.py index 776375598..b87031213 100644 --- a/qt/tools/build_ui.py +++ b/qt/tools/build_ui.py @@ -6,16 +6,10 @@ from __future__ import annotations import io import re import sys +from dataclasses import dataclass from pathlib import Path -try: - from PyQt6.uic import compileUi -except ImportError: - # ARM64 Linux builds may not have access to PyQt6, and may have aliased - # it to PyQt5. We allow fallback, but the _qt6.py files will not be valid. - from PyQt5.uic import compileUi # type: ignore - -from dataclasses import dataclass +from PyQt6.uic import compileUi def compile(ui_file: str | Path) -> str: @@ -53,21 +47,9 @@ def with_fixes_for_qt6(code: str) -> str: return "\n".join(outlines) -def with_fixes_for_qt5(code: str) -> str: - code = code.replace( - "from PyQt5 import QtCore, QtGui, QtWidgets", - "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom aqt.utils import tr\n", - ) - code = code.replace("Qt6", "Qt5") - code = code.replace("QtGui.QAction", "QtWidgets.QAction") - code = code.replace("import icons_rc", "") - return code - - @dataclass class UiFileAndOutputs: ui_file: Path - qt5_file: str qt6_file: str @@ -82,7 +64,6 @@ def get_files() -> list[UiFileAndOutputs]: out.append( UiFileAndOutputs( ui_file=path, - qt5_file=outpath.replace(".ui", "_qt5.py"), qt6_file=outpath.replace(".ui", "_qt6.py"), ) ) @@ -93,8 +74,5 @@ if __name__ == "__main__": for entry in get_files(): stock = compile(entry.ui_file) for_qt6 = with_fixes_for_qt6(stock) - for_qt5 = with_fixes_for_qt5(for_qt6) - with open(entry.qt5_file, "w") as file: - file.write(for_qt5) with open(entry.qt6_file, "w") as file: file.write(for_qt6) From a41c60c016065b792a3ea894ad8bef6929af90c4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 19 Jun 2025 22:17:11 +0700 Subject: [PATCH 003/197] Trigger uv sync if user approves update --- ftl/qt/qt-misc.ftl | 1 + qt/aqt/update.py | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/ftl/qt/qt-misc.ftl b/ftl/qt/qt-misc.ftl index 294cd8a83..60c22ef8b 100644 --- a/ftl/qt/qt-misc.ftl +++ b/ftl/qt/qt-misc.ftl @@ -73,6 +73,7 @@ qt-misc-second = qt-misc-layout-auto-enabled = Responsive layout enabled qt-misc-layout-vertical-enabled = Vertical layout enabled qt-misc-layout-horizontal-enabled = Horizontal layout enabled +qt-misc-please-restart-to-update-anki = Please restart Anki to update to the latest version. ## deprecated- these strings will be removed in the future, and do not need ## to be translated diff --git a/qt/aqt/update.py b/qt/aqt/update.py index fd0e4eafd..d8e92426c 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -1,13 +1,16 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import os +from pathlib import Path + 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 anki.utils import dev_mode, int_time, int_version, is_mac, is_win, plat_desc from aqt.operations import QueryOp from aqt.qt import * -from aqt.utils import openLink, show_warning, showText, tr +from aqt.utils import show_info, show_warning, showText, tr def check_for_update() -> None: @@ -77,4 +80,33 @@ def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None: # ignore this update mw.pm.meta["suppressUpdate"] = ver elif ret == QMessageBox.StandardButton.Yes: - openLink(aqt.appWebsiteDownloadSection) + update_and_restart() + + +def update_and_restart() -> None: + """Download and install the update, then restart Anki.""" + update_on_next_run() + # todo: do this automatically in the future + show_info(tr.qt_misc_please_restart_to_update_anki()) + + +def update_on_next_run() -> None: + """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + try: + # Get the local data directory equivalent to Rust's dirs::data_local_dir() + if is_win: + data_dir = Path(os.environ.get("LOCALAPPDATA", "")) + elif is_mac: + data_dir = Path.home() / "Library" / "Application Support" + else: # Linux + data_dir = Path( + os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share") + ) + + pyproject_path = data_dir / "AnkiProgramFiles" / "pyproject.toml" + + if pyproject_path.exists(): + # Touch the file to update its mtime + pyproject_path.touch() + except Exception as e: + print(e) From a60a955c611bcc2d4435c0b9ba5975d2ab2c1c6c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 19 Jun 2025 23:33:51 +0700 Subject: [PATCH 004/197] Handle beta/rc tags, bump beta, add exact version pin to aqt --- .version | 2 +- build/configure/src/python.rs | 70 +++++++++++++++++++++++++++++++---- pylib/pyproject.toml | 3 +- qt/hatch_build.py | 26 +++++++++++++ qt/pyproject.toml | 4 +- tools/publish | 6 ++- 6 files changed, 96 insertions(+), 15 deletions(-) diff --git a/.version b/.version index 9bab2a4b4..d1e34a036 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.06 +25.06b2 diff --git a/build/configure/src/python.rs b/build/configure/src/python.rs index 7cd66b76e..474a55f31 100644 --- a/build/configure/src/python.rs +++ b/build/configure/src/python.rs @@ -20,6 +20,45 @@ use ninja_gen::python::PythonTypecheck; use ninja_gen::rsync::RsyncFiles; use ninja_gen::Build; +/// Normalize version string by removing leading zeros from numeric parts +/// while preserving pre-release markers (b1, rc2, a3, etc.) +fn normalize_version(version: &str) -> String { + version + .split('.') + .map(|part| { + // Check if the part contains only digits + if part.chars().all(|c| c.is_ascii_digit()) { + // Numeric part: remove leading zeros + part.parse::().unwrap_or(0).to_string() + } else { + // Mixed part (contains both numbers and pre-release markers) + // Split on first non-digit character and normalize the numeric prefix + let chars = part.chars(); + let mut numeric_prefix = String::new(); + let mut rest = String::new(); + let mut found_non_digit = false; + + for ch in chars { + if ch.is_ascii_digit() && !found_non_digit { + numeric_prefix.push(ch); + } else { + found_non_digit = true; + rest.push(ch); + } + } + + if numeric_prefix.is_empty() { + part.to_string() + } else { + let normalized_prefix = numeric_prefix.parse::().unwrap_or(0).to_string(); + format!("{}{}", normalized_prefix, rest) + } + } + }) + .collect::>() + .join(".") +} + pub fn setup_venv(build: &mut Build) -> Result<()> { let extra_binary_exports = &[ "mypy", @@ -131,14 +170,7 @@ impl BuildAction for BuildWheel { let name = self.name; - // Normalize version like hatchling does: remove leading zeros from version - // parts - let normalized_version = self - .version - .split('.') - .map(|part| part.parse::().unwrap_or(0).to_string()) - .collect::>() - .join("."); + let normalized_version = normalize_version(&self.version); let wheel_path = format!("wheels/{name}-{normalized_version}-{tag}.whl"); build.add_outputs("out", vec![wheel_path]); @@ -279,3 +311,25 @@ pub(crate) fn setup_sphinx(build: &mut Build) -> Result<()> { )?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_version_basic() { + assert_eq!(normalize_version("1.2.3"), "1.2.3"); + assert_eq!(normalize_version("01.02.03"), "1.2.3"); + assert_eq!(normalize_version("1.0.0"), "1.0.0"); + } + + #[test] + fn test_normalize_version_with_prerelease() { + assert_eq!(normalize_version("1.2.3b1"), "1.2.3b1"); + assert_eq!(normalize_version("01.02.03b1"), "1.2.3b1"); + assert_eq!(normalize_version("1.0.0rc2"), "1.0.0rc2"); + assert_eq!(normalize_version("2.1.0a3"), "2.1.0a3"); + assert_eq!(normalize_version("1.2.3beta1"), "1.2.3beta1"); + assert_eq!(normalize_version("1.2.3alpha1"), "1.2.3alpha1"); + } +} diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml index a12c6848b..555f30c86 100644 --- a/pylib/pyproject.toml +++ b/pylib/pyproject.toml @@ -1,7 +1,6 @@ [project] name = "anki" -# dynamic = ["version"] -version = "0.1.2" +dynamic = ["version"] requires-python = ">=3.9" license = "AGPL-3.0-or-later" dependencies = [ diff --git a/qt/hatch_build.py b/qt/hatch_build.py index e475b5d84..52ca7a0ec 100644 --- a/qt/hatch_build.py +++ b/qt/hatch_build.py @@ -18,6 +18,9 @@ class CustomBuildHook(BuildHookInterface): """Initialize the build hook.""" force_include = build_data.setdefault("force_include", {}) + # Pin anki== + self._set_anki_dependency(version, build_data) + # Look for generated files in out/qt/_aqt project_root = Path(self.root).parent generated_root = project_root / "out" / "qt" / "_aqt" @@ -30,6 +33,29 @@ class CustomBuildHook(BuildHookInterface): assert generated_root.exists(), "you should build with --wheel" self._add_aqt_files(force_include, generated_root) + def _set_anki_dependency(self, version: str, build_data: Dict[str, Any]) -> None: + # Get current dependencies and replace 'anki' with exact version + dependencies = build_data.setdefault("dependencies", []) + + # Remove any existing anki dependency + dependencies[:] = [dep for dep in dependencies if not dep.startswith("anki")] + + # Handle version detection + actual_version = version + if version == "standard": + # Read actual version from .version file + project_root = Path(self.root).parent + version_file = project_root / ".version" + if version_file.exists(): + actual_version = version_file.read_text().strip() + + # Only add exact version for real releases, not editable installs + if actual_version != "editable": + dependencies.append(f"anki=={actual_version}") + else: + # For editable installs, just add anki without version constraint + dependencies.append("anki") + def _add_aqt_files(self, force_include: Dict[str, str], aqt_root: Path) -> None: """Add _aqt files to the build.""" for path in aqt_root.rglob("*"): diff --git a/qt/pyproject.toml b/qt/pyproject.toml index d8b4c4306..0c1d1b2ae 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -1,7 +1,6 @@ [project] name = "aqt" -# dynamic = ["version"] -version = "0.1.2" +dynamic = ["version"] requires-python = ">=3.9" license = "AGPL-3.0-or-later" dependencies = [ @@ -25,6 +24,7 @@ dependencies = [ "types-pywin32", "pyqt6>=6.2", "pyqt6-webengine>=6.2", + # anki dependency is added dynamically in hatch_build.py with exact version ] [project.optional-dependencies] diff --git a/tools/publish b/tools/publish index 570f75c85..4c625c36d 100755 --- a/tools/publish +++ b/tools/publish @@ -2,6 +2,8 @@ set -e -export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) -out/extracted/uv/uv publish --index testpypi out/wheels/* +#export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) +#out/extracted/uv/uv publish --index testpypi out/wheels/* +export UV_PUBLISH_TOKEN=$(pass show w/pypi-api) +out/extracted/uv/uv publish out/wheels/* From 4abc0eb8b843c33b2930d837e11f5d26137cb872 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 00:45:30 +0700 Subject: [PATCH 005/197] Use same version for anki-release; publish to main index --- qt/pyproject.toml | 3 +++ qt/release/publish.sh | 6 +++--- qt/release/update.sh | 12 +++--------- uv.lock | 4 +--- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/qt/pyproject.toml b/qt/pyproject.toml index 0c1d1b2ae..0dce88c99 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -62,6 +62,9 @@ conflicts = [ ], ] +[tool.uv.sources] +anki = { workspace = true } + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/qt/release/publish.sh b/qt/release/publish.sh index b906637f9..273e34953 100755 --- a/qt/release/publish.sh +++ b/qt/release/publish.sh @@ -1,7 +1,5 @@ #!/bin/bash -export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) - # Get the project root (two levels up from qt/release) PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" @@ -10,4 +8,6 @@ UV="$PROJ_ROOT/out/extracted/uv/uv" rm -rf dist "$UV" build --wheel -"$UV" publish --index testpypi + +#UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) "$UV" publish --index testpypi +UV_PUBLISH_TOKEN=$(pass show w/pypi-api) "$UV" publish diff --git a/qt/release/update.sh b/qt/release/update.sh index e25eaec49..ddd99c398 100755 --- a/qt/release/update.sh +++ b/qt/release/update.sh @@ -13,8 +13,8 @@ PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" # Use extracted uv binary UV="$PROJ_ROOT/out/extracted/uv/uv" -# Prompt for wheel version -read -p "Wheel version: " VERSION +# Read version from .version file +VERSION=$(cat "$PROJ_ROOT/.version" | tr -d '[:space:]') # Copy existing pyproject.toml to .old if it exists if [ -f pyproject.toml ]; then @@ -24,7 +24,7 @@ fi # Export dependencies using uv echo "Exporting dependencies..." rm -f pyproject.toml -DEPS=$("$UV" export --no-hashes --no-annotate --no-header --extra audio --extra qt --all-packages --no-dev --no-emit-workspace) +DEPS=$(cd "$PROJ_ROOT" && "$UV" export --no-hashes --no-annotate --no-header --extra audio --extra qt --all-packages --no-dev --no-emit-workspace) # Generate the pyproject.toml file cat > pyproject.toml << EOF @@ -49,12 +49,6 @@ done cat >> pyproject.toml << 'EOF' ] -[[tool.uv.index]] -name = "testpypi" -url = "https://test.pypi.org/simple/" -publish-url = "https://test.pypi.org/legacy/" -explicit = true - [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 51b16f8ab..9b713648d 100644 --- a/uv.lock +++ b/uv.lock @@ -48,7 +48,6 @@ wheels = [ [[package]] name = "anki" -version = "0.1.2" source = { editable = "pylib" } dependencies = [ { name = "beautifulsoup4" }, @@ -151,7 +150,6 @@ wheels = [ [[package]] name = "aqt" -version = "0.1.2" source = { editable = "qt" } dependencies = [ { name = "anki-mac-helper", marker = "sys_platform == 'darwin' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, @@ -246,7 +244,7 @@ requires-dist = [ { name = "types-waitress" }, { name = "waitress", specifier = ">=2.0.0" }, ] -provides-extras = ["audio", "qt66", "qt67", "qt"] +provides-extras = ["audio", "qt", "qt66", "qt67"] [[package]] name = "astroid" From cd7193150674e636585d81bc5017fc6383539f88 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 01:20:13 +0700 Subject: [PATCH 006/197] Launcher tweaks - Handle beta/rc tags in .version when launching Anki - Update pyproject.toml/.python_version if distributed version newer - Support prerelease marker to opt in to betas - Check for updates when using uv sync - Avoid system Python by default, as it can cause breakages (e.g. ARM Python installed on Windows) --- pylib/anki/utils.py | 9 +++++++-- qt/hatch_build.py | 6 +++--- qt/launcher/mac/build.sh | 1 + qt/launcher/pyproject.toml | 14 -------------- qt/launcher/src/main.rs | 39 +++++++++++++++++++------------------- rslib/io/src/lib.rs | 36 +++++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 46daa3b97..c61fd0588 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -309,12 +309,17 @@ def int_version() -> int: """Anki's version as an integer in the form YYMMPP, e.g. 230900. (year, month, patch). In 2.1.x releases, this was just the last number.""" + import re + from anki.buildinfo import version + # Strip non-numeric characters (handles beta/rc suffixes like '25.02b1' or 'rc3') + numeric_version = re.sub(r"[^0-9.]", "", version) + try: - [year, month, patch] = version.split(".") + [year, month, patch] = numeric_version.split(".") except ValueError: - [year, month] = version.split(".") + [year, month] = numeric_version.split(".") patch = "0" year_num = int(year) diff --git a/qt/hatch_build.py b/qt/hatch_build.py index 52ca7a0ec..fc716a57f 100644 --- a/qt/hatch_build.py +++ b/qt/hatch_build.py @@ -36,10 +36,10 @@ class CustomBuildHook(BuildHookInterface): def _set_anki_dependency(self, version: str, build_data: Dict[str, Any]) -> None: # Get current dependencies and replace 'anki' with exact version dependencies = build_data.setdefault("dependencies", []) - + # Remove any existing anki dependency dependencies[:] = [dep for dep in dependencies if not dep.startswith("anki")] - + # Handle version detection actual_version = version if version == "standard": @@ -48,7 +48,7 @@ class CustomBuildHook(BuildHookInterface): version_file = project_root / ".version" if version_file.exists(): actual_version = version_file.read_text().strip() - + # Only add exact version for real releases, not editable installs if actual_version != "editable": dependencies.append(f"anki=={actual_version}") diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh index 8a60d488d..eb4483488 100755 --- a/qt/launcher/mac/build.sh +++ b/qt/launcher/mac/build.sh @@ -8,6 +8,7 @@ APP_LAUNCHER="$OUTPUT_DIR/Anki.app" rm -rf "$APP_LAUNCHER" # Build binaries for both architectures +rustup target add aarch64-apple-darwin x86_64-apple-darwin cargo build -p launcher --release --target aarch64-apple-darwin cargo build -p launcher --release --target x86_64-apple-darwin (cd ../../.. && ./ninja launcher:uv_universal) diff --git a/qt/launcher/pyproject.toml b/qt/launcher/pyproject.toml index 6ba027844..2a45626c7 100644 --- a/qt/launcher/pyproject.toml +++ b/qt/launcher/pyproject.toml @@ -5,18 +5,4 @@ description = "UV-based launcher for Anki." requires-python = ">=3.9" dependencies = [ "anki-release", - # so we can use testpypi - "anki", - "aqt", ] - -[tool.uv.sources] -anki-release = { index = "testpypi" } -anki = { index = "testpypi" } -aqt = { index = "testpypi" } - -[[tool.uv.index]] -name = "testpypi" -url = "https://test.pypi.org/simple/" -publish-url = "https://test.pypi.org/legacy/" -explicit = true diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 77268ce3a..e40253eff 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -6,9 +6,9 @@ use std::io::stdin; use std::process::Command; -use anki_io::copy_file; +use anki_io::copy_if_newer; use anki_io::create_dir_all; -use anki_io::metadata; +use anki_io::modified_time; use anki_io::remove_file; use anki_io::write_file; use anki_process::CommandExt; @@ -51,6 +51,7 @@ fn run() -> Result<()> { .join("AnkiProgramFiles"); let sync_complete_marker = uv_install_root.join(".sync_complete"); + let prerelease_marker = uv_install_root.join("prerelease"); let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; let dist_pyproject_path = resources_dir.join("pyproject.toml"); let user_pyproject_path = uv_install_root.join("pyproject.toml"); @@ -59,14 +60,15 @@ fn run() -> Result<()> { let uv_lock_path = uv_install_root.join("uv.lock"); let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name()); + // Create install directory and copy project files in + create_dir_all(&uv_install_root)?; + copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?; + copy_if_newer(&dist_python_version_path, &user_python_version_path)?; + let pyproject_has_changed = !user_pyproject_path.exists() || !sync_complete_marker.exists() || { - let pyproject_toml_time = metadata(&user_pyproject_path)? - .modified() - .context("Failed to get pyproject.toml modified time")?; - let sync_complete_time = metadata(&sync_complete_marker)? - .modified() - .context("Failed to get sync marker modified time")?; + let pyproject_toml_time = modified_time(&user_pyproject_path)?; + let sync_complete_time = modified_time(&sync_complete_marker)?; Ok::(pyproject_toml_time > sync_complete_time) } .unwrap_or(true); @@ -81,22 +83,21 @@ fn run() -> Result<()> { // we'll need to launch uv; reinvoke ourselves in a terminal so the user can see handle_terminal_launch()?; - // Create install directory and copy project files in - create_dir_all(&uv_install_root)?; - if !user_pyproject_path.exists() { - copy_file(&dist_pyproject_path, &user_pyproject_path)?; - copy_file(&dist_python_version_path, &user_python_version_path)?; - } - // Remove sync marker before attempting sync let _ = remove_file(&sync_complete_marker); // Sync the venv - if let Err(e) = Command::new(&uv_path) + let mut command = Command::new(&uv_path); + command .current_dir(&uv_install_root) - .args(["sync", "--refresh"]) - .ensure_success() - { + .args(["sync", "--upgrade", "--managed-python"]); + + // Set UV_PRERELEASE=allow if prerelease file exists + if prerelease_marker.exists() { + command.env("UV_PRERELEASE", "allow"); + } + + if let Err(e) = command.ensure_success() { // If sync fails due to things like a missing wheel on pypi, // we need to remove the lockfile or uv will cache the bad result. let _ = remove_file(&uv_lock_path); diff --git a/rslib/io/src/lib.rs b/rslib/io/src/lib.rs index c1d4c0205..cb44467e6 100644 --- a/rslib/io/src/lib.rs +++ b/rslib/io/src/lib.rs @@ -152,6 +152,34 @@ pub fn copy_file(src: impl AsRef, dst: impl AsRef) -> Result { }) } +/// Copy a file from src to dst if dst doesn't exist or if src is newer than +/// dst. Preserves the modification time from the source file. +pub fn copy_if_newer(src: impl AsRef, dst: impl AsRef) -> Result { + let src = src.as_ref(); + let dst = dst.as_ref(); + + let should_copy = if !dst.exists() { + true + } else { + let src_time = modified_time(src)?; + let dst_time = modified_time(dst)?; + src_time > dst_time + }; + + if should_copy { + copy_file(src, dst)?; + + // Preserve the modification time from the source file + let src_mtime = modified_time(src)?; + let times = FileTimes::new().set_modified(src_mtime); + set_file_times(dst, times)?; + + Ok(true) + } else { + Ok(false) + } +} + /// Like [read_file], but skips the section that is potentially locked by /// SQLite. pub fn read_locked_db_file(path: impl AsRef) -> Result> { @@ -188,6 +216,14 @@ pub fn metadata(path: impl AsRef) -> Result { }) } +/// Get the modification time of a file. +pub fn modified_time(path: impl AsRef) -> Result { + metadata(&path)?.modified().context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Metadata, + }) +} + pub fn new_tempfile() -> Result { NamedTempFile::new().context(FileIoSnafu { path: std::env::temp_dir(), From d542ae90653d2a3600f34cf7b21c6991883846f8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 16:37:54 +0700 Subject: [PATCH 007/197] Fix check action on Windows ARM - Update nextest (not required) - Build nextest without self-update, which pulls in ring - Disable running of tests in rsbridge, as it has no tests, and requires host arch's python.lib to execute - A double \ in CARGO_TARGET_DIR was breaking update_* tests --- build/ninja_gen/src/cargo.rs | 2 +- pylib/rsbridge/Cargo.toml | 1 + tools/ninja.bat | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build/ninja_gen/src/cargo.rs b/build/ninja_gen/src/cargo.rs index 645203170..2a3397704 100644 --- a/build/ninja_gen/src/cargo.rs +++ b/build/ninja_gen/src/cargo.rs @@ -162,7 +162,7 @@ impl BuildAction for CargoTest { "cargo-nextest", CargoInstall { binary_name: "cargo-nextest", - args: "cargo-nextest --version 0.9.57 --locked", + args: "cargo-nextest --version 0.9.99 --locked --no-default-features --features default-no-update", }, )?; setup_flags(build) diff --git a/pylib/rsbridge/Cargo.toml b/pylib/rsbridge/Cargo.toml index fbe76c8a5..22dca83fa 100644 --- a/pylib/rsbridge/Cargo.toml +++ b/pylib/rsbridge/Cargo.toml @@ -12,6 +12,7 @@ description = "Anki's Rust library code Python bindings" name = "rsbridge" crate-type = ["cdylib"] path = "lib.rs" +test = false [dependencies] anki.workspace = true diff --git a/tools/ninja.bat b/tools/ninja.bat index 7d939846e..6310103c3 100755 --- a/tools/ninja.bat +++ b/tools/ninja.bat @@ -1,5 +1,5 @@ @echo off -set CARGO_TARGET_DIR=%~dp0\..\out\rust +set CARGO_TARGET_DIR=%~dp0..\out\rust REM separate build+run steps so build env doesn't leak into subprocesses cargo build -p runner --release || exit /b 1 out\rust\release\runner build %* || exit /b 1 From b2dc5a02636333a27dc862e156a598725de42f3a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 16:08:00 +0700 Subject: [PATCH 008/197] Bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index d1e34a036..9079196d0 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.06b2 +25.06b4 From 718f39fdf382968348575f776aed26678f94546c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 19:05:30 +0700 Subject: [PATCH 009/197] Temporarily force-enable prereleases Some users are struggling to read or understand the steps to enable it. --- qt/launcher/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index e40253eff..4f397ad99 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -97,6 +97,9 @@ fn run() -> Result<()> { command.env("UV_PRERELEASE", "allow"); } + // temporarily force it on during initial beta testing + command.env("UV_PRERELEASE", "allow"); + if let Err(e) = command.ensure_success() { // If sync fails due to things like a missing wheel on pypi, // we need to remove the lockfile or uv will cache the bad result. From b781dfabf58e20b32b6f6ca8dfd1eb1a8ac1d45b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 20 Jun 2025 21:50:27 +0700 Subject: [PATCH 010/197] Add helpers to run Qt 6.7 and 6.9 Removed the 6.8 one, as that's our default --- qt/pyproject.toml | 8 + tools/{run-qt6.8 => run-qt6.7} | 4 +- tools/run-qt6.9 | 9 + uv.lock | 338 +++++++++++++++++++++------------ 4 files changed, 237 insertions(+), 122 deletions(-) rename tools/{run-qt6.8 => run-qt6.7} (70%) create mode 100755 tools/run-qt6.9 diff --git a/qt/pyproject.toml b/qt/pyproject.toml index 0dce88c99..e6537c76c 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -45,6 +45,13 @@ qt67 = [ "pyqt6-webengine-qt6==6.7.3", "pyqt6_sip==13.10.2", ] +qt69 = [ + "pyqt6==6.9.1", + "pyqt6-qt6==6.9.1", + "pyqt6-webengine==6.9.0", + "pyqt6-webengine-qt6==6.9.1", + "pyqt6_sip==13.10.2", +] qt = [ "pyqt6==6.8.0", "pyqt6-qt6==6.8.1", @@ -59,6 +66,7 @@ conflicts = [ { extra = "qt" }, { extra = "qt66" }, { extra = "qt67" }, + { extra = "qt69" }, ], ] diff --git a/tools/run-qt6.8 b/tools/run-qt6.7 similarity index 70% rename from tools/run-qt6.8 rename to tools/run-qt6.7 index 1628a1e33..d01d46cea 100755 --- a/tools/run-qt6.8 +++ b/tools/run-qt6.7 @@ -4,6 +4,6 @@ set -e ./ninja extract:uv -export PYENV=./out/pyenv68 -UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt68 +export PYENV=./out/pyenv67 +UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt67 ./run $* diff --git a/tools/run-qt6.9 b/tools/run-qt6.9 new file mode 100755 index 000000000..6576b6c81 --- /dev/null +++ b/tools/run-qt6.9 @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +./ninja extract:uv + +export PYENV=./out/pyenv69 +UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt69 +./run $* diff --git a/uv.lock b/uv.lock index 9b713648d..348d14dd0 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,7 @@ conflicts = [[ { package = "aqt", extra = "qt" }, { package = "aqt", extra = "qt66" }, { package = "aqt", extra = "qt67" }, + { package = "aqt", extra = "qt69" }, ]] [manifest] @@ -52,11 +53,11 @@ source = { editable = "pylib" } dependencies = [ { name = "beautifulsoup4" }, { name = "decorator" }, - { name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "markdown" }, { name = "orjson" }, { name = "protobuf" }, - { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "requests", extra = ["socks"] }, { name = "types-orjson" }, { name = "types-protobuf" }, @@ -97,9 +98,9 @@ source = { virtual = "." } [package.optional-dependencies] sphinx = [ - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "sphinx-autoapi" }, { name = "sphinx-rtd-theme" }, ] @@ -152,22 +153,24 @@ wheels = [ name = "aqt" source = { editable = "qt" } dependencies = [ - { name = "anki-mac-helper", marker = "sys_platform == 'darwin' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "anki-mac-helper", marker = "sys_platform == 'darwin' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "beautifulsoup4" }, { name = "flask" }, { name = "flask-cors" }, { name = "jsonschema" }, { name = "mock" }, - { name = "pip-system-certs", version = "4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "pip-system-certs", version = "5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6", version = "6.7.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67')" }, - { name = "pyqt6-webengine", version = "6.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67')" }, - { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "pip-system-certs", version = "4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pip-system-certs", version = "5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6", version = "6.7.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt69' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67')" }, + { name = "pyqt6-webengine", version = "6.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt69' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67')" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "requests" }, { name = "send2trash" }, { name = "types-decorator" }, @@ -181,7 +184,7 @@ dependencies = [ [package.optional-dependencies] audio = [ - { name = "anki-audio", marker = "sys_platform == 'darwin' or sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "anki-audio", marker = "sys_platform == 'darwin' or sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] qt = [ { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" } }, @@ -204,6 +207,13 @@ qt67 = [ { name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" } }, { name = "pyqt6-webengine-qt6", version = "6.7.3", source = { registry = "https://pypi.org/simple" } }, ] +qt69 = [ + { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, +] [package.metadata] requires-dist = [ @@ -220,19 +230,24 @@ requires-dist = [ { name = "pyqt6", marker = "extra == 'qt'", specifier = "==6.8.0" }, { name = "pyqt6", marker = "extra == 'qt66'", specifier = "==6.6.1" }, { name = "pyqt6", marker = "extra == 'qt67'", specifier = "==6.7.1" }, + { name = "pyqt6", marker = "extra == 'qt69'", specifier = "==6.9.1" }, { name = "pyqt6-qt6", marker = "extra == 'qt'", specifier = "==6.8.1" }, { name = "pyqt6-qt6", marker = "extra == 'qt66'", specifier = "==6.6.2" }, { name = "pyqt6-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" }, + { name = "pyqt6-qt6", marker = "extra == 'qt69'", specifier = "==6.9.1" }, { name = "pyqt6-sip", marker = "extra == 'qt'", specifier = "==13.10.2" }, { name = "pyqt6-sip", marker = "extra == 'qt66'", specifier = "==13.6.0" }, { name = "pyqt6-sip", marker = "extra == 'qt67'", specifier = "==13.10.2" }, + { name = "pyqt6-sip", marker = "extra == 'qt69'", specifier = "==13.10.2" }, { name = "pyqt6-webengine", specifier = ">=6.2" }, { name = "pyqt6-webengine", marker = "extra == 'qt'", specifier = "==6.8.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt66'", specifier = "==6.6.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" }, + { name = "pyqt6-webengine", marker = "extra == 'qt69'", specifier = "==6.9.0" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt'", specifier = "==6.8.1" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt66'", specifier = "==6.6.2" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" }, + { name = "pyqt6-webengine-qt6", marker = "extra == 'qt69'", specifier = "==6.9.1" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "send2trash" }, @@ -244,14 +259,14 @@ requires-dist = [ { name = "types-waitress" }, { name = "waitress", specifier = ">=2.0.0" }, ] -provides-extras = ["audio", "qt", "qt66", "qt67"] +provides-extras = ["audio", "qt", "qt66", "qt67", "qt69"] [[package]] name = "astroid" version = "3.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" } wheels = [ @@ -294,14 +309,14 @@ name = "black" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } wheels = [ @@ -428,7 +443,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ @@ -445,7 +460,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (python_full_version < '3.10' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -502,7 +517,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -515,9 +530,9 @@ version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, @@ -549,7 +564,7 @@ dependencies = [ { name = "packaging" }, { name = "pathspec" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "trove-classifiers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } @@ -580,7 +595,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -658,7 +673,7 @@ name = "markdown" version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/7c/0738e5ff0adccd0b4e02c66d0446c03a3c557e02bb49b7c263d7ab56c57d/markdown-3.8.1.tar.gz", hash = "sha256:a2e2f01cead4828ee74ecca9623045f62216aef2212a7685d6eb9163f590b8c1", size = 361280, upload-time = "2025-06-18T14:50:49.618Z" } wheels = [ @@ -758,7 +773,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } @@ -932,7 +947,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "wrapt", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "wrapt", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/9a/4e949d0a281c5dd45c8d5b02b03fe32044936234675e967de49317a1daee/pip_system_certs-4.0.tar.gz", hash = "sha256:db8e6a31388d9795ec9139957df1a89fa5274fb66164456fd091a5d3e94c350c", size = 5622, upload-time = "2022-11-04T11:01:12.537Z" } wheels = [ @@ -949,7 +964,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "pip", marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "pip", marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/0c/a338ae5d49192861cf54da4d5c2af0efe47edbaa0827995b284005366ca5/pip_system_certs-5.2.tar.gz", hash = "sha256:80b776b5cf17191bf99d313699b7fce2fdb84eb7bbb225fd134109a82706406f", size = 5408, upload-time = "2025-06-17T23:33:15.322Z" } wheels = [ @@ -1030,14 +1045,14 @@ version = "3.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "dill" }, { name = "isort" }, { name = "mccabe" }, { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "tomlkit" }, - { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" } wheels = [ @@ -1055,8 +1070,8 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6-qt6", version = "6.6.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-sip", version = "13.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6-qt6", version = "6.6.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/2b/6fe0409501798abc780a70cab48c39599742ab5a8168e682107eaab78fca/PyQt6-6.6.1.tar.gz", hash = "sha256:9f158aa29d205142c56f0f35d07784b8df0be28378d20a97bcda8bd64ffd0379", size = 1043203, upload-time = "2023-12-04T10:37:27.406Z" } wheels = [ @@ -1076,8 +1091,8 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6-qt6", version = "6.7.3", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6-qt6", version = "6.7.3", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/f9/b0c2ba758b14a7219e076138ea1e738c068bf388e64eee68f3df4fc96f5a/PyQt6-6.7.1.tar.gz", hash = "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9", size = 1051212, upload-time = "2024-07-19T08:49:58.247Z" } wheels = [ @@ -1100,8 +1115,8 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/0a/accbebed526158ab2aedd5c84d238159754bd99f481082b3fe7f374c6a3b/PyQt6-6.8.0.tar.gz", hash = "sha256:6d8628de4c2a050f0b74462e4c9cb97f839bf6ffabbca91711722ffb281570d9", size = 1061357, upload-time = "2024-12-12T15:30:42.021Z" } wheels = [ @@ -1113,6 +1128,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/a0/f564279285ed92b4fe38ed7b2a8fcddab414512391088b6a0f67a1107f24/PyQt6-6.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:48bace7b87676bba5e6114482f3a20ca20be90c7f261b5d340464313f5f2bf5e", size = 5409032, upload-time = "2024-12-12T15:30:38.859Z" }, ] +[[package]] +name = "pyqt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c4/fc2a69cf3df09b213185ef5a677c3940cd20e7855d29e40061a685b9c6ee/pyqt6-6.9.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:33c23d28f6608747ecc8bfd04c8795f61631af9db4fb1e6c2a7523ec4cc916d9", size = 59770566, upload-time = "2025-06-06T08:48:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d5/78/92f3c46440a83ebe22ae614bd6792e7b052bcb58ff128f677f5662015184/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:37884df27f774e2e1c0c96fa41e817a222329b80ffc6241725b0dc8c110acb35", size = 37804959, upload-time = "2025-06-06T08:48:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5e/e77fa2761d809cd08d724f44af01a4b6ceb0ff9648e43173187b0e4fac4e/pyqt6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:055870b703c1a49ca621f8a89e2ec4d848e6c739d39367eb9687af3b056d9aa3", size = 40414608, upload-time = "2025-06-06T08:49:00.26Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/69cf80456b6a985e06dd24ed0c2d3451e43567bf2807a5f3a86ef7a74a2e/pyqt6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:15b95bd273bb6288b070ed7a9503d5ff377aa4882dd6d175f07cad28cdb21da0", size = 25717996, upload-time = "2025-06-06T08:49:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/52/b3/0839d8fd18b86362a4de384740f2f6b6885b5d06fda7720f8a335425e316/pyqt6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:08792c72d130a02e3248a120f0b9bbb4bf4319095f92865bc5b365b00518f53d", size = 25212132, upload-time = "2025-06-06T08:49:27.41Z" }, +] + [[package]] name = "pyqt6-qt6" version = "6.6.2" @@ -1170,6 +1208,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/0d/b86b64f0ac5e08b1bd18edac9e949897a4626196725e9a86af4e9f628c80/PyQt6_Qt6-6.8.1-py3-none-win_arm64.whl", hash = "sha256:a8bc2ed4ee5e7c6ff4dd1c7db0b27705d151fee5dc232bbd1bf17618f937f515", size = 47836492, upload-time = "2024-12-06T13:52:29.511Z" }, ] +[[package]] +name = "pyqt6-qt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/40/04f652e714f85ba6b0c24f4ead860f2c5769f9e64737f415524d792d5914/pyqt6_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:3854c7f83ee4e8c2d91e23ab88b77f90e2ca7ace34fe72f634a446959f2b4d4a", size = 66236777, upload-time = "2025-06-03T14:53:17.684Z" }, + { url = "https://files.pythonhosted.org/packages/57/31/e4fa40568a59953ce5cf9a5adfbd1be4a806dafd94e39072d3cc0bed5468/pyqt6_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:123e4aeb037c099bb4696a3ea8edcb1d9d62cedd0b2b950556b26024c97f3293", size = 60551574, upload-time = "2025-06-03T14:53:48.42Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8d/7c8073cbbefe9c103ec8add70f29ffee1db95a3755b429b9f47cd6afa41b/pyqt6_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cc5bd193ebd2d1a3ec66e1eee65bf532d762c239459bce1ecebf56177243e89b", size = 82000130, upload-time = "2025-06-03T14:54:26.585Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/a4ab932028b0c15c0501cb52eb1e7f24f4ce2e4c78d46c7cce58a375a88c/pyqt6_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b065af7243d1d450a49470a8185301196a18b1d41085d3ef476eb55bbb225083", size = 80463127, upload-time = "2025-06-03T14:55:03.272Z" }, + { url = "https://files.pythonhosted.org/packages/e7/85/552710819019a96d39d924071324a474aec54b31c410d7de8ebb398adcc1/pyqt6_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:f9e54c424bc921ecb76792a75d123e4ecfc26b00b0c57dae526f41f1d57951d3", size = 73778423, upload-time = "2025-06-03T14:55:39.756Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/70f6b18a4913f2326dcf7acb15c12cc0b91cb3932c2ba3b5728811f22acd/pyqt6_qt6-6.9.1-py3-none-win_arm64.whl", hash = "sha256:432caaedf5570bc8a9b7c75bc6af6a26bf88589536472eca73417ac019f59d41", size = 49617924, upload-time = "2025-06-03T14:57:13.038Z" }, +] + [[package]] name = "pyqt6-sip" version = "13.6.0" @@ -1248,9 +1305,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-sip", version = "13.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-webengine-qt6", version = "6.6.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine-qt6", version = "6.6.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/9a/69db3a2ab1ba43f762144a66f0375540e195e107a1049d7263ab48ebc9cc/PyQt6_WebEngine-6.6.0.tar.gz", hash = "sha256:d50b984c3f85e409e692b156132721522d4e8cf9b6c25e0cf927eea2dfb39487", size = 31817, upload-time = "2023-10-30T10:57:13.211Z" } wheels = [ @@ -1270,9 +1327,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6", version = "6.7.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, - { name = "pyqt6-webengine-qt6", version = "6.7.3", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6", version = "6.7.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine-qt6", version = "6.7.3", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/88/230ec599944edf941f4cca8d1439e3a9c8c546715434eee05dce7ff032ed/PyQt6_WebEngine-6.7.0.tar.gz", hash = "sha256:68edc7adb6d9e275f5de956881e79cca0d71fad439abeaa10d823bff5ac55001", size = 32593, upload-time = "2024-04-26T08:37:08.355Z" } wheels = [ @@ -1293,9 +1350,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-webengine-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pyqt6-webengine-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/c8/cadaa950eaf97f29e48c435e274ea5a81c051e745a3e2f5d9d994b7a6cda/PyQt6_WebEngine-6.8.0.tar.gz", hash = "sha256:64045ea622b6a41882c2b18f55ae9714b8660acff06a54e910eb72822c2f3ff2", size = 34203, upload-time = "2024-12-12T15:34:35.573Z" } wheels = [ @@ -1306,6 +1363,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/74/9b20e505737ceefe2ffb47355633c84b7d5d7d592f32165425b3e0ce7dd9/PyQt6_WebEngine-6.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:d7366809d681bcc096fa565f2a81d0ab040f7da5bb4f12f78e834a2b173c87d1", size = 234566, upload-time = "2024-12-12T15:34:33.59Z" }, ] +[[package]] +name = "pyqt6-webengine" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/1a/9971af004a7e859347702f816fb71ecd67c3e32b2f0ae8daf1c1ded99f62/pyqt6_webengine-6.9.0.tar.gz", hash = "sha256:6ae537e3bbda06b8e06535e4852297e0bc3b00543c47929541fcc9b11981aa25", size = 34616, upload-time = "2025-04-08T08:57:35.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/e1/964ee1c464a0e1f07f8be54ce9316dc76e431d1bc99c9e5c1437bf548d92/PyQt6_WebEngine-6.9.0-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:3ea5bdd48d109f35bf726f59d85b250e430ddd50175fe79a386b7f14d3e34d2d", size = 438004, upload-time = "2025-04-08T08:57:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/8674bb27e2497fdad7ae5bc000831b42dbfb546aacd11ae7a8cca4493190/PyQt6_WebEngine-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c15012245036604c82abcd865e0b808e75bcfd0b477454298b7a70d9e6c4958b", size = 299003, upload-time = "2025-04-08T08:57:31.334Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9c/b6a1ce7026260d518a103c467a4795c719dd1e4d7f8dc00416d3ec292d3a/PyQt6_WebEngine-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e4404899290f86d4652a07471262a2f41397c64ecb091229b5bbbd8b82af35ce", size = 297424, upload-time = "2025-04-08T08:57:32.664Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e8/444487c86472c522d6ab28686b9f3c4d6fe2febde81b40561d42c11b5cd7/PyQt6_WebEngine-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:541cf838facadfc38243baaecfeeaf07c8eff030cf27341c85c245d00e571489", size = 237847, upload-time = "2025-04-08T08:57:34.174Z" }, +] + [[package]] name = "pyqt6-webengine-qt6" version = "6.6.2" @@ -1334,7 +1414,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6-webenginesubwheel-qt6", marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6-webenginesubwheel-qt6", marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/06/35/570d072bec7c114b5d155d990e2b8339223e230e9276bdf806a20f71e50d/PyQt6_WebEngine_Qt6-6.7.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:68812b2a5d0d417ce32dc4d11a304e7838e02c51013712e7533faf03448672d9", size = 26214456, upload-time = "2024-09-29T16:27:20.257Z" }, @@ -1363,6 +1443,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/b5/a641ebe3e5113bee23d911c58fdd2e65061a6e3786a26b068468b988e5d2/PyQt6_WebEngine_Qt6-6.8.1-py3-none-win_amd64.whl", hash = "sha256:0ced2a10433da2571cfa29ed882698e0e164184d54068d17ba73799c45af5f0f", size = 95657750, upload-time = "2024-12-06T13:47:43.048Z" }, ] +[[package]] +name = "pyqt6-webengine-qt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/68/8eccda2a78d100a95db2131fac82b4ad842bdf0255019dcf86d5db0db3fa/pyqt6_webengine_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:38a461e21df3e09829ce18cd0ecd052ecff0f9a4001ae000ea6ddac10fae6b0f", size = 120033076, upload-time = "2025-06-03T14:59:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8a/a2cf83dd9cb8e944507e7b4070ac537a30d6caef7e498c87f1612507431d/pyqt6_webengine_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9351ac6cccfdbf414a2514b58d49765d3f48fb3b690ceeaa8ca804340eb460b4", size = 108530410, upload-time = "2025-06-03T15:00:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/99b189e30641469dda6eb09a5f3cf0c5e12e2b39967e1df5d156f8499c4f/pyqt6_webengine_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:37f9d33e3f68681687a7d28b80058c6473813c6c2f9706a44dc7bc07486b9e9a", size = 110750602, upload-time = "2025-06-03T15:01:32.055Z" }, + { url = "https://files.pythonhosted.org/packages/68/fc/958c7f5e0f9cfe7903a90ed41b435b17de6ca7cf5d1f73e97ad07fe8107c/pyqt6_webengine_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:e8440e65b79df167b49bd3c26b8bf7179306df010e8c319a7ad6b8bc2a8ae8d4", size = 106652228, upload-time = "2025-06-03T15:02:18.985Z" }, + { url = "https://files.pythonhosted.org/packages/4c/82/4a3e2233ca3aa9c3ec77390e570fbcffcc511bc8513461daa1d38cda652a/pyqt6_webengine_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:c7f460226c054a52b7868d3befeed4fe2af03f4f80d2e53ab49fcb238bac2bc7", size = 110244976, upload-time = "2025-06-03T15:03:09.743Z" }, +] + [[package]] name = "pyqt6-webenginesubwheel-qt6" version = "6.7.3" @@ -1389,13 +1487,13 @@ name = "pytest" version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ @@ -1483,7 +1581,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ @@ -1678,24 +1776,24 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "babel", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "docutils", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "imagesize", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "jinja2", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "requests", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "snowballstemmer", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "babel", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (python_full_version >= '3.10' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "docutils", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "imagesize", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "jinja2", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "requests", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ @@ -1710,23 +1808,23 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "babel", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "colorama", marker = "(python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "docutils", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "imagesize", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "jinja2", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "packaging", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "pygments", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "requests", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "snowballstemmer", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "babel", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "colorama", marker = "(python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (python_full_version != '3.10.*' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "docutils", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "imagesize", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "jinja2", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "packaging", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pygments", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "requests", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -1742,23 +1840,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "babel", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "colorama", marker = "(python_full_version >= '3.11' and sys_platform == 'win32') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "docutils", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "imagesize", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "jinja2", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "requests", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "babel", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "colorama", marker = "(python_full_version >= '3.11' and sys_platform == 'win32') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (python_full_version < '3.11' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform != 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "docutils", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "imagesize", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "jinja2", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "requests", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ @@ -1773,10 +1871,10 @@ dependencies = [ { name = "astroid" }, { name = "jinja2" }, { name = "pyyaml" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "stdlib-list", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "stdlib-list", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7f/a8/22b379a2a75ccb881217d3d4ae56d7d35f2d1bb4c8c0c51d0253676746a1/sphinx_autoapi-3.6.0.tar.gz", hash = "sha256:c685f274e41d0842ae7e199460c322c4bd7fec816ccc2da8d806094b4f64af06", size = 55417, upload-time = "2025-02-18T01:50:55.241Z" } wheels = [ @@ -1789,9 +1887,9 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "sphinxcontrib-jquery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } @@ -1831,9 +1929,9 @@ name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ From a4c95f5fbdb22f165c41d31a16c365b3edbeff97 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Sat, 21 Jun 2025 01:59:35 +0800 Subject: [PATCH 011/197] include decay in ComputeMemoryStateResponse (#4102) * include decay in ComputeMemoryStateResponse * Add decay attribute to ComputedMemoryState and update Collection methods * Refactor decay calculation into a helper function for improved readability and maintainability in memory state management * format & clippy --- proto/anki/scheduler.proto | 1 + pylib/anki/collection.py | 7 +++++- rslib/src/scheduler/fsrs/memory_state.rs | 28 +++++++++++++++--------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index ea483d3db..1b7d44a83 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -450,6 +450,7 @@ message EvaluateParamsResponse { message ComputeMemoryStateResponse { optional cards.FsrsMemoryState state = 1; float desired_retention = 2; + float decay = 3; } message FuzzDeltaRequest { diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 17ee08e2f..6cf38174c 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -122,6 +122,7 @@ class ComputedMemoryState: desired_retention: float stability: float | None = None difficulty: float | None = None + decay: float | None = None @dataclass @@ -1189,9 +1190,13 @@ class Collection(DeprecatedNamesMixin): desired_retention=resp.desired_retention, stability=resp.state.stability, difficulty=resp.state.difficulty, + decay=resp.decay, ) else: - return ComputedMemoryState(desired_retention=resp.desired_retention) + return ComputedMemoryState( + desired_retention=resp.desired_retention, + decay=resp.decay, + ) def fuzz_delta(self, card_id: CardId, interval: int) -> int: "The delta days of fuzz applied if reviewing the card in v3." diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 787fa212d..f5af97674 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -30,6 +30,18 @@ pub struct ComputeMemoryProgress { pub total_cards: u32, } +/// Helper function to determine the appropriate decay value based on FSRS +/// parameters +fn get_decay_from_params(params: &[f32]) -> f32 { + if params.is_empty() { + FSRS6_DEFAULT_DECAY // default decay for FSRS-6 + } else if params.len() < 21 { + FSRS5_DEFAULT_DECAY // default decay for FSRS-4.5 and FSRS-5 + } else { + params[20] + } +} + #[derive(Debug)] pub(crate) struct UpdateMemoryStateRequest { pub params: Params, @@ -77,15 +89,7 @@ impl Collection { .then(|| Rescheduler::new(self)) .transpose()?; let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; - let decay = req.as_ref().map(|w| { - if w.params.is_empty() { - FSRS6_DEFAULT_DECAY // default decay for FSRS-6 - } else if w.params.len() < 21 { - FSRS5_DEFAULT_DECAY // default decay for FSRS-4.5 and FSRS-5 - } else { - w.params[20] - } - }); + let decay = req.as_ref().map(|w| get_decay_from_params(&w.params)); let historical_retention = req.as_ref().map(|w| w.historical_retention); let items = fsrs_items_for_memory_states( &fsrs, @@ -190,7 +194,9 @@ impl Collection { .or_not_found(conf_id)?; let desired_retention = config.inner.desired_retention; let historical_retention = config.inner.historical_retention; - let fsrs = FSRS::new(Some(config.fsrs_params()))?; + let params = config.fsrs_params(); + let decay = get_decay_from_params(params); + let fsrs = FSRS::new(Some(params))?; let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; let item = fsrs_item_for_memory_state( &fsrs, @@ -204,6 +210,7 @@ impl Collection { Ok(ComputeMemoryStateResponse { state: card.memory_state.map(Into::into), desired_retention, + decay, }) } else { card.memory_state = None; @@ -211,6 +218,7 @@ impl Collection { Ok(ComputeMemoryStateResponse { state: None, desired_retention, + decay, }) } } From cc395f7c448ee0ba268077a1e21fb8e0b80e99f4 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 21 Jun 2025 20:15:19 +0800 Subject: [PATCH 012/197] Upgrade to nom 8.0.0 (#4105) * bump nom to 8.0.0 * update cloze.rs * update template.rs * update imageocclusion.rs * update search/parser.rs * update card_rendering/parser.rs * replace use of fold_many0 with many0 in nom 8, `many0` doesn't accumulate when used within `recognize` --- Cargo.lock | 13 ++++- Cargo.toml | 2 +- cargo/licenses.json | 9 +++ rslib/src/card_rendering/parser.rs | 61 ++++++++++++--------- rslib/src/cloze.rs | 10 ++-- rslib/src/image_occlusion/imageocclusion.rs | 6 +- rslib/src/search/parser.rs | 32 ++++++----- rslib/src/template.rs | 7 ++- 8 files changed, 90 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66173027b..03f9e63c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,7 +117,7 @@ dependencies = [ "id_tree", "inflections", "itertools 0.14.0", - "nom", + "nom 8.0.0", "num_cpus", "num_enum", "once_cell", @@ -4117,6 +4117,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normpath" version = "1.3.0" @@ -6258,7 +6267,7 @@ dependencies = [ "bytesize", "lazy_static", "libc", - "nom", + "nom 7.1.3", "time", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index 980956b05..61cca8649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ junction = "1.2.0" libc = "0.2" libc-stdhandle = "0.1" maplit = "1.0.2" -nom = "7.1.3" +nom = "8.0.0" num-format = "0.4.4" num_cpus = "1.17.0" num_enum = "0.7.3" diff --git a/cargo/licenses.json b/cargo/licenses.json index c16b20aa7..f2695ac76 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -2645,6 +2645,15 @@ "license_file": null, "description": "A byte-oriented, zero-copy, parser combinators library" }, + { + "name": "nom", + "version": "8.0.0", + "authors": "contact@geoffroycouprie.com", + "repository": "https://github.com/rust-bakery/nom", + "license": "MIT", + "license_file": null, + "description": "A byte-oriented, zero-copy, parser combinators library" + }, { "name": "ntapi", "version": "0.4.1", diff --git a/rslib/src/card_rendering/parser.rs b/rslib/src/card_rendering/parser.rs index 6f1cc662e..b124c069d 100644 --- a/rslib/src/card_rendering/parser.rs +++ b/rslib/src/card_rendering/parser.rs @@ -14,14 +14,14 @@ use nom::combinator::recognize; use nom::combinator::rest; use nom::combinator::success; use nom::combinator::value; -use nom::multi::fold_many0; use nom::multi::many0; use nom::sequence::delimited; use nom::sequence::pair; use nom::sequence::preceded; use nom::sequence::separated_pair; use nom::sequence::terminated; -use nom::sequence::tuple; +use nom::Input; +use nom::Parser; use super::CardNodes; use super::Directive; @@ -86,9 +86,12 @@ impl<'a> Directive<'a> { } /// Consume 0 or more of anything in " \t\r\n" after `parser`. -fn trailing_whitespace0<'parser, 's, P, O>(parser: P) -> impl FnMut(&'s str) -> IResult<'s, O> +fn trailing_whitespace0(parser: P) -> impl Parser where - P: FnMut(&'s str) -> IResult<'s, O> + 'parser, + I: Input, + ::Item: nom::AsChar, + E: nom::error::ParseError, + P: Parser, { terminated(parser, multispace0) } @@ -97,11 +100,11 @@ where fn is_not0<'parser, 'arr: 'parser, 's: 'parser>( arr: &'arr str, ) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser { - alt((is_not(arr), success(""))) + move |s| alt((is_not(arr), success(""))).parse(s) } fn node(s: &str) -> IResult { - alt((sound_node, tag_node, text_node))(s) + alt((sound_node, tag_node, text_node)).parse(s) } /// A sound tag `[sound:resource]`, where `resource` is pointing to a sound or @@ -110,11 +113,11 @@ fn sound_node(s: &str) -> IResult { map( delimited(tag("[sound:"), is_not("]"), tag("]")), Node::SoundOrVideo, - )(s) + ) + .parse(s) } fn take_till_potential_tag_start(s: &str) -> IResult<&str> { - use nom::InputTake; // first char could be '[', but wasn't part of a node, so skip (eof ends parse) let (after, offset) = anychar(s).map(|(s, c)| (s, c.len_utf8()))?; Ok(match after.find('[') { @@ -127,7 +130,7 @@ fn take_till_potential_tag_start(s: &str) -> IResult<&str> { fn tag_node(s: &str) -> IResult { /// Match the start of an opening tag and return its name. fn name(s: &str) -> IResult<&str> { - preceded(tag("[anki:"), is_not("] \t\r\n"))(s) + preceded(tag("[anki:"), is_not("] \t\r\n")).parse(s) } /// Return a parser to match an opening `name` tag and return its options. @@ -138,31 +141,35 @@ fn tag_node(s: &str) -> IResult { /// empty. fn options(s: &str) -> IResult> { fn key(s: &str) -> IResult<&str> { - is_not("] \t\r\n=")(s) + is_not("] \t\r\n=").parse(s) } fn val(s: &str) -> IResult<&str> { alt(( delimited(tag("\""), is_not0("\""), tag("\"")), is_not0("] \t\r\n\""), - ))(s) + )) + .parse(s) } - many0(trailing_whitespace0(separated_pair(key, tag("="), val)))(s) + many0(trailing_whitespace0(separated_pair(key, tag("="), val))).parse(s) } - delimited( - pair(tag("[anki:"), trailing_whitespace0(tag(name))), - options, - tag("]"), - ) + move |s| { + delimited( + pair(tag("[anki:"), trailing_whitespace0(tag(name))), + options, + tag("]"), + ) + .parse(s) + } } /// Return a parser to match a closing `name` tag. fn closing_parser<'parser, 'name: 'parser, 's: 'parser>( name: &'name str, ) -> impl FnMut(&'s str) -> IResult<'s, ()> + 'parser { - value((), tuple((tag("[/anki:"), tag(name), tag("]")))) + move |s| value((), (tag("[/anki:"), tag(name), tag("]"))).parse(s) } /// Return a parser to match and return anything until a closing `name` tag @@ -170,12 +177,13 @@ fn tag_node(s: &str) -> IResult { fn content_parser<'parser, 'name: 'parser, 's: 'parser>( name: &'name str, ) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser { - recognize(fold_many0( - pair(not(closing_parser(name)), take_till_potential_tag_start), - // we don't need to accumulate anything - || (), - |_, _| (), - )) + move |s| { + recognize(many0(pair( + not(closing_parser(name)), + take_till_potential_tag_start, + ))) + .parse(s) + } } let (_, tag_name) = name(s)?; @@ -185,11 +193,12 @@ fn tag_node(s: &str) -> IResult { closing_parser(tag_name), ), |(options, content)| Node::Directive(Directive::new(tag_name, options, content)), - )(s) + ) + .parse(s) } fn text_node(s: &str) -> IResult { - map(take_till_potential_tag_start, Node::Text)(s) + map(take_till_potential_tag_start, Node::Text).parse(s) } #[cfg(test)] diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index f57d07ab0..208a2f4ed 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -15,6 +15,7 @@ use nom::bytes::complete::tag; use nom::bytes::complete::take_while; use nom::combinator::map; use nom::IResult; +use nom::Parser; use regex::Captures; use regex::Regex; @@ -72,7 +73,7 @@ fn tokenize(mut text: &str) -> impl Iterator { } fn close_cloze(text: &str) -> IResult<&str, Token> { - map(tag("}}"), |_| Token::CloseCloze)(text) + map(tag("}}"), |_| Token::CloseCloze).parse(text) } /// Match a run of text until an open/close marker is encountered. @@ -87,7 +88,7 @@ fn tokenize(mut text: &str) -> impl Iterator { // start with the no-match case let mut index = text.len(); for (idx, _) in text.char_indices() { - if other_token(&text[idx..]).is_ok() { + if other_token.parse(&text[idx..]).is_ok() { index = idx; break; } @@ -99,8 +100,9 @@ fn tokenize(mut text: &str) -> impl Iterator { if text.is_empty() { None } else { - let (remaining_text, token) = - alt((open_cloze, close_cloze, normal_text))(text).unwrap(); + let (remaining_text, token) = alt((open_cloze, close_cloze, normal_text)) + .parse(text) + .unwrap(); text = remaining_text; Some(token) } diff --git a/rslib/src/image_occlusion/imageocclusion.rs b/rslib/src/image_occlusion/imageocclusion.rs index 2ba83374f..e2eea9a39 100644 --- a/rslib/src/image_occlusion/imageocclusion.rs +++ b/rslib/src/image_occlusion/imageocclusion.rs @@ -13,6 +13,7 @@ use nom::character::complete::char; use nom::error::ErrorKind; use nom::sequence::preceded; use nom::sequence::separated_pair; +use nom::Parser; fn unescape(text: &str) -> String { text.replace("\\:", ":") @@ -22,11 +23,12 @@ pub fn parse_image_cloze(text: &str) -> Option { if let Some((shape, _)) = text.split_once(':') { let mut properties = vec![]; let mut remaining = &text[shape.len()..]; - while let Ok((rem, (name, value))) = separated_pair::<_, _, _, _, (_, ErrorKind), _, _, _>( + while let Ok((rem, (name, value))) = separated_pair::<_, _, _, (_, ErrorKind), _, _, _>( preceded(tag(":"), is_not("=")), tag("="), escaped(is_not("\\:"), '\\', char(':')), - )(remaining) + ) + .parse(remaining) { remaining = rem; let value = unescape(value); diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 93df4ea08..041ec4948 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -19,6 +19,7 @@ use nom::error::ErrorKind as NomErrorKind; use nom::multi::many0; use nom::sequence::preceded; use nom::sequence::separated_pair; +use nom::Parser; use regex::Captures; use regex::Regex; @@ -202,18 +203,19 @@ fn group_inner(input: &str) -> IResult> { } fn whitespace0(s: &str) -> IResult> { - many0(one_of(" \u{3000}"))(s) + many0(one_of(" \u{3000}")).parse(s) } /// Optional leading space, then a (negated) group or text fn node(s: &str) -> IResult { - preceded(whitespace0, alt((negated_node, group, text)))(s) + preceded(whitespace0, alt((negated_node, group, text))).parse(s) } fn negated_node(s: &str) -> IResult { map(preceded(char('-'), alt((group, text))), |node| { Node::Not(Box::new(node)) - })(s) + }) + .parse(s) } /// One or more nodes surrounded by brackets, eg (one OR two) @@ -233,7 +235,7 @@ fn group(s: &str) -> IResult { /// Either quoted or unquoted text fn text(s: &str) -> IResult { - alt((quoted_term, partially_quoted_term, unquoted_term))(s) + alt((quoted_term, partially_quoted_term, unquoted_term)).parse(s) } /// Quoted text, including the outer double quotes. @@ -248,7 +250,8 @@ fn partially_quoted_term(s: &str) -> IResult { escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")), char(':'), quoted_term_str, - )(s)?; + ) + .parse(s)?; Ok(( remaining, Node::Search(search_node_for_text_with_argument(key, val)?), @@ -296,7 +299,7 @@ fn unquoted_term(s: &str) -> IResult { fn quoted_term_str(s: &str) -> IResult<&str> { let (opened, _) = char('"')(s)?; if let Ok((tail, inner)) = - escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened) + escaped::<_, ParseError, _, _>(is_not(r#""\"#), '\\', anychar).parse(opened) { if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) { Ok((remaining, inner)) @@ -321,7 +324,8 @@ fn search_node_for_text(s: &str) -> ParseResult { // leading : is only possible error for well-formed input let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| { !t.is_empty() - })(s) + }) + .parse(s) .map_err(|_: nom::Err| parse_failure(s, FailKind::MissingKey))?; if tail.is_empty() { Ok(SearchNode::UnqualifiedText(unescape(head)?)) @@ -407,7 +411,7 @@ fn parse_resched(s: &str) -> ParseResult { /// eg prop:ivl>3, prop:ease!=2.5 fn parse_prop(prop_clause: &str) -> ParseResult { - let (tail, prop) = alt::<_, _, ParseError, _>(( + let (tail, prop) = alt(( tag("ivl"), tag("due"), tag("reps"), @@ -421,8 +425,9 @@ fn parse_prop(prop_clause: &str) -> ParseResult { tag("r"), recognize(preceded(tag("cdn:"), alphanumeric1)), recognize(preceded(tag("cds:"), alphanumeric1)), - ))(prop_clause) - .map_err(|_| { + )) + .parse(prop_clause) + .map_err(|_: nom::Err| { parse_failure( prop_clause, FailKind::InvalidPropProperty { @@ -431,15 +436,16 @@ fn parse_prop(prop_clause: &str) -> ParseResult { ) })?; - let (num, operator) = alt::<_, _, ParseError, _>(( + let (num, operator) = alt(( tag("<="), tag(">="), tag("!="), tag("="), tag("<"), tag(">"), - ))(tail) - .map_err(|_| { + )) + .parse(tail) + .map_err(|_: nom::Err| { parse_failure( prop_clause, FailKind::InvalidPropOperator { diff --git a/rslib/src/template.rs b/rslib/src/template.rs index d09ade580..e3a900a2b 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -13,6 +13,7 @@ use nom::bytes::complete::tag; use nom::bytes::complete::take_until; use nom::combinator::map; use nom::sequence::delimited; +use nom::Parser; use regex::Regex; use crate::cloze::cloze_number_in_fields; @@ -67,7 +68,8 @@ impl TemplateMode { tag(self.end_tag()), ), |out| classify_handle(out), - )(s) + ) + .parse(s) } /// Return the next handlebar, comment or text token. @@ -127,7 +129,8 @@ fn comment_token(s: &str) -> nom::IResult<&str, Token> { tag(COMMENT_END), ), Token::Comment, - )(s) + ) + .parse(s) } fn tokens(mut template: &str) -> impl Iterator>> { From 88538d8badeb11e76d534077caa05f93d5d07267 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Sat, 21 Jun 2025 20:15:30 +0800 Subject: [PATCH 013/197] Fix/set due date on intraday learning card (#4101) - Introduced `next_day_start` parameter to `set_due_date` for improved due date handling. - Updated logic to account for Unix epoch timestamps when calculating due dates. --- rslib/src/scheduler/reviews.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/rslib/src/scheduler/reviews.rs b/rslib/src/scheduler/reviews.rs index 7e61447da..06390e57d 100644 --- a/rslib/src/scheduler/reviews.rs +++ b/rslib/src/scheduler/reviews.rs @@ -17,6 +17,7 @@ use crate::collection::Collection; use crate::config::StringKey; use crate::error::Result; use crate::prelude::*; +use crate::scheduler::timing::is_unix_epoch_timestamp; impl Card { /// Make card due in `days_from_today`. @@ -27,6 +28,7 @@ impl Card { fn set_due_date( &mut self, today: u32, + next_day_start: i64, days_from_today: u32, ease_factor: f32, force_reset: bool, @@ -34,8 +36,15 @@ impl Card { let new_due = (today + days_from_today) as i32; let fsrs_enabled = self.memory_state.is_some(); let new_interval = if fsrs_enabled { - self.interval - .saturating_add_signed(new_due - self.original_or_current_due()) + let due = self.original_or_current_due(); + let due_diff = if is_unix_epoch_timestamp(due) { + let offset = (due as i64 - next_day_start) / 86_400; + let due = (today as i64 + offset) as i32; + new_due - due + } else { + new_due - due + }; + self.interval.saturating_add_signed(due_diff) } else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) { days_from_today.max(1) } else { @@ -114,6 +123,7 @@ impl Collection { let spec = parse_due_date_str(days)?; let usn = self.usn()?; let today = self.timing_today()?.days_elapsed; + let next_day_start = self.timing_today()?.next_day_at.0; let mut rng = rand::rng(); let distribution = Uniform::new_inclusive(spec.min, spec.max).unwrap(); let mut decks_initial_ease: HashMap = HashMap::new(); @@ -137,7 +147,13 @@ impl Collection { }; let original = card.clone(); let days_from_today = distribution.sample(&mut rng); - card.set_due_date(today, days_from_today, ease_factor, spec.force_reset); + card.set_due_date( + today, + next_day_start, + days_from_today, + ease_factor, + spec.force_reset, + ); col.log_manually_scheduled_review(&card, original.interval, usn)?; col.update_card_inner(&mut card, original, usn)?; } @@ -228,26 +244,26 @@ mod test { let mut c = Card::new(NoteId(0), 0, DeckId(0), 0); // setting the due date of a new card will convert it - c.set_due_date(5, 2, 1.8, false); + c.set_due_date(5, 0, 2, 1.8, false); assert_eq!(c.ctype, CardType::Review); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // reschedule it again the next day, shifting it from day 7 to day 9 - c.set_due_date(6, 3, 2.5, false); + c.set_due_date(6, 0, 3, 2.5, false); assert_eq!(c.due, 9); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // interval doesn't change // we can bring cards forward too - return it to its original due date - c.set_due_date(6, 1, 2.4, false); + c.set_due_date(6, 0, 1, 2.4, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 1800); // interval doesn't change // we can force the interval to be reset instead of shifted - c.set_due_date(6, 3, 2.3, true); + c.set_due_date(6, 0, 3, 2.3, true); assert_eq!(c.due, 9); assert_eq!(c.interval, 3); assert_eq!(c.ease_factor, 1800); // interval doesn't change @@ -259,7 +275,7 @@ mod test { c.original_deck_id = DeckId(1); c.due = -10000; c.queue = CardQueue::New; - c.set_due_date(6, 1, 2.2, false); + c.set_due_date(6, 0, 1, 2.2, false); assert_eq!(c.due, 7); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 2200); @@ -271,7 +287,7 @@ mod test { c.ctype = CardType::Relearn; c.original_due = c.due; c.due = 12345678; - c.set_due_date(6, 10, 2.1, false); + c.set_due_date(6, 0, 10, 2.1, false); assert_eq!(c.due, 16); assert_eq!(c.interval, 2); assert_eq!(c.ease_factor, 2200); // interval doesn't change From c28306eb94f23c45f6695ac286cd722ff149c0d0 Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Sat, 21 Jun 2025 17:46:54 +0530 Subject: [PATCH 014/197] Save dr and decay in card even if item is None (#4106) * Document the purpose of storing dr and decay in card * Format * Fix type mismatch errors * Update memory_state.rs * Save dr and decay in card even if item is None * Format * Fix mismatched types * Update memory_state.rs --- rslib/src/scheduler/fsrs/memory_state.rs | 135 +++++++++++++---------- 1 file changed, 74 insertions(+), 61 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index f5af97674..d01cde767 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -105,77 +105,92 @@ impl Collection { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); - if let (Some(req), Some(item)) = (&req, item) { - card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + if let Some(req) = &req { + // Store decay and desired retention in the card so that add-ons, card info, + // stats and browser search/sorts don't need to access the deck config. + // Unlike memory states, scheduler doesn't use decay and dr stored in the card. card.desired_retention = desired_retention; card.decay = decay; - // if rescheduling - if let Some(reviews) = &last_revlog_info { - // and we have a last review time for the card - if let Some(last_info) = reviews.get(&card.id) { - if let Some(last_review) = &last_info.last_reviewed_at { - let days_elapsed = - timing.next_day_at.elapsed_days_since(*last_review) as i32; - // and the card's not new - if let Some(state) = &card.memory_state { - // or in (re)learning - if card.ctype == CardType::Review { - let deck = self - .get_deck(card.original_or_current_deck_id())? - .or_not_found(card.original_or_current_deck_id())?; - let deckconfig_id = deck.config_id().unwrap(); - // reschedule it - let original_interval = card.interval; - let interval = fsrs.next_interval( - Some(state.stability), - card.desired_retention.unwrap(), - 0, - ); - card.interval = rescheduler - .as_mut() - .and_then(|r| { - r.find_interval( - interval, - 1, - req.max_interval, - days_elapsed as u32, - deckconfig_id, - get_fuzz_seed(&card, true), - ) - }) - .unwrap_or_else(|| { - with_review_fuzz( - card.get_fuzz_factor(true), - interval, - 1, - req.max_interval, - ) - }); - let due = if card.original_due != 0 { - &mut card.original_due - } else { - &mut card.due - }; - let new_due = (timing.days_elapsed as i32) - days_elapsed - + card.interval as i32; - if let Some(rescheduler) = &mut rescheduler { - rescheduler.update_due_cnt_per_day( - *due, - new_due, - deckconfig_id, + if let Some(item) = item { + card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; + // if rescheduling + if let Some(reviews) = &last_revlog_info { + // and we have a last review time for the card + if let Some(last_info) = reviews.get(&card.id) { + if let Some(last_review) = &last_info.last_reviewed_at { + let days_elapsed = + timing.next_day_at.elapsed_days_since(*last_review) as i32; + // and the card's not new + if let Some(state) = &card.memory_state { + // or in (re)learning + if card.ctype == CardType::Review { + let deck = self + .get_deck(card.original_or_current_deck_id())? + .or_not_found(card.original_or_current_deck_id())?; + let deckconfig_id = deck.config_id().unwrap(); + // reschedule it + let original_interval = card.interval; + let interval = fsrs.next_interval( + Some(state.stability), + desired_retention.unwrap(), + 0, ); + card.interval = rescheduler + .as_mut() + .and_then(|r| { + r.find_interval( + interval, + 1, + req.max_interval, + days_elapsed as u32, + deckconfig_id, + get_fuzz_seed(&card, true), + ) + }) + .unwrap_or_else(|| { + with_review_fuzz( + card.get_fuzz_factor(true), + interval, + 1, + req.max_interval, + ) + }); + let due = if card.original_due != 0 { + &mut card.original_due + } else { + &mut card.due + }; + let new_due = (timing.days_elapsed as i32) + - days_elapsed + + card.interval as i32; + if let Some(rescheduler) = &mut rescheduler { + rescheduler.update_due_cnt_per_day( + *due, + new_due, + deckconfig_id, + ); + } + *due = new_due; + // Add a rescheduled revlog entry + self.log_rescheduled_review( + &card, + original_interval, + usn, + )?; } - *due = new_due; - // Add a rescheduled revlog entry - self.log_rescheduled_review(&card, original_interval, usn)?; } } } } + } else { + // clear memory states if item is None + card.memory_state = None; } } else { + // clear FSRS data if FSRS is disabled card.memory_state = None; card.desired_retention = None; + card.decay = None; } self.update_card_inner(&mut card, original, usn)?; } @@ -213,8 +228,6 @@ impl Collection { decay, }) } else { - card.memory_state = None; - card.desired_retention = None; Ok(ComputeMemoryStateResponse { state: None, desired_retention, From 5cc3a2276ba3adeea3e53bb532af3d343c7f475b Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Sat, 21 Jun 2025 17:47:18 +0530 Subject: [PATCH 015/197] Fix repeated ticks in reviews graph (#4108) Regressed in #4086 --- ts/routes/graphs/reviews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 164fc4199..fd3786b5f 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -141,7 +141,7 @@ export function renderReviews( const yTickFormat = (n: number): string => { if (showTime) { - return timeSpan(n / 1000, true, false, TimespanUnit.Hours); + return timeSpan(n / 1000, true, true, TimespanUnit.Hours); } else { if (Math.round(n) != n) { return ""; From cfd448565ae0e9e917a0cdff1d3a5c5d89ac0fb0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:03:23 +0700 Subject: [PATCH 016/197] Fix sync-server separate compile https://forums.ankiweb.net/t/anki-25-06-beta/62271/96 --- rslib/sync/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/sync/Cargo.toml b/rslib/sync/Cargo.toml index e2d960503..7a8f8534a 100644 --- a/rslib/sync/Cargo.toml +++ b/rslib/sync/Cargo.toml @@ -13,4 +13,4 @@ path = "main.rs" name = "anki-sync-server" [dependencies] -anki.workspace = true +anki = { workspace = true, features = ["rustls"] } From 246fa75a35b3ace140ddc97b45f137eea07ddd1c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:13:26 +0700 Subject: [PATCH 017/197] Create release wheel as part of normal build Avoids the need for a separate publish --- qt/release/{update.sh => build.sh} | 7 +++++-- qt/release/publish.sh | 13 ------------- tools/build | 4 +++- 3 files changed, 8 insertions(+), 16 deletions(-) rename qt/release/{update.sh => build.sh} (91%) delete mode 100755 qt/release/publish.sh diff --git a/qt/release/update.sh b/qt/release/build.sh similarity index 91% rename from qt/release/update.sh rename to qt/release/build.sh index ddd99c398..423638bc4 100755 --- a/qt/release/update.sh +++ b/qt/release/build.sh @@ -2,7 +2,7 @@ set -e -test -f update.sh || { +test -f build.sh || { echo "run from release folder" exit 1 } @@ -63,6 +63,9 @@ echo "Generated pyproject.toml with version $VERSION" # Show diff if .old file exists if [ -f pyproject.toml.old ]; then echo - echo "Differences from previous version:" + echo "Differences from previous release version:" diff -u --color=always pyproject.toml.old pyproject.toml || true fi + +echo "Building wheel..." +"$UV" build --wheel --out-dir "$PROJ_ROOT/out/wheels" diff --git a/qt/release/publish.sh b/qt/release/publish.sh deleted file mode 100755 index 273e34953..000000000 --- a/qt/release/publish.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Get the project root (two levels up from qt/release) -PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" - -# Use extracted uv binary -UV="$PROJ_ROOT/out/extracted/uv/uv" - -rm -rf dist -"$UV" build --wheel - -#UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) "$UV" publish --index testpypi -UV_PUBLISH_TOKEN=$(pass show w/pypi-api) "$UV" publish diff --git a/tools/build b/tools/build index 3df9456ed..4074ff98d 100755 --- a/tools/build +++ b/tools/build @@ -1,6 +1,8 @@ #!/bin/bash -set -e +set -eo pipefail + rm -rf out/wheels/* RELEASE=2 ./ninja wheels +(cd qt/release && ./build.sh) echo "wheels are in out/wheels" From 782645d92e25b7d84a8f75ae8cfc3eaca39d16de Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 21 Jun 2025 19:13:30 +0700 Subject: [PATCH 018/197] Bump beta version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index 9079196d0..a38238a29 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.06b4 +25.06b5 From eb6c977e082f076dcac3056dc28ea8ecfad4e661 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 22 Jun 2025 20:25:15 +0700 Subject: [PATCH 019/197] Add menu to launcher, and improve terminal display on Windows --- Cargo.toml | 2 +- qt/launcher/src/bin/build_win.rs | 5 +- qt/launcher/src/main.rs | 348 +++++++++++++++++++++++++--- qt/launcher/src/platform/mac.rs | 7 +- qt/launcher/src/platform/unix.rs | 4 +- qt/launcher/src/platform/windows.rs | 61 ++++- 6 files changed, 378 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 61cca8649..d2ce2ce2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0" unicode-normalization = "0.1.24" walkdir = "2.5.0" which = "8.0.0" -winapi = { version = "0.3", features = ["wincon"] } +winapi = { version = "0.3", features = ["wincon", "errhandlingapi", "consoleapi"] } windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] } wiremock = "0.6.3" xz2 = "0.1.7" diff --git a/qt/launcher/src/bin/build_win.rs b/qt/launcher/src/bin/build_win.rs index 959034438..ff385d9ea 100644 --- a/qt/launcher/src/bin/build_win.rs +++ b/qt/launcher/src/bin/build_win.rs @@ -114,10 +114,13 @@ fn copy_files(output_dir: &Path) -> Result<()> { let launcher_dst = output_dir.join("anki.exe"); copy_file(&launcher_src, &launcher_dst)?; - // Copy uv.exe + // Copy uv.exe and uvw.exe let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe"); let uv_dst = output_dir.join("uv.exe"); copy_file(&uv_src, &uv_dst)?; + let uv_src = PathBuf::from("../../../out/extracted/uv/uvw.exe"); + let uv_dst = output_dir.join("uvw.exe"); + copy_file(&uv_src, &uv_dst)?; println!("Copying support files..."); diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 4f397ad99..40896caa9 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -4,28 +4,48 @@ #![windows_subsystem = "windows"] use std::io::stdin; +use std::io::stdout; +use std::io::Write; use std::process::Command; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; use anki_io::copy_if_newer; use anki_io::create_dir_all; use anki_io::modified_time; +use anki_io::read_file; use anki_io::remove_file; use anki_io::write_file; use anki_process::CommandExt; use anyhow::Context; use anyhow::Result; +use crate::platform::ensure_terminal_shown; use crate::platform::exec_anki; use crate::platform::get_anki_binary_path; use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_uv_binary_name; use crate::platform::handle_first_launch; -use crate::platform::handle_terminal_launch; use crate::platform::initial_terminal_setup; use crate::platform::launch_anki_detached; mod platform; +#[derive(Debug, Clone)] +pub enum VersionKind { + PyOxidizer(String), + Uv(String), +} + +#[derive(Debug, Clone)] +pub enum MainMenuChoice { + Latest, + KeepExisting, + Version(VersionKind), + ToggleBetas, + Quit, +} + #[derive(Debug, Clone, Default)] pub struct Config { pub show_console: bool, @@ -33,6 +53,9 @@ pub struct Config { fn main() { if let Err(e) = run() { + let mut config: Config = Config::default(); + initial_terminal_setup(&mut config); + eprintln!("Error: {:#}", e); eprintln!("Press enter to close..."); let mut input = String::new(); @@ -43,8 +66,7 @@ fn main() { } fn run() -> Result<()> { - let mut config = Config::default(); - initial_terminal_setup(&mut config); + let mut config: Config = Config::default(); let uv_install_root = dirs::data_local_dir() .context("Unable to determine data_dir")? @@ -62,60 +84,320 @@ fn run() -> Result<()> { // Create install directory and copy project files in create_dir_all(&uv_install_root)?; + let had_user_pyproj = user_pyproject_path.exists(); + if !had_user_pyproj { + // during initial launcher testing, enable betas by default + write_file(&prerelease_marker, "")?; + } + copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?; copy_if_newer(&dist_python_version_path, &user_python_version_path)?; - let pyproject_has_changed = - !user_pyproject_path.exists() || !sync_complete_marker.exists() || { - let pyproject_toml_time = modified_time(&user_pyproject_path)?; - let sync_complete_time = modified_time(&sync_complete_marker)?; - Ok::(pyproject_toml_time > sync_complete_time) - } - .unwrap_or(true); + let pyproject_has_changed = !sync_complete_marker.exists() || { + let pyproject_toml_time = modified_time(&user_pyproject_path)?; + let sync_complete_time = modified_time(&sync_complete_marker)?; + Ok::(pyproject_toml_time > sync_complete_time) + } + .unwrap_or(true); if !pyproject_has_changed { // If venv is already up to date, exec as normal + initial_terminal_setup(&mut config); let anki_bin = get_anki_binary_path(&uv_install_root); exec_anki(&anki_bin, &config)?; return Ok(()); } // we'll need to launch uv; reinvoke ourselves in a terminal so the user can see - handle_terminal_launch()?; + ensure_terminal_shown()?; + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top + println!("\x1B[1mAnki Launcher\x1B[0m\n"); - // Remove sync marker before attempting sync - let _ = remove_file(&sync_complete_marker); + // Check if there's an existing installation before removing marker + let has_existing_install = sync_complete_marker.exists(); - // Sync the venv - let mut command = Command::new(&uv_path); - command - .current_dir(&uv_install_root) - .args(["sync", "--upgrade", "--managed-python"]); + loop { + let menu_choice = get_main_menu_choice(has_existing_install, &prerelease_marker); - // Set UV_PRERELEASE=allow if prerelease file exists - if prerelease_marker.exists() { - command.env("UV_PRERELEASE", "allow"); + match menu_choice { + MainMenuChoice::Quit => std::process::exit(0), + MainMenuChoice::KeepExisting => { + // Skip sync, just launch existing installation + break; + } + MainMenuChoice::ToggleBetas => { + // Toggle beta prerelease file + if prerelease_marker.exists() { + let _ = remove_file(&prerelease_marker); + println!("Beta releases disabled."); + } else { + write_file(&prerelease_marker, "")?; + println!("Beta releases enabled."); + } + println!(); + continue; + } + _ => { + // For other choices, update project files and sync + update_pyproject_for_version( + menu_choice.clone(), + dist_pyproject_path.clone(), + user_pyproject_path.clone(), + dist_python_version_path.clone(), + user_python_version_path.clone(), + )?; + + // Remove sync marker before attempting sync + let _ = remove_file(&sync_complete_marker); + + // Sync the venv + let mut command = Command::new(&uv_path); + command.current_dir(&uv_install_root).args([ + "sync", + "--upgrade", + "--managed-python", + ]); + + // Add python version if .python-version file exists + if user_python_version_path.exists() { + let python_version = read_file(&user_python_version_path)?; + let python_version_str = String::from_utf8(python_version) + .context("Invalid UTF-8 in .python-version")?; + let python_version_trimmed = python_version_str.trim(); + command.args(["--python", python_version_trimmed]); + } + + // Set UV_PRERELEASE=allow if beta mode is enabled + if prerelease_marker.exists() { + command.env("UV_PRERELEASE", "allow"); + } + + match command.ensure_success() { + Ok(_) => { + // Sync succeeded, break out of loop + break; + } + Err(e) => { + // If sync fails due to things like a missing wheel on pypi, + // we need to remove the lockfile or uv will cache the bad result. + let _ = remove_file(&uv_lock_path); + println!("Install failed: {:#}", e); + println!(); + continue; + } + } + } + } } - // temporarily force it on during initial beta testing - command.env("UV_PRERELEASE", "allow"); - - if let Err(e) = command.ensure_success() { - // If sync fails due to things like a missing wheel on pypi, - // we need to remove the lockfile or uv will cache the bad result. - let _ = remove_file(&uv_lock_path); - return Err(e.into()); - } - - // Write marker file to indicate successful sync - write_file(&sync_complete_marker, "")?; + // Write marker file to indicate we've completed the sync process + write_sync_marker(&sync_complete_marker)?; // First launch let anki_bin = get_anki_binary_path(&uv_install_root); handle_first_launch(&anki_bin)?; + println!("\nPress enter to start Anki."); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + // Then launch the binary as detached subprocess so the terminal can close launch_anki_detached(&anki_bin, &config)?; Ok(()) } + +fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("Failed to get system time")? + .as_secs(); + write_file(sync_complete_marker, timestamp.to_string())?; + Ok(()) +} + +fn get_main_menu_choice( + has_existing_install: bool, + prerelease_marker: &std::path::Path, +) -> MainMenuChoice { + loop { + println!("1) Latest Anki (just press enter)"); + println!("2) Choose a version"); + if has_existing_install { + println!("3) Keep existing version"); + } + println!(); + + let betas_enabled = prerelease_marker.exists(); + println!( + "4) Allow betas: {}", + if betas_enabled { "on" } else { "off" } + ); + println!("5) Quit"); + print!("> "); + let _ = stdout().flush(); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + let input = input.trim(); + + println!(); + + return match input { + "" | "1" => MainMenuChoice::Latest, + "2" => MainMenuChoice::Version(get_version_kind()), + "3" => { + if has_existing_install { + MainMenuChoice::KeepExisting + } else { + println!("Invalid input. Please try again.\n"); + continue; + } + } + "4" => MainMenuChoice::ToggleBetas, + "5" => MainMenuChoice::Quit, + _ => { + println!("Invalid input. Please try again."); + continue; + } + }; + } +} + +fn get_version_kind() -> VersionKind { + loop { + println!("Enter the version you want to install:"); + print!("> "); + let _ = stdout().flush(); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + let input = input.trim(); + + if input.is_empty() { + println!("Please enter a version."); + continue; + } + + match parse_version_kind(input) { + Some(version_kind) => { + println!(); + return version_kind; + } + None => { + println!("Invalid version format. Please enter a version like 24.10 or 25.06.1 (minimum 2.1.50)"); + continue; + } + } + } +} + +fn update_pyproject_for_version( + menu_choice: MainMenuChoice, + dist_pyproject_path: std::path::PathBuf, + user_pyproject_path: std::path::PathBuf, + dist_python_version_path: std::path::PathBuf, + user_python_version_path: std::path::PathBuf, +) -> Result<()> { + match menu_choice { + MainMenuChoice::Latest => { + let content = read_file(&dist_pyproject_path)?; + write_file(&user_pyproject_path, &content)?; + let python_version_content = read_file(&dist_python_version_path)?; + write_file(&user_python_version_path, &python_version_content)?; + } + MainMenuChoice::KeepExisting => { + // Do nothing - keep existing pyproject.toml and .python-version + } + MainMenuChoice::ToggleBetas => { + // This should not be reached as ToggleBetas is handled in the loop + unreachable!("ToggleBetas should be handled in the main loop"); + } + MainMenuChoice::Version(version_kind) => { + let content = read_file(&dist_pyproject_path)?; + let content_str = + String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?; + let updated_content = match &version_kind { + VersionKind::PyOxidizer(version) => { + // Replace package name and add PyQt6 dependencies + content_str.replace( + "anki-release", + &format!( + concat!( + "aqt[qt6]=={}\",\n", + " \"pyqt6==6.6.1\",\n", + " \"pyqt6-qt6==6.6.2\",\n", + " \"pyqt6-webengine==6.6.0\",\n", + " \"pyqt6-webengine-qt6==6.6.2\",\n", + " \"pyqt6_sip==13.6.0" + ), + version + ), + ) + } + VersionKind::Uv(version) => { + content_str.replace("anki-release", &format!("anki-release=={}", version)) + } + }; + write_file(&user_pyproject_path, &updated_content)?; + + // Update .python-version based on version kind + match &version_kind { + VersionKind::PyOxidizer(_) => { + write_file(&user_python_version_path, "3.9")?; + } + VersionKind::Uv(_) => { + copy_if_newer(&dist_python_version_path, &user_python_version_path)?; + } + } + } + MainMenuChoice::Quit => { + std::process::exit(0); + } + } + Ok(()) +} + +fn parse_version_kind(version: &str) -> Option { + let numeric_chars: String = version + .chars() + .filter(|c| c.is_ascii_digit() || *c == '.') + .collect(); + + let parts: Vec<&str> = numeric_chars.split('.').collect(); + + if parts.len() < 2 { + return None; + } + + let major: u32 = match parts[0].parse() { + Ok(val) => val, + Err(_) => return None, + }; + + let minor: u32 = match parts[1].parse() { + Ok(val) => val, + Err(_) => return None, + }; + + let patch: u32 = if parts.len() >= 3 { + match parts[2].parse() { + Ok(val) => val, + Err(_) => return None, + } + } else { + 0 // Default patch to 0 if not provided + }; + + // Reject versions < 2.1.50 + if major == 2 && (minor != 1 || patch < 50) { + return None; + } + + if major < 25 || (major == 25 && minor < 6) { + Some(VersionKind::PyOxidizer(version.to_string())) + } else { + Some(VersionKind::Uv(version.to_string())) + } +} diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs index b5157dd4b..1e7e4a695 100644 --- a/qt/launcher/src/platform/mac.rs +++ b/qt/launcher/src/platform/mac.rs @@ -28,12 +28,9 @@ pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) Ok(()) } -pub fn handle_terminal_launch() -> Result<()> { +pub fn ensure_terminal_shown() -> Result<()> { let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout()); - if stdout_is_terminal { - print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top - println!("\x1B[1mPreparing to start Anki...\x1B[0m\n"); - } else { + if !stdout_is_terminal { // If launched from GUI, relaunch in Terminal.app relaunch_in_terminal()?; } diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs index 4155f39d1..65a223f30 100644 --- a/qt/launcher/src/platform/unix.rs +++ b/qt/launcher/src/platform/unix.rs @@ -16,9 +16,7 @@ pub fn initial_terminal_setup(_config: &mut Config) { // No special terminal setup needed on Unix } -pub fn handle_terminal_launch() -> Result<()> { - print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top - println!("\x1B[1mPreparing to start Anki...\x1B[0m\n"); +pub fn ensure_terminal_shown() -> Result<()> { // Skip terminal relaunch on non-macOS Unix systems as we don't know which // terminal is installed Ok(()) diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index c536c8c76..4e3752d44 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -7,27 +7,77 @@ use std::process::Command; use anki_process::CommandExt; use anyhow::Context; use anyhow::Result; +use winapi::um::consoleapi; +use winapi::um::errhandlingapi; +use winapi::um::wincon; use crate::Config; -pub fn handle_terminal_launch() -> Result<()> { - // uv will do this itself +pub fn ensure_terminal_shown() -> Result<()> { + ensure_console(); + // // Check if we're already relaunched to prevent infinite recursion + // if std::env::var("ANKI_LAUNCHER_IN_TERMINAL").is_ok() { + // println!("Recurse: Preparing to start Anki...\n"); + // return Ok(()); + // } + + // if have_console { + // } else { + // relaunch_in_cmd()?; + // } Ok(()) } +fn ensure_console() { + unsafe { + if !wincon::GetConsoleWindow().is_null() { + return; + } + + if consoleapi::AllocConsole() == 0 { + let error_code = errhandlingapi::GetLastError(); + eprintln!("unexpected AllocConsole error: {}", error_code); + return; + } + + // This black magic triggers Windows to switch to the new + // ANSI-supporting console host, which is usually only available + // when the app is built with the console subsystem. + let _ = Command::new("cmd").args(&["/C", ""]).status(); + } +} + +fn attach_to_parent_console() -> bool { + unsafe { + if !wincon::GetConsoleWindow().is_null() { + // we have a console already + println!("attach: already had console, false"); + return false; + } + + if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 { + // successfully attached to parent + println!("attach: true"); + true + } else { + println!("attach: false"); + false + } + } +} + /// If parent process has a console (eg cmd.exe), redirect our output there. /// Sets config.show_console to true if successfully attached to console. pub fn initial_terminal_setup(config: &mut Config) { use std::ffi::CString; use libc_stdhandle::*; - use winapi::um::wincon; - let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) }; - if console_attached == 0 { + if !attach_to_parent_console() { return; } + // we launched without a console, so we'll need to open stdin/out/err let conin = CString::new("CONIN$").unwrap(); let conout = CString::new("CONOUT$").unwrap(); let r = CString::new("r").unwrap(); @@ -113,6 +163,5 @@ pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { } pub fn get_uv_binary_name() -> &'static str { - // Windows uses standard uv binary name "uv.exe" } From d2f818fad286f08d86bc5243cb8bb7c83e3263e9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 22 Jun 2025 20:57:53 +0700 Subject: [PATCH 020/197] macOS launcher improvements - do mpv initial run in parallel - improve messages, show dots regularly --- qt/aqt/package.py | 74 ++++++++++++++++----------------- qt/launcher/mac/build.sh | 10 ++--- qt/launcher/src/main.rs | 2 + qt/launcher/src/platform/mac.rs | 39 ++++++++++++++++- 4 files changed, 81 insertions(+), 44 deletions(-) diff --git a/qt/aqt/package.py b/qt/aqt/package.py index a0642fca0..f1834b594 100644 --- a/qt/aqt/package.py +++ b/qt/aqt/package.py @@ -23,47 +23,45 @@ def first_run_setup() -> None: if not is_mac: return - def _dot(): - print(".", flush=True, end="") - - _dot() - import anki.collection - - _dot() - import PyQt6.sip - - _dot() - import PyQt6.QtCore - - _dot() - import PyQt6.QtGui - - _dot() - import PyQt6.QtNetwork - - _dot() - import PyQt6.QtQuick - - _dot() - import PyQt6.QtWebChannel - - _dot() - import PyQt6.QtWebEngineCore - - _dot() - import PyQt6.QtWebEngineWidgets - - _dot() + # Import anki_audio first and spawn commands import anki_audio - import PyQt6.QtWidgets audio_pkg_path = Path(anki_audio.__file__).parent - # Invoke mpv and lame - cmd = [Path(""), "--version"] + # Start mpv and lame commands concurrently + processes = [] for cmd_name in ["mpv", "lame"]: - _dot() - cmd[0] = audio_pkg_path / cmd_name - subprocess.run([str(cmd[0]), str(cmd[1])], check=True, capture_output=True) + cmd_path = audio_pkg_path / cmd_name + proc = subprocess.Popen( + [str(cmd_path), "--version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + processes.append(proc) - print() + # Continue with other imports while commands run + import concurrent.futures + + import bs4 + import flask + import flask_cors + import markdown + import PyQt6.QtCore + import PyQt6.QtGui + import PyQt6.QtNetwork + import PyQt6.QtQuick + import PyQt6.QtWebChannel + import PyQt6.QtWebEngineCore + import PyQt6.QtWebEngineWidgets + import PyQt6.QtWidgets + import PyQt6.sip + import requests + import waitress + + import anki.collection + + from . import _macos_helper + + # Wait for both commands to complete + for proc in processes: + proc.wait() diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh index eb4483488..0ec39ad8f 100755 --- a/qt/launcher/mac/build.sh +++ b/qt/launcher/mac/build.sh @@ -47,8 +47,8 @@ done codesign -vvv "$APP_LAUNCHER" spctl -a "$APP_LAUNCHER" -# Notarize -./notarize.sh "$OUTPUT_DIR" - -# Bundle -./dmg/build.sh "$OUTPUT_DIR" \ No newline at end of file +# Notarize and bundle (skip if NODMG is set) +if [ -z "$NODMG" ]; then + ./notarize.sh "$OUTPUT_DIR" + ./dmg/build.sh "$OUTPUT_DIR" +fi \ No newline at end of file diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 40896caa9..2ad3ac00c 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -172,6 +172,8 @@ fn run() -> Result<()> { command.env("UV_PRERELEASE", "allow"); } + println!("\x1B[1mUpdating Anki...\x1B[0m\n"); + match command.ensure_success() { Ok(_) => { // Sync succeeded, break out of loop diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs index 1e7e4a695..57d6bcc73 100644 --- a/qt/launcher/src/platform/mac.rs +++ b/qt/launcher/src/platform/mac.rs @@ -3,6 +3,11 @@ use std::os::unix::process::CommandExt; use std::process::Command; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; use anki_process::CommandExt as AnkiCommandExt; use anyhow::Context; @@ -25,6 +30,9 @@ pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) .process_group(0) .ensure_spawn()?; std::mem::forget(child); + + println!("Anki will start shortly."); + println!("\x1B[1mYou can close this window.\x1B[0m\n"); Ok(()) } @@ -34,6 +42,10 @@ pub fn ensure_terminal_shown() -> Result<()> { // If launched from GUI, relaunch in Terminal.app relaunch_in_terminal()?; } + + // Set terminal title to "Anki Launcher" + print!("\x1b]0;Anki Launcher\x07"); + Ok(()) } @@ -47,12 +59,37 @@ fn relaunch_in_terminal() -> Result<()> { } pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { + use std::io::Write; + use std::io::{ + self, + }; + // Pre-validate by running --version to trigger any Gatekeeper checks - println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m"); + print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m"); + io::stdout().flush().unwrap(); + + // Start progress indicator + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + let progress_thread = thread::spawn(move || { + while running_clone.load(Ordering::Relaxed) { + print!("."); + io::stdout().flush().unwrap(); + thread::sleep(Duration::from_secs(1)); + } + }); + let _ = Command::new(anki_bin) .env("ANKI_FIRST_RUN", "1") .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) .ensure_success(); + + // Stop progress indicator + running.store(false, Ordering::Relaxed); + progress_thread.join().unwrap(); + println!(); // New line after dots Ok(()) } From b250a2f7245cb41f10581bfca2f89106fc0e366c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 22 Jun 2025 21:52:44 +0700 Subject: [PATCH 021/197] Add terminal support for *nix --- qt/launcher/lin/build.sh | 43 ++++++++++++++++++------- qt/launcher/src/platform/mac.rs | 16 ++-------- qt/launcher/src/platform/unix.rs | 55 ++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/qt/launcher/lin/build.sh b/qt/launcher/lin/build.sh index e4ddce243..de96a1b50 100755 --- a/qt/launcher/lin/build.sh +++ b/qt/launcher/lin/build.sh @@ -1,9 +1,15 @@ #!/bin/bash +# +# This script currently only supports universal builds on x86_64. +# set -e # Add Linux cross-compilation target rustup target add aarch64-unknown-linux-gnu +# Detect host architecture +HOST_ARCH=$(uname -m) + # Define output paths OUTPUT_DIR="../../../out/launcher" @@ -12,11 +18,18 @@ LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher" # Clean existing output directory rm -rf "$LAUNCHER_DIR" -# Build binaries for both Linux architectures -cargo build -p launcher --release --target x86_64-unknown-linux-gnu -CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ +# Build binaries based on host architecture +if [ "$HOST_ARCH" = "aarch64" ]; then + # On aarch64 host, only build for aarch64 cargo build -p launcher --release --target aarch64-unknown-linux-gnu -(cd ../../.. && ./ninja extract:uv_lin_arm) +else + # On other hosts, build for both architectures + cargo build -p launcher --release --target x86_64-unknown-linux-gnu + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + cargo build -p launcher --release --target aarch64-unknown-linux-gnu + # Extract uv_lin_arm for cross-compilation + (cd ../../.. && ./ninja extract:uv_lin_arm) +fi # Create output directory mkdir -p "$LAUNCHER_DIR" @@ -24,13 +37,21 @@ mkdir -p "$LAUNCHER_DIR" # Copy binaries and support files TARGET_DIR=${CARGO_TARGET_DIR:-../../../target} -# Copy launcher binaries with architecture suffixes -cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64" -cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64" - -# Copy uv binaries with architecture suffixes -cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64" -cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64" +# Copy binaries with architecture suffixes +if [ "$HOST_ARCH" = "aarch64" ]; then + # On aarch64 host, copy arm64 binary to both locations + cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64" + cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64" + # Copy uv binary to both locations + cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64" + cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.arm64" +else + # On other hosts, copy architecture-specific binaries + cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64" + cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64" + cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64" + cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64" +fi # Copy support files from lin directory for file in README.md anki.1 anki.desktop anki.png anki.xml anki.xpm install.sh uninstall.sh anki; do diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs index 57d6bcc73..2369f538a 100644 --- a/qt/launcher/src/platform/mac.rs +++ b/qt/launcher/src/platform/mac.rs @@ -15,6 +15,7 @@ use anyhow::Result; // Re-export Unix functions that macOS uses pub use super::unix::{ + ensure_terminal_shown, exec_anki, get_anki_binary_path, initial_terminal_setup, @@ -36,20 +37,7 @@ pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) Ok(()) } -pub fn ensure_terminal_shown() -> Result<()> { - let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout()); - if !stdout_is_terminal { - // If launched from GUI, relaunch in Terminal.app - relaunch_in_terminal()?; - } - - // Set terminal title to "Anki Launcher" - print!("\x1b]0;Anki Launcher\x07"); - - Ok(()) -} - -fn relaunch_in_terminal() -> Result<()> { +pub fn relaunch_in_terminal() -> Result<()> { let current_exe = std::env::current_exe().context("Failed to get current executable path")?; Command::new("open") .args(["-a", "Terminal"]) diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs index 65a223f30..324bf5aa3 100644 --- a/qt/launcher/src/platform/unix.rs +++ b/qt/launcher/src/platform/unix.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] +use std::io::IsTerminal; use std::path::PathBuf; use std::process::Command; @@ -17,8 +18,58 @@ pub fn initial_terminal_setup(_config: &mut Config) { } pub fn ensure_terminal_shown() -> Result<()> { - // Skip terminal relaunch on non-macOS Unix systems as we don't know which - // terminal is installed + let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout()); + if !stdout_is_terminal { + // If launched from GUI, try to relaunch in a terminal + crate::platform::relaunch_in_terminal()?; + } + + // Set terminal title to "Anki Launcher" + print!("\x1b]2;Anki Launcher\x07"); + + Ok(()) +} + +#[cfg(not(target_os = "macos"))] +pub fn relaunch_in_terminal() -> Result<()> { + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + + // Try terminals in order of preference + let terminals = [ + ("x-terminal-emulator", vec!["-e"]), + ("gnome-terminal", vec!["--"]), + ("konsole", vec!["-e"]), + ("xfce4-terminal", vec!["-e"]), + ("alacritty", vec!["-e"]), + ("kitty", vec![]), + ("foot", vec![]), + ("urxvt", vec!["-e"]), + ("xterm", vec!["-e"]), + ]; + + for (terminal_cmd, args) in &terminals { + // Check if terminal exists + if Command::new("which") + .arg(terminal_cmd) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + // Try to launch the terminal + let mut cmd = Command::new(terminal_cmd); + if args.is_empty() { + cmd.arg(¤t_exe); + } else { + cmd.args(args).arg(¤t_exe); + } + + if cmd.spawn().is_ok() { + std::process::exit(0); + } + } + } + + // If no terminal worked, continue without relaunching Ok(()) } From a73f1507ba6a0e2174b8f9487d83a87278cc5d3d Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 25 Jun 2025 19:08:25 +0800 Subject: [PATCH 022/197] use KeyboardEvent.key instead of code (#4114) --- ts/lib/tag-editor/TagInput.svelte | 2 +- ts/lib/tslib/keys.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ts/lib/tag-editor/TagInput.svelte b/ts/lib/tag-editor/TagInput.svelte index a8d76bcee..31d3b51f6 100644 --- a/ts/lib/tag-editor/TagInput.svelte +++ b/ts/lib/tag-editor/TagInput.svelte @@ -166,7 +166,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } function onKeydown(event: KeyboardEvent): void { - switch (event.code) { + switch (event.key) { case "Enter": onEnter(event); break; diff --git a/ts/lib/tslib/keys.ts b/ts/lib/tslib/keys.ts index 9bd6b42d8..58f571fac 100644 --- a/ts/lib/tslib/keys.ts +++ b/ts/lib/tslib/keys.ts @@ -90,7 +90,7 @@ export function keyToPlatformString(key: string): string { } export function isArrowLeft(event: KeyboardEvent): boolean { - if (event.code === "ArrowLeft") { + if (event.key === "ArrowLeft") { return true; } @@ -98,7 +98,7 @@ export function isArrowLeft(event: KeyboardEvent): boolean { } export function isArrowRight(event: KeyboardEvent): boolean { - if (event.code === "ArrowRight") { + if (event.key === "ArrowRight") { return true; } @@ -106,7 +106,7 @@ export function isArrowRight(event: KeyboardEvent): boolean { } export function isArrowUp(event: KeyboardEvent): boolean { - if (event.code === "ArrowUp") { + if (event.key === "ArrowUp") { return true; } @@ -114,7 +114,7 @@ export function isArrowUp(event: KeyboardEvent): boolean { } export function isArrowDown(event: KeyboardEvent): boolean { - if (event.code === "ArrowDown") { + if (event.key === "ArrowDown") { return true; } From 06195d1268ae01115dad79452d824a14df641f8c Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 25 Jun 2025 19:15:45 +0800 Subject: [PATCH 023/197] add bottom and right margins to account for focus outline (#4115) --- ts/lib/tag-editor/TagEditor.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index cbbbf3f57..eb033ef7a 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -510,7 +510,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html border: 1px solid var(--border); border-radius: var(--border-radius); padding: 6px; - margin: 1px; + margin: 1px 3px 3px 1px; &:focus-within { outline-offset: -1px; From d6f93fab767dfca0522ef129f9c613c0ac0ce767 Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 25 Jun 2025 19:20:31 +0800 Subject: [PATCH 024/197] adjust top toolbar height on body class update (#4120) --- qt/aqt/toolbar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 44f2ee66c..be547b5ba 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -87,6 +87,7 @@ class TopWebView(ToolbarWebView): else: self.flatten() + self.adjustHeightToFit() self.show() def _onHeight(self, qvar: int | None) -> None: From 992fb054bd2ada03cd9950c22f777a714e2291f1 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Wed, 25 Jun 2025 21:40:51 +0800 Subject: [PATCH 025/197] Refactor FSRS data clearing into Card::clear_fsrs_data (#4123) Extracted repeated logic for clearing FSRS-related fields into a new Card::clear_fsrs_data() method. Updated set_deck and FSRS disabling code paths to use this method for improved code reuse and maintainability. --- rslib/src/card/mod.rs | 10 +++++++--- rslib/src/scheduler/fsrs/memory_state.rs | 4 +--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 49d952ecf..598ac602b 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -185,12 +185,16 @@ impl Card { self.usn = usn; } - /// Caller must ensure provided deck exists and is not filtered. - fn set_deck(&mut self, deck: DeckId) { - self.remove_from_filtered_deck_restoring_queue(); + pub fn clear_fsrs_data(&mut self) { self.memory_state = None; self.desired_retention = None; self.decay = None; + } + + /// Caller must ensure provided deck exists and is not filtered. + fn set_deck(&mut self, deck: DeckId) { + self.remove_from_filtered_deck_restoring_queue(); + self.clear_fsrs_data(); self.deck_id = deck; } diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index d01cde767..b592e4da4 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -188,9 +188,7 @@ impl Collection { } } else { // clear FSRS data if FSRS is disabled - card.memory_state = None; - card.desired_retention = None; - card.decay = None; + card.clear_fsrs_data(); } self.update_card_inner(&mut card, original, usn)?; } From 630bdd31893d5113b81cd60612cef3ebf146d5b4 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 25 Jun 2025 14:44:47 +0100 Subject: [PATCH 026/197] Fix/Optimize button alignment (#4117) * Fix/Button alignment * add hr --- ts/routes/deck-options/FsrsOptions.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index f573a0278..a2eb36fe3 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -391,7 +391,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -
+
+ +
@@ -419,4 +421,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html align-content: center; flex-wrap: wrap; } + + hr { + border-top: 1px solid var(--border); + opacity: 1; + } From 7edd9221acb93f02a02227d9b4a576a5ed89466f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 27 Jun 2025 16:05:03 +0700 Subject: [PATCH 027/197] Avoid Qt's automatic About labeling It gets confused by our launcher process, and provides no way to alter the default assigned text while keeping the About role on a Mac. --- ftl/qt/qt-accel.ftl | 1 + qt/aqt/forms/main.ui | 2 +- qt/aqt/main.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ftl/qt/qt-accel.ftl b/ftl/qt/qt-accel.ftl index 327cd6c46..6c832b368 100644 --- a/ftl/qt/qt-accel.ftl +++ b/ftl/qt/qt-accel.ftl @@ -1,4 +1,5 @@ qt-accel-about = &About +qt-accel-about-mac = About Anki... qt-accel-cards = &Cards qt-accel-check-database = &Check Database qt-accel-check-media = Check &Media diff --git a/qt/aqt/forms/main.ui b/qt/aqt/forms/main.ui index 596ea985c..0687d4ef3 100644 --- a/qt/aqt/forms/main.ui +++ b/qt/aqt/forms/main.ui @@ -138,7 +138,7 @@ qt_accel_about - QAction::AboutRole + QAction::MenuRole::ApplicationSpecificRole diff --git a/qt/aqt/main.py b/qt/aqt/main.py index bc28e287b..8e01208a4 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1405,6 +1405,7 @@ title="{}" {}>{}""".format( qconnect(m.actionDocumentation.triggered, self.onDocumentation) qconnect(m.actionDonate.triggered, self.onDonate) qconnect(m.actionAbout.triggered, self.onAbout) + m.actionAbout.setText(tr.qt_accel_about_mac()) # Edit qconnect(m.actionUndo.triggered, self.undo) From 9b287dc51ac8d7081cd54312bdbc390753f180e7 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 23 Jun 2025 16:59:50 +0700 Subject: [PATCH 028/197] Python dependency/wheel tweaks - Use --locked to assert that the lockfile won't change, so we need to explicitly 'uv lock' when making changes. Still trying to get to the bottom of why the lockfile sometimes has editable entries, which break things when switching between platforms. - Exclude __pycache__ from wheels - Move the typing stubs to our dev deps (https://github.com/ankitects/anki/pull/4074#pullrequestreview-2948088436) --- CLAUDE.md | 2 +- build/configure/src/aqt.rs | 7 +++++- build/configure/src/pylib.rs | 3 ++- build/runner/src/pyenv.rs | 2 +- pylib/hatch_build.py | 10 ++++++++- pylib/pyproject.toml | 4 +--- pyproject.toml | 10 +++++++++ qt/hatch_build.py | 9 ++++++-- qt/pyproject.toml | 7 ------ uv.lock | 42 ++++++++++++++++++------------------ 10 files changed, 58 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6ec6db642..3be5cc70b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ Please do this as a final step before marking a task as completed. During development, you can build/check subsections of our code: - Rust: 'cargo check' -- Python: './tools/dmypy' +- Python: './tools/dmypy', and if wheel-related, './ninja wheels' - TypeScript/Svelte: './ninja check:svelte' Be mindful that some changes (such as modifications to .proto files) may diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 0a9b32270..83be77e91 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -342,7 +342,12 @@ fn build_wheel(build: &mut Build) -> Result<()> { name: "aqt", version: anki_version(), platform: None, - deps: inputs![":qt:aqt", glob!("qt/aqt/**"), "qt/pyproject.toml"], + deps: inputs![ + ":qt:aqt", + glob!("qt/aqt/**"), + "qt/pyproject.toml", + "qt/hatch_build.py" + ], }, ) } diff --git a/build/configure/src/pylib.rs b/build/configure/src/pylib.rs index bcef1ecc4..21820ae8b 100644 --- a/build/configure/src/pylib.rs +++ b/build/configure/src/pylib.rs @@ -68,7 +68,8 @@ pub fn build_pylib(build: &mut Build) -> Result<()> { deps: inputs![ ":pylib:anki", glob!("pylib/anki/**"), - "pylib/pyproject.toml" + "pylib/pyproject.toml", + "pylib/hatch_build.py" ], }, )?; diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs index 0bd5ec662..d64c8fb3f 100644 --- a/build/runner/src/pyenv.rs +++ b/build/runner/src/pyenv.rs @@ -35,7 +35,7 @@ pub fn setup_pyenv(args: PyenvArgs) { run_command( Command::new(args.uv_bin) .env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone()) - .args(["sync", "--frozen"]) + .args(["sync", "--locked"]) .args(args.extra_args), ); diff --git a/pylib/hatch_build.py b/pylib/hatch_build.py index c3539da56..9e8ee9799 100644 --- a/pylib/hatch_build.py +++ b/pylib/hatch_build.py @@ -35,8 +35,16 @@ class CustomBuildHook(BuildHookInterface): assert generated_root.exists(), "you should build with --wheel" for path in generated_root.rglob("*"): - if path.is_file(): + if path.is_file() and not self._should_exclude(path): relative_path = path.relative_to(generated_root) # Place files under anki/ in the distribution dist_path = "anki" / relative_path force_include[str(path)] = str(dist_path) + + def _should_exclude(self, path: Path) -> bool: + """Check if a file should be excluded from the wheel.""" + # Exclude __pycache__ + path_str = str(path) + if "/__pycache__/" in path_str: + return True + return False diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml index 555f30c86..d6d41f970 100644 --- a/pylib/pyproject.toml +++ b/pylib/pyproject.toml @@ -10,10 +10,8 @@ dependencies = [ "orjson", "protobuf>=4.21", "requests[socks]", + # remove after we update to min python 3.11+ "typing_extensions", - "types-protobuf", - "types-requests", - "types-orjson", # platform-specific dependencies "distro; sys_platform != 'darwin' and sys_platform != 'win32'", "psutil; sys_platform == 'win32'", diff --git a/pyproject.toml b/pyproject.toml index f5443e229..2e47ee2f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,16 @@ dev = [ "colorama", # for isort --color "wheel", "hatchling", # for type checking hatch_build.py files + "mock", + "types-protobuf", + "types-requests", + "types-orjson", + "types-decorator", + "types-flask", + "types-flask-cors", + "types-markdown", + "types-waitress", + "types-pywin32", ] [project.optional-dependencies] diff --git a/qt/hatch_build.py b/qt/hatch_build.py index fc716a57f..aaf345842 100644 --- a/qt/hatch_build.py +++ b/qt/hatch_build.py @@ -67,11 +67,16 @@ class CustomBuildHook(BuildHookInterface): def _should_exclude(self, path: Path) -> bool: """Check if a file should be excluded from the wheel.""" - # Match the exclusions from write_wheel.py exclude_aqt function + path_str = str(path) + + # Exclude __pycache__ + if "/__pycache__/" in path_str: + return True + if path.suffix in [".ui", ".scss", ".map", ".ts"]: return True if path.name.startswith("tsconfig"): return True - if "/aqt/data" in str(path): + if "/aqt/data" in path_str: return True return False diff --git a/qt/pyproject.toml b/qt/pyproject.toml index e6537c76c..ab5f50263 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -15,13 +15,6 @@ dependencies = [ "pywin32; sys.platform == 'win32'", "anki-mac-helper; sys.platform == 'darwin'", "pip-system-certs!=5.1", - "mock", - "types-decorator", - "types-flask", - "types-flask-cors", - "types-markdown", - "types-waitress", - "types-pywin32", "pyqt6>=6.2", "pyqt6-webengine>=6.2", # anki dependency is added dynamically in hatch_build.py with exact version diff --git a/uv.lock b/uv.lock index 348d14dd0..ce2f9536d 100644 --- a/uv.lock +++ b/uv.lock @@ -59,9 +59,6 @@ dependencies = [ { name = "protobuf" }, { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "requests", extra = ["socks"] }, - { name = "types-orjson" }, - { name = "types-protobuf" }, - { name = "types-requests" }, { name = "typing-extensions" }, ] @@ -75,9 +72,6 @@ requires-dist = [ { name = "protobuf", specifier = ">=4.21" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "requests", extras = ["socks"] }, - { name = "types-orjson" }, - { name = "types-protobuf" }, - { name = "types-requests" }, { name = "typing-extensions" }, ] @@ -111,11 +105,21 @@ dev = [ { name = "colorama" }, { name = "hatchling" }, { name = "isort" }, + { name = "mock" }, { name = "mypy" }, { name = "mypy-protobuf" }, { name = "pychromedevtools" }, { name = "pylint" }, { name = "pytest" }, + { name = "types-decorator" }, + { name = "types-flask" }, + { name = "types-flask-cors" }, + { name = "types-markdown" }, + { name = "types-orjson" }, + { name = "types-protobuf" }, + { name = "types-pywin32" }, + { name = "types-requests" }, + { name = "types-waitress" }, { name = "wheel" }, ] @@ -133,11 +137,21 @@ dev = [ { name = "colorama" }, { name = "hatchling" }, { name = "isort" }, + { name = "mock" }, { name = "mypy" }, { name = "mypy-protobuf" }, { name = "pychromedevtools" }, { name = "pylint" }, { name = "pytest" }, + { name = "types-decorator" }, + { name = "types-flask" }, + { name = "types-flask-cors" }, + { name = "types-markdown" }, + { name = "types-orjson" }, + { name = "types-protobuf" }, + { name = "types-pywin32" }, + { name = "types-requests" }, + { name = "types-waitress" }, { name = "wheel" }, ] @@ -158,7 +172,6 @@ dependencies = [ { name = "flask" }, { name = "flask-cors" }, { name = "jsonschema" }, - { name = "mock" }, { name = "pip-system-certs", version = "4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "pip-system-certs", version = "5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, @@ -173,12 +186,6 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "requests" }, { name = "send2trash" }, - { name = "types-decorator" }, - { name = "types-flask" }, - { name = "types-flask-cors" }, - { name = "types-markdown" }, - { name = "types-pywin32" }, - { name = "types-waitress" }, { name = "waitress" }, ] @@ -223,7 +230,6 @@ requires-dist = [ { name = "flask" }, { name = "flask-cors" }, { name = "jsonschema" }, - { name = "mock" }, { name = "pip-system-certs", specifier = "!=5.1" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "pyqt6", specifier = ">=6.2" }, @@ -251,12 +257,6 @@ requires-dist = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "send2trash" }, - { name = "types-decorator" }, - { name = "types-flask" }, - { name = "types-flask-cors" }, - { name = "types-markdown" }, - { name = "types-pywin32" }, - { name = "types-waitress" }, { name = "waitress", specifier = ">=2.0.0" }, ] provides-extras = ["audio", "qt", "qt66", "qt67", "qt69"] @@ -595,7 +595,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ From 73edf23954928bae3e262f88114595ccff783bde Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 24 Jun 2025 13:05:06 +0700 Subject: [PATCH 029/197] Drop Pauker and SuperMemo importers from legacy importer The legacy importer has only been kept around to support some add-ons, and these are so infrequently used that they're better off shifted to add-ons (even they even still work) --- LICENSE | 2 - ftl/core/importing.ftl | 4 +- pylib/anki/importing/__init__.py | 4 - pylib/anki/importing/pauker.py | 94 ----- pylib/anki/importing/supermemo_xml.py | 484 -------------------------- pylib/pyproject.toml | 1 - pylib/tests/test_importing.py | 17 - tools/minilints/src/main.rs | 5 - uv.lock | 2 - 9 files changed, 2 insertions(+), 611 deletions(-) delete mode 100644 pylib/anki/importing/pauker.py delete mode 100644 pylib/anki/importing/supermemo_xml.py diff --git a/LICENSE b/LICENSE index 033dc2a0a..456a7cfd6 100644 --- a/LICENSE +++ b/LICENSE @@ -6,8 +6,6 @@ The following included source code items use a license other than AGPL3: In the pylib folder: - * The SuperMemo importer: GPL3 and 0BSD. - * The Pauker importer: BSD-3. * statsbg.py: CC BY 4.0. In the qt folder: diff --git a/ftl/core/importing.ftl b/ftl/core/importing.ftl index 70bc5f4d1..3b9f7c401 100644 --- a/ftl/core/importing.ftl +++ b/ftl/core/importing.ftl @@ -65,7 +65,6 @@ importing-with-deck-configs-help = If enabled, any deck options that the deck sharer included will also be imported. Otherwise, all decks will be assigned the default preset. importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip) -importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz) # the '|' character importing-pipe = Pipe # Warning displayed when the csv import preview table is clipped (some columns were hidden) @@ -78,7 +77,6 @@ importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } field importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual. importing-semicolon = Semicolon importing-skipped = Skipped -importing-supermemo-xml-export-xml = Supermemo XML export (*.xml) importing-tab = Tab importing-tag-modified-notes = Tag modified notes: importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*) @@ -252,3 +250,5 @@ importing-importing-collection = Importing collection... importing-unable-to-import-filename = Unable to import { $filename }: file type not supported importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val } importing-added = Added +importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz) +importing-supermemo-xml-export-xml = Supermemo XML export (*.xml) diff --git a/pylib/anki/importing/__init__.py b/pylib/anki/importing/__init__.py index cfc2cac3f..d4fccc643 100644 --- a/pylib/anki/importing/__init__.py +++ b/pylib/anki/importing/__init__.py @@ -11,8 +11,6 @@ from anki.importing.apkg import AnkiPackageImporter from anki.importing.base import Importer from anki.importing.csvfile import TextImporter from anki.importing.mnemo import MnemosyneImporter -from anki.importing.pauker import PaukerImporter -from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore from anki.lang import TR @@ -24,8 +22,6 @@ def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]: AnkiPackageImporter, ), (col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter), - (col.tr.importing_supermemo_xml_export_xml(), SupermemoXmlImporter), - (col.tr.importing_pauker_18_lesson_paugz(), PaukerImporter), ] anki.hooks.importing_importers(importers) return importers diff --git a/pylib/anki/importing/pauker.py b/pylib/anki/importing/pauker.py deleted file mode 100644 index ea5c45082..000000000 --- a/pylib/anki/importing/pauker.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright: Andreas Klauer -# License: BSD-3 - -# pylint: disable=invalid-name - -import gzip -import html -import math -import random -import time -import xml.etree.ElementTree as ET - -from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter -from anki.stdmodels import _legacy_add_forward_reverse - -ONE_DAY = 60 * 60 * 24 - - -class PaukerImporter(NoteImporter): - """Import Pauker 1.8 Lesson (*.pau.gz)""" - - needMapper = False - allowHTML = True - - def run(self): - model = _legacy_add_forward_reverse(self.col) - model["name"] = "Pauker" - self.col.models.save(model, updateReqs=False) - self.col.models.set_current(model) - self.model = model - self.initMapping() - NoteImporter.run(self) - - def fields(self): - """Pauker is Front/Back""" - return 2 - - def foreignNotes(self): - """Build and return a list of notes.""" - notes = [] - - try: - f = gzip.open(self.file) - tree = ET.parse(f) # type: ignore - lesson = tree.getroot() - assert lesson.tag == "Lesson" - finally: - f.close() - - index = -4 - - for batch in lesson.findall("./Batch"): - index += 1 - - for card in batch.findall("./Card"): - # Create a note for this card. - front = card.findtext("./FrontSide/Text") - back = card.findtext("./ReverseSide/Text") - note = ForeignNote() - assert front and back - note.fields = [ - html.escape(x.strip()) - .replace("\n", "
") - .replace(" ", "  ") - for x in [front, back] - ] - notes.append(note) - - # Determine due date for cards. - frontdue = card.find("./FrontSide[@LearnedTimestamp]") - backdue = card.find("./ReverseSide[@Batch][@LearnedTimestamp]") - - if frontdue is not None: - note.cards[0] = self._learnedCard( - index, int(frontdue.attrib["LearnedTimestamp"]) - ) - - if backdue is not None: - note.cards[1] = self._learnedCard( - int(backdue.attrib["Batch"]), - int(backdue.attrib["LearnedTimestamp"]), - ) - - return notes - - def _learnedCard(self, batch, timestamp): - ivl = math.exp(batch) - now = time.time() - due = ivl - (now - timestamp / 1000.0) / ONE_DAY - fc = ForeignCard() - fc.due = self.col.sched.today + int(due + 0.5) - fc.ivl = random.randint(int(ivl * 0.90), int(ivl + 0.5)) - fc.factor = random.randint(1500, 2500) - return fc diff --git a/pylib/anki/importing/supermemo_xml.py b/pylib/anki/importing/supermemo_xml.py deleted file mode 100644 index 202592c2e..000000000 --- a/pylib/anki/importing/supermemo_xml.py +++ /dev/null @@ -1,484 +0,0 @@ -# Copyright: petr.michalec@gmail.com -# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -# pytype: disable=attribute-error -# type: ignore -# pylint: disable=C -from __future__ import annotations - -import re -import sys -import time -import unicodedata -from string import capwords -from xml.dom import minidom -from xml.dom.minidom import Element, Text - -from anki.collection import Collection -from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter -from anki.stdmodels import _legacy_add_basic_model - - -class SmartDict(dict): - """ - See http://www.peterbe.com/plog/SmartDict - Copyright 2005, Peter Bengtsson, peter@fry-it.com - 0BSD - - A smart dict can be instantiated either from a pythonic dict - or an instance object (eg. SQL recordsets) but it ensures that you can - do all the convenient lookups such as x.first_name, x['first_name'] or - x.get('first_name'). - """ - - def __init__(self, *a, **kw) -> None: - if a: - if isinstance(type(a[0]), dict): - kw.update(a[0]) - elif isinstance(type(a[0]), object): - kw.update(a[0].__dict__) - elif hasattr(a[0], "__class__") and a[0].__class__.__name__ == "SmartDict": - kw.update(a[0].__dict__) - - dict.__init__(self, **kw) - self.__dict__ = self - - -class SuperMemoElement(SmartDict): - "SmartDict wrapper to store SM Element data" - - def __init__(self, *a, **kw) -> None: - SmartDict.__init__(self, *a, **kw) - # default content - self.__dict__["lTitle"] = None - self.__dict__["Title"] = None - self.__dict__["Question"] = None - self.__dict__["Answer"] = None - self.__dict__["Count"] = None - self.__dict__["Type"] = None - self.__dict__["ID"] = None - self.__dict__["Interval"] = None - self.__dict__["Lapses"] = None - self.__dict__["Repetitions"] = None - self.__dict__["LastRepetiton"] = None - self.__dict__["AFactor"] = None - self.__dict__["UFactor"] = None - - -# This is an AnkiImporter -class SupermemoXmlImporter(NoteImporter): - needMapper = False - allowHTML = True - - """ - Supermemo XML export's to Anki parser. - Goes through a SM collection and fetch all elements. - - My SM collection was a big mess where topics and items were mixed. - I was unable to parse my content in a regular way like for loop on - minidom.getElementsByTagName() etc. My collection had also an - limitation, topics were splited into branches with max 100 items - on each. Learning themes were in deep structure. I wanted to have - full title on each element to be stored in tags. - - Code should be upgrade to support importing of SM2006 exports. - """ - - def __init__(self, col: Collection, file: str) -> None: - """Initialize internal variables. - Pameters to be exposed to GUI are stored in self.META""" - NoteImporter.__init__(self, col, file) - m = _legacy_add_basic_model(self.col) - m["name"] = "Supermemo" - self.col.models.save(m) - self.initMapping() - - self.lines = None - self.numFields = int(2) - - # SmXmlParse VARIABLES - self.xmldoc = None - self.pieces = [] - self.cntBuf = [] # to store last parsed data - self.cntElm = [] # to store SM Elements data - self.cntCol = [] # to store SM Colections data - - # store some meta info related to parse algorithm - # SmartDict works like dict / class wrapper - self.cntMeta = SmartDict() - self.cntMeta.popTitles = False - self.cntMeta.title = [] - - # META stores controls of import script, should be - # exposed to import dialog. These are default values. - self.META = SmartDict() - self.META.resetLearningData = False # implemented - self.META.onlyMemorizedItems = False # implemented - self.META.loggerLevel = 2 # implemented 0no,1info,2error,3debug - self.META.tagAllTopics = True - self.META.pathsToBeTagged = [ - "English for beginners", - "Advanced English 97", - "Phrasal Verbs", - ] # path patterns to be tagged - in gui entered like 'Advanced English 97|My Vocablary' - self.META.tagMemorizedItems = True # implemented - self.META.logToStdOutput = False # implemented - - self.notes = [] - - ## TOOLS - - def _fudgeText(self, text: str) -> str: - "Replace sm syntax to Anki syntax" - text = text.replace("\n\r", "
") - text = text.replace("\n", "
") - return text - - def _unicode2ascii(self, str: str) -> str: - "Remove diacritic punctuation from strings (titles)" - return "".join( - [ - c - for c in unicodedata.normalize("NFKD", str) - if not unicodedata.combining(c) - ] - ) - - def _decode_htmlescapes(self, html: str) -> str: - """Unescape HTML code.""" - # In case of bad formatted html you can import MinimalSoup etc.. see BeautifulSoup source code - from bs4 import BeautifulSoup - - # my sm2004 also ecaped & char in escaped sequences. - html = re.sub("&", "&", html) - - # https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx - if html.find(">") < 0: - return html - - # unescaped solitary chars < or > that were ok for minidom confuse btfl soup - # html = re.sub(u'>',u'>',html) - # html = re.sub(u'<',u'<',html) - - return str(BeautifulSoup(html, "html.parser")) - - def _afactor2efactor(self, af: float) -> float: - # Adapted from - - # Ranges for A-factors and E-factors - af_min = 1.2 - af_max = 6.9 - ef_min = 1.3 - ef_max = 3.3 - - # Sanity checks for the A-factor - if af < af_min: - af = af_min - elif af > af_max: - af = af_max - - # Scale af to the range 0..1 - af_scaled = (af - af_min) / (af_max - af_min) - # Rescale to the interval ef_min..ef_max - ef = ef_min + af_scaled * (ef_max - ef_min) - - return ef - - ## DEFAULT IMPORTER METHODS - - def foreignNotes(self) -> list[ForeignNote]: - # Load file and parse it by minidom - self.loadSource(self.file) - - # Migrating content / time consuming part - # addItemToCards is called for each sm element - self.logger("Parsing started.") - self.parse() - self.logger("Parsing done.") - - # Return imported cards - self.total = len(self.notes) - self.log.append("%d cards imported." % self.total) - return self.notes - - def fields(self) -> int: - return 2 - - ## PARSER METHODS - - def addItemToCards(self, item: SuperMemoElement) -> None: - "This method actually do conversion" - - # new anki card - note = ForeignNote() - - # clean Q and A - note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Question))) - note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Answer))) - note.tags = [] - - # pre-process scheduling data - # convert learning data - if ( - not self.META.resetLearningData - and int(item.Interval) >= 1 - and getattr(item, "LastRepetition", None) - ): - # migration of LearningData algorithm - tLastrep = time.mktime(time.strptime(item.LastRepetition, "%d.%m.%Y")) - tToday = time.time() - card = ForeignCard() - card.ivl = int(item.Interval) - card.lapses = int(item.Lapses) - card.reps = int(item.Repetitions) + int(item.Lapses) - nextDue = tLastrep + (float(item.Interval) * 86400.0) - remDays = int((nextDue - time.time()) / 86400) - card.due = self.col.sched.today + remDays - card.factor = int( - self._afactor2efactor(float(item.AFactor.replace(",", "."))) * 1000 - ) - note.cards[0] = card - - # categories & tags - # it's worth to have every theme (tree structure of sm collection) stored in tags, but sometimes not - # you can deceide if you are going to tag all toppics or just that containing some pattern - tTaggTitle = False - for pattern in self.META.pathsToBeTagged: - if ( - item.lTitle is not None - and pattern.lower() in " ".join(item.lTitle).lower() - ): - tTaggTitle = True - break - if tTaggTitle or self.META.tagAllTopics: - # normalize - remove diacritic punctuation from unicode chars to ascii - item.lTitle = [self._unicode2ascii(topic) for topic in item.lTitle] - - # Transform xyz / aaa / bbb / ccc on Title path to Tag xyzAaaBbbCcc - # clean things like [999] or [111-2222] from title path, example: xyz / [1000-1200] zyx / xyz - # clean whitespaces - # set Capital letters for first char of the word - tmp = list( - {re.sub(r"(\[[0-9]+\])", " ", i).replace("_", " ") for i in item.lTitle} - ) - tmp = list({re.sub(r"(\W)", " ", i) for i in tmp}) - tmp = list({re.sub("^[0-9 ]+$", "", i) for i in tmp}) - tmp = list({capwords(i).replace(" ", "") for i in tmp}) - tags = [j[0].lower() + j[1:] for j in tmp if j.strip() != ""] - - note.tags += tags - - if self.META.tagMemorizedItems and int(item.Interval) > 0: - note.tags.append("Memorized") - - self.logger("Element tags\t- " + repr(note.tags), level=3) - - self.notes.append(note) - - def logger(self, text: str, level: int = 1) -> None: - "Wrapper for Anki logger" - - dLevels = {0: "", 1: "Info", 2: "Verbose", 3: "Debug"} - if level <= self.META.loggerLevel: - # self.deck.updateProgress(_(text)) - - if self.META.logToStdOutput: - print( - self.__class__.__name__ - + " - " - + dLevels[level].ljust(9) - + " -\t" - + text - ) - - # OPEN AND LOAD - def openAnything(self, source): - """Open any source / actually only opening of files is used - @return an open handle which must be closed after use, i.e., handle.close()""" - - if source == "-": - return sys.stdin - - # try to open with urllib (if source is http, ftp, or file URL) - import urllib.error - import urllib.parse - import urllib.request - - try: - return urllib.request.urlopen(source) - except OSError: - pass - - # try to open with native open function (if source is pathname) - try: - return open(source, encoding="utf8") - except OSError: - pass - - # treat source as string - import io - - return io.StringIO(str(source)) - - def loadSource(self, source: str) -> None: - """Load source file and parse with xml.dom.minidom""" - self.source = source - self.logger("Load started...") - sock = open(self.source, encoding="utf8") - self.xmldoc = minidom.parse(sock).documentElement - sock.close() - self.logger("Load done.") - - # PARSE - def parse(self, node: Text | Element | None = None) -> None: - "Parse method - parses document elements" - - if node is None and self.xmldoc is not None: - node = self.xmldoc - - _method = "parse_%s" % node.__class__.__name__ - if hasattr(self, _method): - parseMethod = getattr(self, _method) - parseMethod(node) - else: - self.logger("No handler for method %s" % _method, level=3) - - def parse_Document(self, node): - "Parse XML document" - - self.parse(node.documentElement) - - def parse_Element(self, node: Element) -> None: - "Parse XML element" - - _method = "do_%s" % node.tagName - if hasattr(self, _method): - handlerMethod = getattr(self, _method) - handlerMethod(node) - else: - self.logger("No handler for method %s" % _method, level=3) - # print traceback.print_exc() - - def parse_Text(self, node: Text) -> None: - "Parse text inside elements. Text is stored into local buffer." - - text = node.data - self.cntBuf.append(text) - - # def parse_Comment(self, node): - # """ - # Source can contain XML comments, but we ignore them - # """ - # pass - - # DO - def do_SuperMemoCollection(self, node: Element) -> None: - "Process SM Collection" - - for child in node.childNodes: - self.parse(child) - - def do_SuperMemoElement(self, node: Element) -> None: - "Process SM Element (Type - Title,Topics)" - - self.logger("=" * 45, level=3) - - self.cntElm.append(SuperMemoElement()) - self.cntElm[-1]["lTitle"] = self.cntMeta["title"] - - # parse all child elements - for child in node.childNodes: - self.parse(child) - - # strip all saved strings, just for sure - for key in list(self.cntElm[-1].keys()): - if hasattr(self.cntElm[-1][key], "strip"): - self.cntElm[-1][key] = self.cntElm[-1][key].strip() - - # pop current element - smel = self.cntElm.pop() - - # Process cntElm if is valid Item (and not an Topic etc..) - # if smel.Lapses != None and smel.Interval != None and smel.Question != None and smel.Answer != None: - if smel.Title is None and smel.Question is not None and smel.Answer is not None: - if smel.Answer.strip() != "" and smel.Question.strip() != "": - # migrate only memorized otherway skip/continue - if self.META.onlyMemorizedItems and not (int(smel.Interval) > 0): - self.logger("Element skipped \t- not memorized ...", level=3) - else: - # import sm element data to Anki - self.addItemToCards(smel) - self.logger("Import element \t- " + smel["Question"], level=3) - - # print element - self.logger("-" * 45, level=3) - for key in list(smel.keys()): - self.logger( - "\t{} {}".format((key + ":").ljust(15), smel[key]), level=3 - ) - else: - self.logger("Element skipped \t- no valid Q and A ...", level=3) - - else: - # now we know that item was topic - # parsing of whole node is now finished - - # test if it's really topic - if smel.Title is not None: - # remove topic from title list - t = self.cntMeta["title"].pop() - self.logger("End of topic \t- %s" % (t), level=2) - - def do_Content(self, node: Element) -> None: - "Process SM element Content" - - for child in node.childNodes: - if hasattr(child, "tagName") and child.firstChild is not None: - self.cntElm[-1][child.tagName] = child.firstChild.data - - def do_LearningData(self, node: Element) -> None: - "Process SM element LearningData" - - for child in node.childNodes: - if hasattr(child, "tagName") and child.firstChild is not None: - self.cntElm[-1][child.tagName] = child.firstChild.data - - # It's being processed in do_Content now - # def do_Question(self, node): - # for child in node.childNodes: self.parse(child) - # self.cntElm[-1][node.tagName]=self.cntBuf.pop() - - # It's being processed in do_Content now - # def do_Answer(self, node): - # for child in node.childNodes: self.parse(child) - # self.cntElm[-1][node.tagName]=self.cntBuf.pop() - - def do_Title(self, node: Element) -> None: - "Process SM element Title" - - t = self._decode_htmlescapes(node.firstChild.data) - self.cntElm[-1][node.tagName] = t - self.cntMeta["title"].append(t) - self.cntElm[-1]["lTitle"] = self.cntMeta["title"] - self.logger("Start of topic \t- " + " / ".join(self.cntMeta["title"]), level=2) - - def do_Type(self, node: Element) -> None: - "Process SM element Type" - - if len(self.cntBuf) >= 1: - self.cntElm[-1][node.tagName] = self.cntBuf.pop() - - -# if __name__ == '__main__': - -# for testing you can start it standalone - -# file = u'/home/epcim/hg2g/dev/python/sm2anki/ADVENG2EXP.xxe.esc.zaloha_FINAL.xml' -# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_noOEM.xml' -# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_oem_1250.xml' -# file = str(sys.argv[1]) -# impo = SupermemoXmlImporter(Deck(),file) -# impo.foreignCards() - -# sys.exit(1) - -# vim: ts=4 sts=2 ft=python diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml index d6d41f970..70bb7aba2 100644 --- a/pylib/pyproject.toml +++ b/pylib/pyproject.toml @@ -4,7 +4,6 @@ dynamic = ["version"] requires-python = ">=3.9" license = "AGPL-3.0-or-later" dependencies = [ - "beautifulsoup4", "decorator", "markdown", "orjson", diff --git a/pylib/tests/test_importing.py b/pylib/tests/test_importing.py index 191de51f4..b7b63de26 100644 --- a/pylib/tests/test_importing.py +++ b/pylib/tests/test_importing.py @@ -13,7 +13,6 @@ from anki.importing import ( Anki2Importer, AnkiPackageImporter, MnemosyneImporter, - SupermemoXmlImporter, TextImporter, ) from tests.shared import getEmptyCol, getUpgradeDeckPath @@ -306,22 +305,6 @@ def test_csv_tag_only_if_modified(): col.close() -@pytest.mark.filterwarnings("ignore:Using or importing the ABCs") -def test_supermemo_xml_01_unicode(): - col = getEmptyCol() - file = str(os.path.join(testDir, "support", "supermemo1.xml")) - i = SupermemoXmlImporter(col, file) - # i.META.logToStdOutput = True - i.run() - assert i.total == 1 - cid = col.db.scalar("select id from cards") - c = col.get_card(cid) - # Applies A Factor-to-E Factor conversion - assert c.factor == 2879 - assert c.reps == 7 - col.close() - - def test_mnemo(): col = getEmptyCol() file = str(os.path.join(testDir, "support", "mnemo.db")) diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index 3a3c06f2c..2650ec648 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -21,12 +21,7 @@ use walkdir::WalkDir; const NONSTANDARD_HEADER: &[&str] = &[ "./pylib/anki/_vendor/stringcase.py", - "./pylib/anki/importing/pauker.py", - "./pylib/anki/importing/supermemo_xml.py", "./pylib/anki/statsbg.py", - "./pylib/tools/protoc-gen-mypy.py", - "./python/pyqt/install.py", - "./python/write_wheel.py", "./qt/aqt/mpv.py", "./qt/aqt/winpaths.py", ]; diff --git a/uv.lock b/uv.lock index ce2f9536d..62c66e494 100644 --- a/uv.lock +++ b/uv.lock @@ -51,7 +51,6 @@ wheels = [ name = "anki" source = { editable = "pylib" } dependencies = [ - { name = "beautifulsoup4" }, { name = "decorator" }, { name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "markdown" }, @@ -64,7 +63,6 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "beautifulsoup4" }, { name = "decorator" }, { name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, { name = "markdown" }, From de7de82f769f58d20c814fbdb5274aa366e887ef Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Jun 2025 16:21:39 +0700 Subject: [PATCH 030/197] Refactor launcher + various tweaks - Launcher can now be accessed via Tools>Upgrade/Downgrade - Anki closes automatically on update - When launcher not available, show update link like in the past - It appears that access to the modern console host requires an app to be built with the windows console subsystem, so we introduce an extra anki-console.exe binary to relaunch ourselves with. Solves https://forums.ankiweb.net/t/new-online-installer-launcher/62745/50 - Windows now requires you to close the terminal like on a Mac, as I couldn't figure out how to have it automatically close. Suggestions welcome! - Reduce the amount of duplicate/near-duplicate code in the various platform files, and improve readability - Add a helper to install the current code into the launcher env - Fix cargo test failing to build on ARM64 Windows --- Cargo.lock | 1 + Cargo.toml | 2 +- ftl/qt/qt-accel.ftl | 1 + ftl/qt/qt-misc.ftl | 2 +- qt/aqt/forms/main.ui | 10 +- qt/aqt/main.py | 13 ++ qt/aqt/update.py | 54 ++++++-- qt/launcher/Cargo.toml | 5 + qt/launcher/src/bin/anki_console.rs | 58 +++++++++ qt/launcher/src/bin/build_win.rs | 10 +- qt/launcher/src/main.rs | 187 +++++++++++++++++----------- qt/launcher/src/platform/mac.rs | 70 ++--------- qt/launcher/src/platform/mod.rs | 108 ++++++++++++++-- qt/launcher/src/platform/unix.rs | 74 ----------- qt/launcher/src/platform/windows.rs | 152 +++++----------------- qt/launcher/win/anki-console.bat | 5 - qt/launcher/win/build.bat | 9 +- rslib/sync/Cargo.toml | 5 + tools/update-launcher-env | 15 +++ tools/update-launcher-env.bat | 8 ++ 20 files changed, 435 insertions(+), 354 deletions(-) create mode 100644 qt/launcher/src/bin/anki_console.rs delete mode 100644 qt/launcher/win/anki-console.bat create mode 100755 tools/update-launcher-env create mode 100644 tools/update-launcher-env.bat diff --git a/Cargo.lock b/Cargo.lock index 03f9e63c8..04e7c6c76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3543,6 +3543,7 @@ dependencies = [ "anki_io", "anki_process", "anyhow", + "camino", "dirs 6.0.0", "embed-resource", "libc", diff --git a/Cargo.toml b/Cargo.toml index d2ce2ce2a..61cca8649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0" unicode-normalization = "0.1.24" walkdir = "2.5.0" which = "8.0.0" -winapi = { version = "0.3", features = ["wincon", "errhandlingapi", "consoleapi"] } +winapi = { version = "0.3", features = ["wincon"] } windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] } wiremock = "0.6.3" xz2 = "0.1.7" diff --git a/ftl/qt/qt-accel.ftl b/ftl/qt/qt-accel.ftl index 6c832b368..3ab54eb24 100644 --- a/ftl/qt/qt-accel.ftl +++ b/ftl/qt/qt-accel.ftl @@ -46,3 +46,4 @@ qt-accel-zoom-editor-in = Zoom Editor &In qt-accel-zoom-editor-out = Zoom Editor &Out qt-accel-create-backup = Create &Backup qt-accel-load-backup = &Revert to Backup +qt-accel-upgrade-downgrade = Upgrade/Downgrade diff --git a/ftl/qt/qt-misc.ftl b/ftl/qt/qt-misc.ftl index 60c22ef8b..d7bbef990 100644 --- a/ftl/qt/qt-misc.ftl +++ b/ftl/qt/qt-misc.ftl @@ -73,7 +73,7 @@ qt-misc-second = qt-misc-layout-auto-enabled = Responsive layout enabled qt-misc-layout-vertical-enabled = Vertical layout enabled qt-misc-layout-horizontal-enabled = Horizontal layout enabled -qt-misc-please-restart-to-update-anki = Please restart Anki to update to the latest version. +qt-misc-open-anki-launcher = Change to a different Anki version? ## deprecated- these strings will be removed in the future, and do not need ## to be translated diff --git a/qt/aqt/forms/main.ui b/qt/aqt/forms/main.ui index 0687d4ef3..bffc67ad0 100644 --- a/qt/aqt/forms/main.ui +++ b/qt/aqt/forms/main.ui @@ -46,7 +46,7 @@ 0 0 667 - 24 + 43 @@ -93,6 +93,7 @@ + @@ -130,7 +131,7 @@ Ctrl+P - QAction::PreferencesRole + QAction::MenuRole::PreferencesRole
@@ -283,6 +284,11 @@ qt_accel_load_backup + + + qt_accel_upgrade_downgrade + + diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 8e01208a4..b261cd34e 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1308,6 +1308,14 @@ title="{}" {}>{}""".format( def onPrefs(self) -> None: aqt.dialogs.open("Preferences", self) + def on_upgrade_downgrade(self) -> None: + if not askUser(tr.qt_misc_open_anki_launcher()): + return + + from aqt.update import update_and_restart + + update_and_restart() + def onNoteTypes(self) -> None: import aqt.models @@ -1389,6 +1397,8 @@ title="{}" {}>{}""".format( ########################################################################## def setupMenus(self) -> None: + from aqt.update import have_launcher + m = self.form # File @@ -1418,6 +1428,9 @@ title="{}" {}>{}""".format( qconnect(m.actionCreateFiltered.triggered, self.onCram) qconnect(m.actionEmptyCards.triggered, self.onEmptyCards) qconnect(m.actionNoteTypes.triggered, self.onNoteTypes) + qconnect(m.action_upgrade_downgrade.triggered, self.on_upgrade_downgrade) + if not have_launcher(): + m.action_upgrade_downgrade.setVisible(False) qconnect(m.actionPreferences.triggered, self.onPrefs) # View diff --git a/qt/aqt/update.py b/qt/aqt/update.py index d8e92426c..61fec8e6b 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -1,7 +1,11 @@ # 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 contextlib import os +import subprocess from pathlib import Path import aqt @@ -10,7 +14,7 @@ from anki.collection import CheckForUpdateResponse, Collection from anki.utils import dev_mode, int_time, int_version, is_mac, is_win, plat_desc from aqt.operations import QueryOp from aqt.qt import * -from aqt.utils import show_info, show_warning, showText, tr +from aqt.utils import openLink, show_warning, showText, tr def check_for_update() -> None: @@ -80,22 +84,56 @@ def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None: # ignore this update mw.pm.meta["suppressUpdate"] = ver elif ret == QMessageBox.StandardButton.Yes: - update_and_restart() + if have_launcher(): + update_and_restart() + else: + openLink(aqt.appWebsiteDownloadSection) + + +def _anki_launcher_path() -> str | None: + return os.getenv("ANKI_LAUNCHER") + + +def have_launcher() -> bool: + return _anki_launcher_path() is not None def update_and_restart() -> None: - """Download and install the update, then restart Anki.""" - update_on_next_run() - # todo: do this automatically in the future - show_info(tr.qt_misc_please_restart_to_update_anki()) + from aqt import mw + + launcher = _anki_launcher_path() + assert launcher + + _trigger_launcher_run() + + with contextlib.suppress(ResourceWarning): + env = os.environ.copy() + creationflags = 0 + if sys.platform == "win32": + creationflags = ( + subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS + ) + subprocess.Popen( + [launcher], + start_new_session=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=env, + creationflags=creationflags, + ) + + mw.app.quit() -def update_on_next_run() -> None: +def _trigger_launcher_run() -> None: """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" try: # Get the local data directory equivalent to Rust's dirs::data_local_dir() if is_win: - data_dir = Path(os.environ.get("LOCALAPPDATA", "")) + from .winpaths import get_local_appdata + + data_dir = Path(get_local_appdata()) elif is_mac: data_dir = Path.home() / "Library" / "Application Support" else: # Linux diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml index 45ca11e9b..735cd892e 100644 --- a/qt/launcher/Cargo.toml +++ b/qt/launcher/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true anki_io.workspace = true anki_process.workspace = true anyhow.workspace = true +camino.workspace = true dirs.workspace = true [target.'cfg(windows)'.dependencies] @@ -22,5 +23,9 @@ libc-stdhandle.workspace = true name = "build_win" path = "src/bin/build_win.rs" +[[bin]] +name = "anki-console" +path = "src/bin/anki_console.rs" + [target.'cfg(windows)'.build-dependencies] embed-resource.workspace = true diff --git a/qt/launcher/src/bin/anki_console.rs b/qt/launcher/src/bin/anki_console.rs new file mode 100644 index 000000000..596377ba1 --- /dev/null +++ b/qt/launcher/src/bin/anki_console.rs @@ -0,0 +1,58 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#![windows_subsystem = "console"] + +use std::env; +use std::io::stdin; +use std::process::Command; + +use anyhow::Context; +use anyhow::Result; + +fn main() { + if let Err(e) = run() { + eprintln!("Error: {:#}", e); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let current_exe = env::current_exe().context("Failed to get current executable path")?; + let exe_dir = current_exe + .parent() + .context("Failed to get executable directory")?; + + let anki_exe = exe_dir.join("anki.exe"); + + if !anki_exe.exists() { + anyhow::bail!("anki.exe not found in the same directory"); + } + + // Forward all command line arguments to anki.exe + let args: Vec = env::args().skip(1).collect(); + + let mut cmd = Command::new(&anki_exe); + cmd.args(&args); + + if std::env::var("ANKI_IMPLICIT_CONSOLE").is_err() { + // if directly invoked by the user, signal the launcher that the + // user wants a Python console + std::env::set_var("ANKI_CONSOLE", "1"); + } + + // Wait for the process to complete and forward its exit code + let status = cmd.status().context("Failed to execute anki.exe")?; + if !status.success() { + println!("\nPress enter to close."); + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + } + + if let Some(code) = status.code() { + std::process::exit(code); + } else { + // Process was terminated by a signal + std::process::exit(1); + } +} diff --git a/qt/launcher/src/bin/build_win.rs b/qt/launcher/src/bin/build_win.rs index ff385d9ea..3ad2c7ce0 100644 --- a/qt/launcher/src/bin/build_win.rs +++ b/qt/launcher/src/bin/build_win.rs @@ -114,6 +114,12 @@ fn copy_files(output_dir: &Path) -> Result<()> { let launcher_dst = output_dir.join("anki.exe"); copy_file(&launcher_src, &launcher_dst)?; + // Copy anki-console binary + let console_src = + PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/anki-console.exe"); + let console_dst = output_dir.join("anki-console.exe"); + copy_file(&console_src, &console_dst)?; + // Copy uv.exe and uvw.exe let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe"); let uv_dst = output_dir.join("uv.exe"); @@ -133,14 +139,12 @@ fn copy_files(output_dir: &Path) -> Result<()> { output_dir.join(".python-version"), )?; - // Copy anki-console.bat - copy_file("anki-console.bat", output_dir.join("anki-console.bat"))?; - Ok(()) } fn sign_binaries(output_dir: &Path) -> Result<()> { sign_file(&output_dir.join("anki.exe"))?; + sign_file(&output_dir.join("anki-console.exe"))?; sign_file(&output_dir.join("uv.exe"))?; Ok(()) } diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 2ad3ac00c..b2535f410 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -16,21 +16,34 @@ use anki_io::modified_time; use anki_io::read_file; use anki_io::remove_file; use anki_io::write_file; +use anki_io::ToUtf8Path; use anki_process::CommandExt; use anyhow::Context; use anyhow::Result; use crate::platform::ensure_terminal_shown; -use crate::platform::exec_anki; -use crate::platform::get_anki_binary_path; use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_uv_binary_name; -use crate::platform::handle_first_launch; -use crate::platform::initial_terminal_setup; -use crate::platform::launch_anki_detached; +use crate::platform::launch_anki_after_update; +use crate::platform::launch_anki_normally; mod platform; +// todo: -c appearing as app name now + +struct State { + has_existing_install: bool, + prerelease_marker: std::path::PathBuf, + uv_install_root: std::path::PathBuf, + uv_path: std::path::PathBuf, + user_pyproject_path: std::path::PathBuf, + user_python_version_path: std::path::PathBuf, + dist_pyproject_path: std::path::PathBuf, + dist_python_version_path: std::path::PathBuf, + uv_lock_path: std::path::PathBuf, + sync_complete_marker: std::path::PathBuf, +} + #[derive(Debug, Clone)] pub enum VersionKind { PyOxidizer(String), @@ -46,16 +59,8 @@ pub enum MainMenuChoice { Quit, } -#[derive(Debug, Clone, Default)] -pub struct Config { - pub show_console: bool, -} - fn main() { if let Err(e) = run() { - let mut config: Config = Config::default(); - initial_terminal_setup(&mut config); - eprintln!("Error: {:#}", e); eprintln!("Press enter to close..."); let mut input = String::new(); @@ -66,58 +71,92 @@ fn main() { } fn run() -> Result<()> { - let mut config: Config = Config::default(); - let uv_install_root = dirs::data_local_dir() .context("Unable to determine data_dir")? .join("AnkiProgramFiles"); - let sync_complete_marker = uv_install_root.join(".sync_complete"); - let prerelease_marker = uv_install_root.join("prerelease"); let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; - let dist_pyproject_path = resources_dir.join("pyproject.toml"); - let user_pyproject_path = uv_install_root.join("pyproject.toml"); - let dist_python_version_path = resources_dir.join(".python-version"); - let user_python_version_path = uv_install_root.join(".python-version"); - let uv_lock_path = uv_install_root.join("uv.lock"); - let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name()); + + let state = State { + has_existing_install: uv_install_root.join(".sync_complete").exists(), + prerelease_marker: uv_install_root.join("prerelease"), + uv_install_root: uv_install_root.clone(), + uv_path: exe_dir.join(get_uv_binary_name()), + user_pyproject_path: uv_install_root.join("pyproject.toml"), + user_python_version_path: uv_install_root.join(".python-version"), + dist_pyproject_path: resources_dir.join("pyproject.toml"), + dist_python_version_path: resources_dir.join(".python-version"), + uv_lock_path: uv_install_root.join("uv.lock"), + sync_complete_marker: uv_install_root.join(".sync_complete"), + }; // Create install directory and copy project files in - create_dir_all(&uv_install_root)?; - let had_user_pyproj = user_pyproject_path.exists(); + create_dir_all(&state.uv_install_root)?; + let had_user_pyproj = state.user_pyproject_path.exists(); if !had_user_pyproj { // during initial launcher testing, enable betas by default - write_file(&prerelease_marker, "")?; + write_file(&state.prerelease_marker, "")?; } - copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?; - copy_if_newer(&dist_python_version_path, &user_python_version_path)?; + copy_if_newer(&state.dist_pyproject_path, &state.user_pyproject_path)?; + copy_if_newer( + &state.dist_python_version_path, + &state.user_python_version_path, + )?; - let pyproject_has_changed = !sync_complete_marker.exists() || { - let pyproject_toml_time = modified_time(&user_pyproject_path)?; - let sync_complete_time = modified_time(&sync_complete_marker)?; + let pyproject_has_changed = !state.sync_complete_marker.exists() || { + let pyproject_toml_time = modified_time(&state.user_pyproject_path)?; + let sync_complete_time = modified_time(&state.sync_complete_marker)?; Ok::(pyproject_toml_time > sync_complete_time) } .unwrap_or(true); if !pyproject_has_changed { - // If venv is already up to date, exec as normal - initial_terminal_setup(&mut config); - let anki_bin = get_anki_binary_path(&uv_install_root); - exec_anki(&anki_bin, &config)?; + // If venv is already up to date, launch Anki normally + let args: Vec = std::env::args().skip(1).collect(); + let cmd = build_python_command(&state.uv_install_root, &args)?; + launch_anki_normally(cmd)?; return Ok(()); } - // we'll need to launch uv; reinvoke ourselves in a terminal so the user can see + // If we weren't in a terminal, respawn ourselves in one ensure_terminal_shown()?; + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top println!("\x1B[1mAnki Launcher\x1B[0m\n"); - // Check if there's an existing installation before removing marker - let has_existing_install = sync_complete_marker.exists(); + main_menu_loop(&state)?; + // Write marker file to indicate we've completed the sync process + write_sync_marker(&state.sync_complete_marker)?; + + #[cfg(target_os = "macos")] + { + let cmd = build_python_command(&state.uv_install_root, &[])?; + platform::mac::prepare_for_launch_after_update(cmd)?; + } + + if cfg!(unix) && !cfg!(target_os = "macos") { + println!("\nPress enter to start Anki."); + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + } else { + // on Windows/macOS, the user needs to close the terminal/console + // currently, but ideas on how we can avoid this would be good! + println!("Anki will start shortly."); + println!("\x1B[1mYou can close this window.\x1B[0m\n"); + } + + let cmd = build_python_command(&state.uv_install_root, &[])?; + launch_anki_after_update(cmd)?; + + Ok(()) +} + +fn main_menu_loop(state: &State) -> Result<()> { loop { - let menu_choice = get_main_menu_choice(has_existing_install, &prerelease_marker); + let menu_choice = + get_main_menu_choice(state.has_existing_install, &state.prerelease_marker); match menu_choice { MainMenuChoice::Quit => std::process::exit(0), @@ -127,40 +166,40 @@ fn run() -> Result<()> { } MainMenuChoice::ToggleBetas => { // Toggle beta prerelease file - if prerelease_marker.exists() { - let _ = remove_file(&prerelease_marker); + if state.prerelease_marker.exists() { + let _ = remove_file(&state.prerelease_marker); println!("Beta releases disabled."); } else { - write_file(&prerelease_marker, "")?; + write_file(&state.prerelease_marker, "")?; println!("Beta releases enabled."); } println!(); continue; } - _ => { + choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => { // For other choices, update project files and sync update_pyproject_for_version( - menu_choice.clone(), - dist_pyproject_path.clone(), - user_pyproject_path.clone(), - dist_python_version_path.clone(), - user_python_version_path.clone(), + choice, + state.dist_pyproject_path.clone(), + state.user_pyproject_path.clone(), + state.dist_python_version_path.clone(), + state.user_python_version_path.clone(), )?; // Remove sync marker before attempting sync - let _ = remove_file(&sync_complete_marker); + let _ = remove_file(&state.sync_complete_marker); // Sync the venv - let mut command = Command::new(&uv_path); - command.current_dir(&uv_install_root).args([ + let mut command = Command::new(&state.uv_path); + command.current_dir(&state.uv_install_root).args([ "sync", "--upgrade", "--managed-python", ]); // Add python version if .python-version file exists - if user_python_version_path.exists() { - let python_version = read_file(&user_python_version_path)?; + if state.user_python_version_path.exists() { + let python_version = read_file(&state.user_python_version_path)?; let python_version_str = String::from_utf8(python_version) .context("Invalid UTF-8 in .python-version")?; let python_version_trimmed = python_version_str.trim(); @@ -168,7 +207,7 @@ fn run() -> Result<()> { } // Set UV_PRERELEASE=allow if beta mode is enabled - if prerelease_marker.exists() { + if state.prerelease_marker.exists() { command.env("UV_PRERELEASE", "allow"); } @@ -182,7 +221,7 @@ fn run() -> Result<()> { Err(e) => { // If sync fails due to things like a missing wheel on pypi, // we need to remove the lockfile or uv will cache the bad result. - let _ = remove_file(&uv_lock_path); + let _ = remove_file(&state.uv_lock_path); println!("Install failed: {:#}", e); println!(); continue; @@ -191,22 +230,6 @@ fn run() -> Result<()> { } } } - - // Write marker file to indicate we've completed the sync process - write_sync_marker(&sync_complete_marker)?; - - // First launch - let anki_bin = get_anki_binary_path(&uv_install_root); - handle_first_launch(&anki_bin)?; - - println!("\nPress enter to start Anki."); - - let mut input = String::new(); - let _ = stdin().read_line(&mut input); - - // Then launch the binary as detached subprocess so the terminal can close - launch_anki_detached(&anki_bin, &config)?; - Ok(()) } @@ -403,3 +426,25 @@ fn parse_version_kind(version: &str) -> Option { Some(VersionKind::Uv(version.to_string())) } } + +fn build_python_command(uv_install_root: &std::path::Path, args: &[String]) -> Result { + let python_exe = if cfg!(target_os = "windows") { + let show_console = std::env::var("ANKI_CONSOLE").is_ok(); + if show_console { + uv_install_root.join(".venv/Scripts/python.exe") + } else { + uv_install_root.join(".venv/Scripts/pythonw.exe") + } + } else { + uv_install_root.join(".venv/bin/python") + }; + + let mut cmd = Command::new(python_exe); + cmd.args(["-c", "import aqt; aqt.run()"]); + cmd.args(args); + // tell the Python code it was invoked by the launcher, and updating is + // available + cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + + Ok(cmd) +} diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs index 2369f538a..ab2c4b8fb 100644 --- a/qt/launcher/src/platform/mac.rs +++ b/qt/launcher/src/platform/mac.rs @@ -1,7 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::os::unix::process::CommandExt; +use std::io; +use std::io::Write; use std::process::Command; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -13,45 +14,7 @@ use anki_process::CommandExt as AnkiCommandExt; use anyhow::Context; use anyhow::Result; -// Re-export Unix functions that macOS uses -pub use super::unix::{ - ensure_terminal_shown, - exec_anki, - get_anki_binary_path, - initial_terminal_setup, -}; - -pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) -> Result<()> { - use std::process::Stdio; - - let child = Command::new(anki_bin) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .process_group(0) - .ensure_spawn()?; - std::mem::forget(child); - - println!("Anki will start shortly."); - println!("\x1B[1mYou can close this window.\x1B[0m\n"); - Ok(()) -} - -pub fn relaunch_in_terminal() -> Result<()> { - let current_exe = std::env::current_exe().context("Failed to get current executable path")?; - Command::new("open") - .args(["-a", "Terminal"]) - .arg(current_exe) - .ensure_spawn()?; - std::process::exit(0); -} - -pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { - use std::io::Write; - use std::io::{ - self, - }; - +pub fn prepare_for_launch_after_update(mut cmd: Command) -> Result<()> { // Pre-validate by running --version to trigger any Gatekeeper checks print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m"); io::stdout().flush().unwrap(); @@ -67,7 +30,7 @@ pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { } }); - let _ = Command::new(anki_bin) + let _ = cmd .env("ANKI_FIRST_RUN", "1") .arg("--version") .stdout(std::process::Stdio::null()) @@ -81,22 +44,11 @@ pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { Ok(()) } -pub fn get_exe_and_resources_dirs() -> Result<(std::path::PathBuf, std::path::PathBuf)> { - let exe_dir = std::env::current_exe() - .context("Failed to get current executable path")? - .parent() - .context("Failed to get executable directory")? - .to_owned(); - - let resources_dir = exe_dir - .parent() - .context("Failed to get parent directory")? - .join("Resources"); - - Ok((exe_dir, resources_dir)) -} - -pub fn get_uv_binary_name() -> &'static str { - // macOS uses standard uv binary name - "uv" +pub fn relaunch_in_terminal() -> Result<()> { + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + Command::new("open") + .args(["-a", "Terminal"]) + .arg(current_exe) + .ensure_spawn()?; + std::process::exit(0); } diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index bb7208abe..9dc74f8e9 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -1,18 +1,108 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] mod unix; #[cfg(target_os = "macos")] -mod mac; +pub mod mac; #[cfg(target_os = "windows")] -mod windows; +pub mod windows; -#[cfg(target_os = "macos")] -pub use mac::*; -#[cfg(all(unix, not(target_os = "macos")))] -pub use unix::*; -#[cfg(target_os = "windows")] -pub use windows::*; +use std::path::PathBuf; + +use anki_process::CommandExt; +use anyhow::Context; +use anyhow::Result; + +pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { + let exe_dir = std::env::current_exe() + .context("Failed to get current executable path")? + .parent() + .context("Failed to get executable directory")? + .to_owned(); + + let resources_dir = if cfg!(target_os = "macos") { + // On macOS, resources are in ../Resources relative to the executable + exe_dir + .parent() + .context("Failed to get parent directory")? + .join("Resources") + } else { + // On other platforms, resources are in the same directory as executable + exe_dir.clone() + }; + + Ok((exe_dir, resources_dir)) +} + +pub fn get_uv_binary_name() -> &'static str { + if cfg!(target_os = "windows") { + "uv.exe" + } else if cfg!(target_os = "macos") { + "uv" + } else if cfg!(target_arch = "x86_64") { + "uv.amd64" + } else { + "uv.arm64" + } +} + +pub fn launch_anki_after_update(mut cmd: std::process::Command) -> Result<()> { + use std::process::Stdio; + + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + const DETACHED_PROCESS: u32 = 0x00000008; + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS); + } + + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + + let child = cmd.ensure_spawn()?; + std::mem::forget(child); + + Ok(()) +} + +pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { + #[cfg(windows)] + { + crate::platform::windows::attach_to_parent_console(); + cmd.ensure_success()?; + } + #[cfg(unix)] + cmd.ensure_exec()?; + Ok(()) +} + +#[cfg(windows)] +pub use windows::ensure_terminal_shown; + +#[cfg(unix)] +pub fn ensure_terminal_shown() -> Result<()> { + use std::io::IsTerminal; + + let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout()); + if !stdout_is_terminal { + #[cfg(target_os = "macos")] + mac::relaunch_in_terminal()?; + #[cfg(not(target_os = "macos"))] + unix::relaunch_in_terminal()?; + } + + // Set terminal title to "Anki Launcher" + print!("\x1b]2;Anki Launcher\x07"); + Ok(()) +} diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs index 324bf5aa3..0430bfa96 100644 --- a/qt/launcher/src/platform/unix.rs +++ b/qt/launcher/src/platform/unix.rs @@ -1,36 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -#![allow(dead_code)] - -use std::io::IsTerminal; -use std::path::PathBuf; use std::process::Command; -use anki_process::CommandExt as AnkiCommandExt; use anyhow::Context; use anyhow::Result; -use crate::Config; - -pub fn initial_terminal_setup(_config: &mut Config) { - // No special terminal setup needed on Unix -} - -pub fn ensure_terminal_shown() -> Result<()> { - let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout()); - if !stdout_is_terminal { - // If launched from GUI, try to relaunch in a terminal - crate::platform::relaunch_in_terminal()?; - } - - // Set terminal title to "Anki Launcher" - print!("\x1b]2;Anki Launcher\x07"); - - Ok(()) -} - -#[cfg(not(target_os = "macos"))] pub fn relaunch_in_terminal() -> Result<()> { let current_exe = std::env::current_exe().context("Failed to get current executable path")?; @@ -72,52 +47,3 @@ pub fn relaunch_in_terminal() -> Result<()> { // If no terminal worked, continue without relaunching Ok(()) } - -pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> PathBuf { - uv_install_root.join(".venv/bin/anki") -} - -pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> { - // On non-macOS Unix systems, we don't need to detach since we never spawned a - // terminal - exec_anki(anki_bin, config) -} - -pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> { - // No special first launch handling needed for generic Unix systems - Ok(()) -} - -pub fn exec_anki(anki_bin: &std::path::Path, _config: &Config) -> Result<()> { - let args: Vec = std::env::args().skip(1).collect(); - Command::new(anki_bin) - .args(args) - .ensure_exec() - .map_err(anyhow::Error::new) -} - -pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { - let exe_dir = std::env::current_exe() - .context("Failed to get current executable path")? - .parent() - .context("Failed to get executable directory")? - .to_owned(); - - // On generic Unix systems, assume resources are in the same directory as - // executable - let resources_dir = exe_dir.clone(); - - Ok((exe_dir, resources_dir)) -} - -pub fn get_uv_binary_name() -> &'static str { - // Use architecture-specific uv binary for non-Mac Unix systems - if cfg!(target_arch = "x86_64") { - "uv.amd64" - } else if cfg!(target_arch = "aarch64") { - "uv.arm64" - } else { - // Fallback to generic uv for other architectures - "uv" - } -} diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index 4e3752d44..0a701c07a 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -1,82 +1,71 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::path::PathBuf; use std::process::Command; -use anki_process::CommandExt; use anyhow::Context; use anyhow::Result; -use winapi::um::consoleapi; -use winapi::um::errhandlingapi; use winapi::um::wincon; -use crate::Config; - pub fn ensure_terminal_shown() -> Result<()> { - ensure_console(); - // // Check if we're already relaunched to prevent infinite recursion - // if std::env::var("ANKI_LAUNCHER_IN_TERMINAL").is_ok() { - // println!("Recurse: Preparing to start Anki...\n"); - // return Ok(()); - // } - - // if have_console { - // } else { - // relaunch_in_cmd()?; - // } - Ok(()) -} - -fn ensure_console() { unsafe { if !wincon::GetConsoleWindow().is_null() { - return; + // We already have a console, no need to spawn anki-console.exe + return Ok(()); } - - if consoleapi::AllocConsole() == 0 { - let error_code = errhandlingapi::GetLastError(); - eprintln!("unexpected AllocConsole error: {}", error_code); - return; - } - - // This black magic triggers Windows to switch to the new - // ANSI-supporting console host, which is usually only available - // when the app is built with the console subsystem. - let _ = Command::new("cmd").args(&["/C", ""]).status(); } + + if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() { + // Successfully attached to parent console + reconnect_stdio_to_console(); + return Ok(()); + } + + // No console available, spawn anki-console.exe and exit + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + let exe_dir = current_exe + .parent() + .context("Failed to get executable directory")?; + + let console_exe = exe_dir.join("anki-console.exe"); + + if !console_exe.exists() { + anyhow::bail!("anki-console.exe not found in the same directory"); + } + + // Spawn anki-console.exe without waiting + Command::new(&console_exe) + .env("ANKI_IMPLICIT_CONSOLE", "1") + .spawn() + .context("Failed to spawn anki-console.exe")?; + + // Exit immediately after spawning + std::process::exit(0); } -fn attach_to_parent_console() -> bool { +pub fn attach_to_parent_console() -> bool { unsafe { if !wincon::GetConsoleWindow().is_null() { // we have a console already - println!("attach: already had console, false"); return false; } if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 { // successfully attached to parent - println!("attach: true"); + reconnect_stdio_to_console(); true } else { - println!("attach: false"); false } } } -/// If parent process has a console (eg cmd.exe), redirect our output there. -/// Sets config.show_console to true if successfully attached to console. -pub fn initial_terminal_setup(config: &mut Config) { +/// Reconnect stdin/stdout/stderr to the console. +fn reconnect_stdio_to_console() { use std::ffi::CString; use libc_stdhandle::*; - if !attach_to_parent_console() { - return; - } - // we launched without a console, so we'll need to open stdin/out/err let conin = CString::new("CONIN$").unwrap(); let conout = CString::new("CONOUT$").unwrap(); @@ -89,79 +78,4 @@ pub fn initial_terminal_setup(config: &mut Config) { libc::freopen(conout.as_ptr(), w.as_ptr(), stdout()); libc::freopen(conout.as_ptr(), w.as_ptr(), stderr()); } - - config.show_console = true; -} - -pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> std::path::PathBuf { - uv_install_root.join(".venv/Scripts/anki.exe") -} - -fn build_python_command( - anki_bin: &std::path::Path, - args: &[String], - config: &Config, -) -> Result { - let venv_dir = anki_bin - .parent() - .context("Failed to get venv Scripts directory")? - .parent() - .context("Failed to get venv directory")?; - - // Use python.exe if show_console is true, otherwise pythonw.exe - let python_exe = if config.show_console { - venv_dir.join("Scripts/python.exe") - } else { - venv_dir.join("Scripts/pythonw.exe") - }; - - let mut cmd = Command::new(python_exe); - cmd.args(["-c", "import aqt; aqt.run()"]); - cmd.args(args); - - Ok(cmd) -} - -pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> { - use std::os::windows::process::CommandExt; - use std::process::Stdio; - - const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; - const DETACHED_PROCESS: u32 = 0x00000008; - - let mut cmd = build_python_command(anki_bin, &[], config)?; - cmd.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS) - .ensure_spawn()?; - Ok(()) -} - -pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> { - Ok(()) -} - -pub fn exec_anki(anki_bin: &std::path::Path, config: &Config) -> Result<()> { - let args: Vec = std::env::args().skip(1).collect(); - let mut cmd = build_python_command(anki_bin, &args, config)?; - cmd.ensure_success()?; - Ok(()) -} - -pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { - let exe_dir = std::env::current_exe() - .context("Failed to get current executable path")? - .parent() - .context("Failed to get executable directory")? - .to_owned(); - - // On Windows, resources dir is the same as exe_dir - let resources_dir = exe_dir.clone(); - - Ok((exe_dir, resources_dir)) -} - -pub fn get_uv_binary_name() -> &'static str { - "uv.exe" } diff --git a/qt/launcher/win/anki-console.bat b/qt/launcher/win/anki-console.bat deleted file mode 100644 index a565fa7b6..000000000 --- a/qt/launcher/win/anki-console.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -"%~dp0"\anki %* -pause - - diff --git a/qt/launcher/win/build.bat b/qt/launcher/win/build.bat index b21831462..da574f210 100644 --- a/qt/launcher/win/build.bat +++ b/qt/launcher/win/build.bat @@ -1,5 +1,10 @@ @echo off -set CODESIGN=1 -REM set NO_COMPRESS=1 +if "%NOCOMP%"=="1" ( + set NO_COMPRESS=1 + set CODESIGN=0 +) else ( + set CODESIGN=1 + set NO_COMPRESS=0 +) cargo run --bin build_win diff --git a/rslib/sync/Cargo.toml b/rslib/sync/Cargo.toml index 7a8f8534a..d23b4f380 100644 --- a/rslib/sync/Cargo.toml +++ b/rslib/sync/Cargo.toml @@ -13,4 +13,9 @@ path = "main.rs" name = "anki-sync-server" [dependencies] + +[target.'cfg(windows)'.dependencies] +anki = { workspace = true, features = ["native-tls"] } + +[target.'cfg(not(windows))'.dependencies] anki = { workspace = true, features = ["rustls"] } diff --git a/tools/update-launcher-env b/tools/update-launcher-env new file mode 100755 index 000000000..c84569f55 --- /dev/null +++ b/tools/update-launcher-env @@ -0,0 +1,15 @@ +#!/bin/bash +# +# Install our latest anki/aqt code into the launcher venv + +set -e + +rm -rf out/wheels +./ninja wheels +if [[ "$OSTYPE" == "darwin"* ]]; then + export VIRTUAL_ENV=$HOME/Library/Application\ Support/AnkiProgramFiles/.venv +else + export VIRTUAL_ENV=$HOME/.local/share/AnkiProgramFiles/.venv +fi +./out/extracted/uv/uv pip install out/wheels/* + diff --git a/tools/update-launcher-env.bat b/tools/update-launcher-env.bat new file mode 100644 index 000000000..9b0b814c6 --- /dev/null +++ b/tools/update-launcher-env.bat @@ -0,0 +1,8 @@ +@echo off +rem +rem Install our latest anki/aqt code into the launcher venv + +rmdir /s /q out\wheels 2>nul +call tools\ninja wheels +set VIRTUAL_ENV=%LOCALAPPDATA%\AnkiProgramFiles\.venv +for %%f in (out\wheels\*.whl) do out\extracted\uv\uv pip install "%%f" \ No newline at end of file From bedab0a54b89819933dd1605fce105d9b60f639b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 27 Jun 2025 16:07:42 +0700 Subject: [PATCH 031/197] Drop psutil from wheel requirements We're not using it ourselves, and usage appears isolated to a couple of add-ons (notably ankirestart) --- pylib/pyproject.toml | 1 - qt/pyproject.toml | 1 - uv.lock | 19 ------------------- 3 files changed, 21 deletions(-) diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml index 70bb7aba2..23e10077f 100644 --- a/pylib/pyproject.toml +++ b/pylib/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "typing_extensions", # platform-specific dependencies "distro; sys_platform != 'darwin' and sys_platform != 'win32'", - "psutil; sys_platform == 'win32'", ] [build-system] diff --git a/qt/pyproject.toml b/qt/pyproject.toml index ab5f50263..101123718 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "requests", "send2trash", "waitress>=2.0.0", - "psutil; sys.platform == 'win32'", "pywin32; sys.platform == 'win32'", "anki-mac-helper; sys.platform == 'darwin'", "pip-system-certs!=5.1", diff --git a/uv.lock b/uv.lock index 62c66e494..868bb55e6 100644 --- a/uv.lock +++ b/uv.lock @@ -56,7 +56,6 @@ dependencies = [ { name = "markdown" }, { name = "orjson" }, { name = "protobuf" }, - { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "requests", extra = ["socks"] }, { name = "typing-extensions" }, ] @@ -68,7 +67,6 @@ requires-dist = [ { name = "markdown" }, { name = "orjson" }, { name = "protobuf", specifier = ">=4.21" }, - { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "requests", extras = ["socks"] }, { name = "typing-extensions" }, ] @@ -172,7 +170,6 @@ dependencies = [ { name = "jsonschema" }, { name = "pip-system-certs", version = "4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "pip-system-certs", version = "5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, - { name = "psutil", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "pyqt6", version = "6.7.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, @@ -229,7 +226,6 @@ requires-dist = [ { name = "flask-cors" }, { name = "jsonschema" }, { name = "pip-system-certs", specifier = "!=5.1" }, - { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "pyqt6", specifier = ">=6.2" }, { name = "pyqt6", marker = "extra == 'qt'", specifier = "==6.8.0" }, { name = "pyqt6", marker = "extra == 'qt66'", specifier = "==6.6.1" }, @@ -1003,21 +999,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] -[[package]] -name = "psutil" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, -] - [[package]] name = "pychromedevtools" version = "1.0.4" From ae6cf98f4083e75bb0ee69eee80480ece536225f Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 27 Jun 2025 10:28:35 +0100 Subject: [PATCH 032/197] Fix/unapplied scss (#4103) * deck options + change notetype * graphs * image occlusion * congrats * imports * ./check * style * $lib * delete unused index.ts files --- .../change-notetype/ChangeNotetypePage.svelte | 2 + .../change-notetype/change-notetype-base.scss | 6 +- ts/routes/change-notetype/index.ts | 41 ----------- ts/routes/congrats/CongratsPage.svelte | 2 + ts/routes/congrats/index.ts | 2 - ts/routes/deck-options/DeckOptionsPage.svelte | 2 + ts/routes/deck-options/index.ts | 65 ----------------- ts/routes/graphs/GraphsPage.svelte | 2 + ts/routes/graphs/index.ts | 72 ------------------- .../image-occlusion/ImageOcclusionPage.svelte | 2 + .../image-occlusion/image-occlusion-base.scss | 8 +-- ts/routes/image-occlusion/index.ts | 59 --------------- .../ImportAnkiPackagePage.svelte | 2 + .../import-anki-package-base.scss | 8 +-- ts/routes/import-anki-package/index.ts | 52 -------------- ts/routes/import-csv/ImportCsvPage.svelte | 2 + ts/routes/import-csv/import-csv-base.scss | 8 +-- ts/routes/import-csv/index.ts | 62 ---------------- ts/routes/import-page/ImportPage.svelte | 2 + ts/routes/import-page/import-page-base.scss | 6 +- ts/routes/import-page/index.ts | 54 -------------- 21 files changed, 34 insertions(+), 425 deletions(-) delete mode 100644 ts/routes/change-notetype/index.ts delete mode 100644 ts/routes/deck-options/index.ts delete mode 100644 ts/routes/graphs/index.ts delete mode 100644 ts/routes/image-occlusion/index.ts delete mode 100644 ts/routes/import-anki-package/index.ts delete mode 100644 ts/routes/import-csv/index.ts delete mode 100644 ts/routes/import-page/index.ts diff --git a/ts/routes/change-notetype/ChangeNotetypePage.svelte b/ts/routes/change-notetype/ChangeNotetypePage.svelte index db25aea2e..e07d088fb 100644 --- a/ts/routes/change-notetype/ChangeNotetypePage.svelte +++ b/ts/routes/change-notetype/ChangeNotetypePage.svelte @@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
-
-