diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py
index a54dae3c1..f3dd649b2 100644
--- a/pylib/anki/_backend/__init__.py
+++ b/pylib/anki/_backend/__init__.py
@@ -5,7 +5,7 @@ from __future__ import annotations
import sys
import traceback
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+from typing import Any, Sequence, Union
from weakref import ref
from markdown import markdown
@@ -59,7 +59,7 @@ class RustBackend(RustBackendGenerated):
def __init__(
self,
- langs: Optional[List[str]] = None,
+ langs: list[str] | None = None,
server: bool = False,
) -> None:
# pick up global defaults if not provided
@@ -74,12 +74,12 @@ class RustBackend(RustBackendGenerated):
def db_query(
self, sql: str, args: Sequence[ValueForDB], first_row_only: bool
- ) -> List[DBRow]:
+ ) -> list[DBRow]:
return self._db_command(
dict(kind="query", sql=sql, args=args, first_row_only=first_row_only)
)
- def db_execute_many(self, sql: str, args: List[List[ValueForDB]]) -> List[DBRow]:
+ def db_execute_many(self, sql: str, args: list[list[ValueForDB]]) -> list[DBRow]:
return self._db_command(dict(kind="executemany", sql=sql, args=args))
def db_begin(self) -> None:
@@ -91,7 +91,7 @@ class RustBackend(RustBackendGenerated):
def db_rollback(self) -> None:
return self._db_command(dict(kind="rollback"))
- def _db_command(self, input: Dict[str, Any]) -> Any:
+ def _db_command(self, input: dict[str, Any]) -> Any:
bytes_input = to_json_bytes(input)
try:
return from_json_bytes(self._backend.db_command(bytes_input))
@@ -102,7 +102,7 @@ class RustBackend(RustBackendGenerated):
raise backend_exception_to_pylib(err)
def translate(
- self, module_index: int, message_index: int, **kwargs: Union[str, int, float]
+ self, module_index: int, message_index: int, **kwargs: str | int | float
) -> str:
return self.translate_string(
translate_string_in(
@@ -133,7 +133,7 @@ class RustBackend(RustBackendGenerated):
def translate_string_in(
- module_index: int, message_index: int, **kwargs: Union[str, int, float]
+ module_index: int, message_index: int, **kwargs: str | int | float
) -> i18n_pb2.TranslateStringRequest:
args = {}
for (k, v) in kwargs.items():
@@ -147,10 +147,10 @@ def translate_string_in(
class Translations(GeneratedTranslations):
- def __init__(self, backend: Optional[ref[RustBackend]]):
+ def __init__(self, backend: ref[RustBackend] | None):
self.backend = backend
- def __call__(self, key: Tuple[int, int], **kwargs: Any) -> str:
+ def __call__(self, key: tuple[int, int], **kwargs: Any) -> str:
"Mimic the old col.tr / TR interface"
if "pytest" not in sys.modules:
traceback.print_stack(file=sys.stdout)
@@ -162,7 +162,7 @@ class Translations(GeneratedTranslations):
)
def _translate(
- self, module: int, message: int, args: Dict[str, Union[str, int, float]]
+ self, module: int, message: int, args: dict[str, str | int | float]
) -> str:
return self.backend().translate(
module_index=module, message_index=message, **args
diff --git a/pylib/anki/_backend/genfluent.py b/pylib/anki/_backend/genfluent.py
index 09330265c..fae0c37cd 100644
--- a/pylib/anki/_backend/genfluent.py
+++ b/pylib/anki/_backend/genfluent.py
@@ -50,7 +50,7 @@ def methods() -> str:
return "\n".join(out) + "\n"
-def get_arg_types(args: List[Variable]) -> str:
+def get_arg_types(args: list[Variable]) -> str:
return ", ".join(
[f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args]
@@ -68,7 +68,7 @@ def arg_kind(arg: Variable) -> str:
return "str"
-def get_args(args: List[Variable]) -> str:
+def get_args(args: list[Variable]) -> str:
return ", ".join(
[f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args]
)
diff --git a/pylib/anki/_legacy.py b/pylib/anki/_legacy.py
index 3603cb39d..7528de6a1 100644
--- a/pylib/anki/_legacy.py
+++ b/pylib/anki/_legacy.py
@@ -7,11 +7,11 @@ import functools
import os
import pathlib
import traceback
-from typing import Any, Callable, Dict, Optional, Tuple, Union, no_type_check
+from typing import Any, Callable, Union, no_type_check
import stringcase
-VariableTarget = Tuple[Any, str]
+VariableTarget = tuple[Any, str]
DeprecatedAliasTarget = Union[Callable, VariableTarget]
@@ -43,7 +43,7 @@ class DeprecatedNamesMixin:
# the @no_type_check lines are required to prevent mypy allowing arbitrary
# attributes on the consuming class
- _deprecated_aliases: Dict[str, str] = {}
+ _deprecated_aliases: dict[str, str] = {}
@no_type_check
def __getattr__(self, name: str) -> Any:
@@ -68,7 +68,7 @@ class DeprecatedNamesMixin:
cls._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
-def deprecated(replaced_by: Optional[Callable] = None, info: str = "") -> Callable:
+def deprecated(replaced_by: Callable | None = None, info: str = "") -> Callable:
"""Print a deprecation warning, telling users to use `replaced_by`, or show `doc`."""
def decorator(func: Callable) -> Callable:
diff --git a/pylib/anki/browser.py b/pylib/anki/browser.py
index 7399fa452..c96fb9c80 100644
--- a/pylib/anki/browser.py
+++ b/pylib/anki/browser.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
diff --git a/pylib/anki/buildinfo.py b/pylib/anki/buildinfo.py
index afa52b65b..bb388d5be 100644
--- a/pylib/anki/buildinfo.py
+++ b/pylib/anki/buildinfo.py
@@ -3,7 +3,6 @@
import os
import sys
-from typing import Dict
def _build_info_path() -> str:
@@ -19,7 +18,7 @@ def _build_info_path() -> str:
raise Exception("missing buildinfo.txt")
-def _get_build_info() -> Dict[str, str]:
+def _get_build_info() -> dict[str, str]:
info = {}
with open(_build_info_path(), encoding="utf8") as file:
for line in file.readlines():
diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py
index 2f7a50f18..0d7785ec2 100644
--- a/pylib/anki/cards.py
+++ b/pylib/anki/cards.py
@@ -7,7 +7,6 @@ from __future__ import annotations
import pprint
import time
-from typing import List, NewType, Optional
import anki # pylint: disable=unused-import
from anki import cards_pb2, hooks
@@ -34,7 +33,7 @@ BackendCard = cards_pb2.Card
class Card(DeprecatedNamesMixin):
- _note: Optional[Note]
+ _note: Note | None
lastIvl: int
ord: int
nid: anki.notes.NoteId
@@ -47,12 +46,12 @@ class Card(DeprecatedNamesMixin):
def __init__(
self,
col: anki.collection.Collection,
- id: Optional[CardId] = None,
- backend_card: Optional[BackendCard] = None,
+ id: CardId | None = None,
+ backend_card: BackendCard | None = None,
) -> None:
self.col = col.weakref()
- self.timer_started: Optional[float] = None
- self._render_output: Optional[anki.template.TemplateRenderOutput] = None
+ self.timer_started: float | None = None
+ self._render_output: anki.template.TemplateRenderOutput | None = None
if id:
# existing card
self.id = id
@@ -126,10 +125,10 @@ class Card(DeprecatedNamesMixin):
def answer(self) -> str:
return self.render_output().answer_and_style()
- def question_av_tags(self) -> List[AVTag]:
+ def question_av_tags(self) -> list[AVTag]:
return self.render_output().question_av_tags
- def answer_av_tags(self) -> List[AVTag]:
+ def answer_av_tags(self) -> list[AVTag]:
return self.render_output().answer_av_tags
def render_output(
diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py
index 770b9b30f..5cd2f350b 100644
--- a/pylib/anki/collection.py
+++ b/pylib/anki/collection.py
@@ -5,7 +5,7 @@
from __future__ import annotations
-from typing import Any, Generator, List, Literal, Optional, Sequence, Tuple, Union, cast
+from typing import Any, Generator, Literal, Sequence, Union, cast
from anki import (
card_rendering_pb2,
@@ -92,17 +92,17 @@ LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo]
class Collection(DeprecatedNamesMixin):
- sched: Union[V1Scheduler, V2Scheduler, V3Scheduler]
+ sched: V1Scheduler | V2Scheduler | V3Scheduler
def __init__(
self,
path: str,
- backend: Optional[RustBackend] = None,
+ backend: RustBackend | None = None,
server: bool = False,
log: bool = False,
) -> None:
self._backend = backend or RustBackend(server=server)
- self.db: Optional[DBProxy] = None
+ self.db: DBProxy | None = None
self._should_log = log
self.server = server
self.path = os.path.abspath(path)
@@ -211,7 +211,7 @@ class Collection(DeprecatedNamesMixin):
# to check if the backend updated the modification time.
return self.db.last_begin_at != self.mod
- def save(self, name: Optional[str] = None, trx: bool = True) -> None:
+ def save(self, name: str | None = None, trx: bool = True) -> None:
"Flush, commit DB, and take out another write lock if trx=True."
# commit needed?
if self.db.modified_in_python or self.modified_by_backend():
@@ -379,7 +379,7 @@ class Collection(DeprecatedNamesMixin):
hooks.notes_will_be_deleted(self, note_ids)
return self._backend.remove_notes(note_ids=note_ids, card_ids=[])
- def remove_notes_by_card(self, card_ids: List[CardId]) -> None:
+ def remove_notes_by_card(self, card_ids: list[CardId]) -> None:
if hooks.notes_will_be_deleted.count():
nids = self.db.list(
f"select nid from cards where id in {ids2str(card_ids)}"
@@ -391,7 +391,7 @@ class Collection(DeprecatedNamesMixin):
return [CardId(id) for id in self._backend.cards_of_note(note_id)]
def defaults_for_adding(
- self, *, current_review_card: Optional[Card]
+ self, *, current_review_card: Card | None
) -> anki.notes.DefaultsForAdding:
"""Get starting deck and notetype for add screen.
An option in the preferences controls whether this will be based on the current deck
@@ -406,7 +406,7 @@ class Collection(DeprecatedNamesMixin):
home_deck_of_current_review_card=home_deck,
)
- def default_deck_for_notetype(self, notetype_id: NotetypeId) -> Optional[DeckId]:
+ def default_deck_for_notetype(self, notetype_id: NotetypeId) -> DeckId | None:
"""If 'change deck depending on notetype' is enabled in the preferences,
return the last deck used with the provided notetype, if any.."""
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
@@ -447,7 +447,7 @@ class Collection(DeprecatedNamesMixin):
##########################################################################
def after_note_updates(
- self, nids: List[NoteId], mark_modified: bool, generate_cards: bool = True
+ self, nids: list[NoteId], mark_modified: bool, generate_cards: bool = True
) -> None:
"If notes modified directly in database, call this afterwards."
self._backend.after_note_updates(
@@ -460,7 +460,7 @@ class Collection(DeprecatedNamesMixin):
def find_cards(
self,
query: str,
- order: Union[bool, str, BrowserColumns.Column] = False,
+ order: bool | str | BrowserColumns.Column = False,
reverse: bool = False,
) -> Sequence[CardId]:
"""Return card ids matching the provided search.
@@ -491,7 +491,7 @@ class Collection(DeprecatedNamesMixin):
def find_notes(
self,
query: str,
- order: Union[bool, str, BrowserColumns.Column] = False,
+ order: bool | str | BrowserColumns.Column = False,
reverse: bool = False,
) -> Sequence[NoteId]:
"""Return note ids matching the provided search.
@@ -506,7 +506,7 @@ class Collection(DeprecatedNamesMixin):
def _build_sort_mode(
self,
- order: Union[bool, str, BrowserColumns.Column],
+ order: bool | str | BrowserColumns.Column,
reverse: bool,
finding_notes: bool,
) -> search_pb2.SortOrder:
@@ -539,7 +539,7 @@ class Collection(DeprecatedNamesMixin):
search: str,
replacement: str,
regex: bool = False,
- field_name: Optional[str] = None,
+ field_name: str | None = None,
match_case: bool = False,
) -> OpChangesWithCount:
"Find and replace fields in a note. Returns changed note count."
@@ -556,14 +556,14 @@ class Collection(DeprecatedNamesMixin):
return self._backend.field_names_for_notes(nids)
# returns array of ("dupestr", [nids])
- def find_dupes(self, field_name: str, search: str = "") -> List[Tuple[str, list]]:
+ def find_dupes(self, field_name: str, search: str = "") -> list[tuple[str, list]]:
nids = self.find_notes(
self.build_search_string(search, SearchNode(field_name=field_name))
)
# go through notes
- vals: Dict[str, List[int]] = {}
+ vals: dict[str, list[int]] = {}
dupes = []
- fields: Dict[int, int] = {}
+ fields: dict[int, int] = {}
def ord_for_mid(mid: NotetypeId) -> int:
if mid not in fields:
@@ -596,7 +596,7 @@ class Collection(DeprecatedNamesMixin):
def build_search_string(
self,
- *nodes: Union[str, SearchNode],
+ *nodes: str | SearchNode,
joiner: SearchJoiner = "AND",
) -> str:
"""Join one or more searches, and return a normalized search string.
@@ -612,7 +612,7 @@ class Collection(DeprecatedNamesMixin):
def group_searches(
self,
- *nodes: Union[str, SearchNode],
+ *nodes: str | SearchNode,
joiner: SearchJoiner = "AND",
) -> SearchNode:
"""Join provided search nodes and strings into a single SearchNode.
@@ -680,7 +680,7 @@ class Collection(DeprecatedNamesMixin):
def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
return self._backend.all_browser_columns()
- def get_browser_column(self, key: str) -> Optional[BrowserColumns.Column]:
+ def get_browser_column(self, key: str) -> BrowserColumns.Column | None:
for column in self._backend.all_browser_columns():
if column.key == key:
return column
@@ -688,7 +688,7 @@ class Collection(DeprecatedNamesMixin):
def browser_row_for_id(
self, id_: int
- ) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
+ ) -> tuple[Generator[tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
row = self._backend.browser_row_for_id(id_)
return (
((cell.text, cell.is_rtl) for cell in row.cells),
@@ -697,7 +697,7 @@ class Collection(DeprecatedNamesMixin):
row.font_size,
)
- def load_browser_card_columns(self) -> List[str]:
+ def load_browser_card_columns(self) -> list[str]:
"""Return the stored card column names and ensure the backend columns are set and in sync."""
columns = self.get_config(
BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS
@@ -705,11 +705,11 @@ class Collection(DeprecatedNamesMixin):
self._backend.set_active_browser_columns(columns)
return columns
- def set_browser_card_columns(self, columns: List[str]) -> None:
+ def set_browser_card_columns(self, columns: list[str]) -> None:
self.set_config(BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, columns)
self._backend.set_active_browser_columns(columns)
- def load_browser_note_columns(self) -> List[str]:
+ def load_browser_note_columns(self) -> list[str]:
"""Return the stored note column names and ensure the backend columns are set and in sync."""
columns = self.get_config(
BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS
@@ -717,7 +717,7 @@ class Collection(DeprecatedNamesMixin):
self._backend.set_active_browser_columns(columns)
return columns
- def set_browser_note_columns(self, columns: List[str]) -> None:
+ def set_browser_note_columns(self, columns: list[str]) -> None:
self.set_config(BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, columns)
self._backend.set_active_browser_columns(columns)
@@ -745,7 +745,7 @@ class Collection(DeprecatedNamesMixin):
def remove_config(self, key: str) -> OpChanges:
return self.conf.remove(key)
- def all_config(self) -> Dict[str, Any]:
+ def all_config(self) -> dict[str, Any]:
"This is a debugging aid. Prefer .get_config() when you know the key you need."
return from_json_bytes(self._backend.get_all_config())
@@ -802,7 +802,7 @@ class Collection(DeprecatedNamesMixin):
# Stats
##########################################################################
- def stats(self) -> "anki.stats.CollectionStats":
+ def stats(self) -> anki.stats.CollectionStats:
from anki.stats import CollectionStats
return CollectionStats(self)
@@ -926,7 +926,7 @@ table.review-log {{ {revlog_style} }}
return True
return False
- def _check_backend_undo_status(self) -> Optional[UndoStatus]:
+ def _check_backend_undo_status(self) -> UndoStatus | None:
"""Return undo status if undo available on backend.
If backend has undo available, clear the Python undo state."""
status = self._backend.get_undo_status()
@@ -956,7 +956,7 @@ table.review-log {{ {revlog_style} }}
self.clear_python_undo()
return undo
- def _save_checkpoint(self, name: Optional[str]) -> None:
+ def _save_checkpoint(self, name: str | None) -> None:
"Call via .save(). If name not provided, clear any existing checkpoint."
self._last_checkpoint_at = time.time()
if name:
@@ -1017,7 +1017,7 @@ table.review-log {{ {revlog_style} }}
# DB maintenance
##########################################################################
- def fix_integrity(self) -> Tuple[str, bool]:
+ def fix_integrity(self) -> tuple[str, bool]:
"""Fix possible problems and rebuild caches.
Returns tuple of (error: str, ok: bool). 'ok' will be true if no
@@ -1108,7 +1108,7 @@ table.review-log {{ {revlog_style} }}
self._startTime = time.time()
self._startReps = self.sched.reps
- def timeboxReached(self) -> Union[Literal[False], Tuple[Any, int]]:
+ def timeboxReached(self) -> Literal[False] | tuple[Any, int]:
"Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf["timeLim"]:
# timeboxing disabled
@@ -1126,7 +1126,7 @@ table.review-log {{ {revlog_style} }}
print(args, kwargs)
@deprecated(replaced_by=undo_status)
- def undo_name(self) -> Optional[str]:
+ def undo_name(self) -> str | None:
"Undo menu item name, or None if undo unavailable."
status = self.undo_status()
return status.undo or None
@@ -1146,7 +1146,7 @@ table.review-log {{ {revlog_style} }}
self.remove_notes(ids)
@deprecated(replaced_by=remove_notes)
- def _remNotes(self, ids: List[NoteId]) -> None:
+ def _remNotes(self, ids: list[NoteId]) -> None:
pass
@deprecated(replaced_by=card_stats)
@@ -1154,21 +1154,21 @@ table.review-log {{ {revlog_style} }}
return self.card_stats(card.id, include_revlog=False)
@deprecated(replaced_by=after_note_updates)
- def updateFieldCache(self, nids: List[NoteId]) -> None:
+ def updateFieldCache(self, nids: list[NoteId]) -> None:
self.after_note_updates(nids, mark_modified=False, generate_cards=False)
@deprecated(replaced_by=after_note_updates)
- def genCards(self, nids: List[NoteId]) -> List[int]:
+ def genCards(self, nids: list[NoteId]) -> list[int]:
self.after_note_updates(nids, mark_modified=False, generate_cards=True)
# previously returned empty cards, no longer does
return []
@deprecated(info="no longer used")
- def emptyCids(self) -> List[CardId]:
+ def emptyCids(self) -> list[CardId]:
return []
@deprecated(info="handled by backend")
- def _logRem(self, ids: List[Union[int, NoteId]], type: int) -> None:
+ def _logRem(self, ids: list[int | NoteId], type: int) -> None:
self.db.executemany(
"insert into graves values (%d, ?, %d)" % (self.usn(), type),
([x] for x in ids),
@@ -1197,7 +1197,7 @@ _Collection = Collection
@dataclass
class _ReviewsUndo:
- entries: List[LegacyReviewUndo] = field(default_factory=list)
+ entries: list[LegacyReviewUndo] = field(default_factory=list)
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py
index f26388245..98ba97b90 100644
--- a/pylib/anki/consts.py
+++ b/pylib/anki/consts.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import sys
-from typing import Any, Dict, NewType, Optional
+from typing import Any, NewType
# whether new cards should be mixed with reviews, or shown first or last
NEW_CARDS_DISTRIBUTE = 0
@@ -94,7 +94,7 @@ REVLOG_RESCHED = 4
import anki.collection
-def _tr(col: Optional[anki.collection.Collection]) -> Any:
+def _tr(col: anki.collection.Collection | None) -> Any:
if col:
return col.tr
else:
@@ -107,7 +107,7 @@ def _tr(col: Optional[anki.collection.Collection]) -> Any:
return tr_legacyglobal
-def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, Any]:
+def newCardOrderLabels(col: anki.collection.Collection | None) -> dict[int, Any]:
tr = _tr(col)
return {
0: tr.scheduling_show_new_cards_in_random_order(),
@@ -116,8 +116,8 @@ def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, A
def newCardSchedulingLabels(
- col: Optional[anki.collection.Collection],
-) -> Dict[int, Any]:
+ col: anki.collection.Collection | None,
+) -> dict[int, Any]:
tr = _tr(col)
return {
0: tr.scheduling_mix_new_cards_and_reviews(),
diff --git a/pylib/anki/db.py b/pylib/anki/db.py
index ae5d55eac..5d55839fb 100644
--- a/pylib/anki/db.py
+++ b/pylib/anki/db.py
@@ -9,12 +9,14 @@ but this class is still used by aqt's profile manager, and a number
of add-ons rely on it.
"""
+from __future__ import annotations
+
import os
import pprint
import time
from sqlite3 import Cursor
from sqlite3 import dbapi2 as sqlite
-from typing import Any, List, Type
+from typing import Any
DBError = sqlite.Error
@@ -82,7 +84,7 @@ class DB:
return res[0]
return None
- def all(self, *a: Any, **kw: Any) -> List:
+ def all(self, *a: Any, **kw: Any) -> list:
return self.execute(*a, **kw).fetchall()
def first(self, *a: Any, **kw: Any) -> Any:
@@ -91,7 +93,7 @@ class DB:
c.close()
return res
- def list(self, *a: Any, **kw: Any) -> List:
+ def list(self, *a: Any, **kw: Any) -> list:
return [x[0] for x in self.execute(*a, **kw)]
def close(self) -> None:
@@ -124,5 +126,5 @@ class DB:
def _textFactory(self, data: bytes) -> str:
return str(data, errors="ignore")
- def cursor(self, factory: Type[Cursor] = Cursor) -> Cursor:
+ def cursor(self, factory: type[Cursor] = Cursor) -> Cursor:
return self._db.cursor(factory)
diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py
index 45c391383..9d0b25b89 100644
--- a/pylib/anki/dbproxy.py
+++ b/pylib/anki/dbproxy.py
@@ -5,7 +5,7 @@ from __future__ import annotations
import re
from re import Match
-from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
+from typing import Any, Iterable, Sequence, Union
import anki._backend
@@ -49,7 +49,7 @@ class DBProxy:
*args: ValueForDB,
first_row_only: bool = False,
**kwargs: ValueForDB,
- ) -> List[Row]:
+ ) -> list[Row]:
# mark modified?
s = sql.strip().lower()
for stmt in "insert", "update", "delete":
@@ -62,15 +62,15 @@ class DBProxy:
# Query shortcuts
###################
- def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]:
+ def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> list[Row]:
return self._query(sql, *args, first_row_only=False, **kwargs)
def list(
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB
- ) -> List[ValueFromDB]:
+ ) -> list[ValueFromDB]:
return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)]
- def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]:
+ def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Row | None:
rows = self._query(sql, *args, first_row_only=True, **kwargs)
if rows:
return rows[0]
@@ -102,8 +102,8 @@ class DBProxy:
# convert kwargs to list format
def emulate_named_args(
- sql: str, args: Tuple, kwargs: Dict[str, Any]
-) -> Tuple[str, Sequence[ValueForDB]]:
+ sql: str, args: tuple, kwargs: dict[str, Any]
+) -> tuple[str, Sequence[ValueForDB]]:
# nothing to do?
if not kwargs:
return sql, args
diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py
index b40216b48..5c7909391 100644
--- a/pylib/anki/decks.py
+++ b/pylib/anki/decks.py
@@ -6,19 +6,7 @@
from __future__ import annotations
import copy
-from typing import (
- TYPE_CHECKING,
- Any,
- Dict,
- Iterable,
- List,
- NewType,
- Optional,
- Sequence,
- Tuple,
- Union,
- no_type_check,
-)
+from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence, no_type_check
if TYPE_CHECKING:
import anki
@@ -40,8 +28,8 @@ DeckConfigsForUpdate = deckconfig_pb2.DeckConfigsForUpdate
UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest
# type aliases until we can move away from dicts
-DeckDict = Dict[str, Any]
-DeckConfigDict = Dict[str, Any]
+DeckDict = dict[str, Any]
+DeckConfigDict = dict[str, Any]
DeckId = NewType("DeckId", int)
DeckConfigId = NewType("DeckConfigId", int)
@@ -96,7 +84,7 @@ class DeckManager(DeprecatedNamesMixin):
self.col = col.weakref()
self.decks = DecksDictProxy(col)
- def save(self, deck_or_config: Union[DeckDict, DeckConfigDict] = None) -> None:
+ def save(self, deck_or_config: DeckDict | DeckConfigDict = None) -> None:
"Can be called with either a deck or a deck configuration."
if not deck_or_config:
print("col.decks.save() should be passed the changed deck")
@@ -131,7 +119,7 @@ class DeckManager(DeprecatedNamesMixin):
name: str,
create: bool = True,
type: DeckConfigId = DeckConfigId(0),
- ) -> Optional[DeckId]:
+ ) -> DeckId | None:
"Add a deck with NAME. Reuse deck if already exists. Return id as int."
id = self.id_for_name(name)
if id:
@@ -155,13 +143,13 @@ class DeckManager(DeprecatedNamesMixin):
skip_empty_default=skip_empty_default, include_filtered=include_filtered
)
- def id_for_name(self, name: str) -> Optional[DeckId]:
+ def id_for_name(self, name: str) -> DeckId | None:
try:
return DeckId(self.col._backend.get_deck_id_by_name(name))
except NotFoundError:
return None
- def get_legacy(self, did: DeckId) -> Optional[DeckDict]:
+ def get_legacy(self, did: DeckId) -> DeckDict | None:
try:
return from_json_bytes(self.col._backend.get_deck_legacy(did))
except NotFoundError:
@@ -170,7 +158,7 @@ class DeckManager(DeprecatedNamesMixin):
def have(self, id: DeckId) -> bool:
return bool(self.get_legacy(id))
- def get_all_legacy(self) -> List[DeckDict]:
+ def get_all_legacy(self) -> list[DeckDict]:
return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values())
def new_deck_legacy(self, filtered: bool) -> DeckDict:
@@ -191,7 +179,7 @@ class DeckManager(DeprecatedNamesMixin):
@classmethod
def find_deck_in_tree(
cls, node: DeckTreeNode, deck_id: DeckId
- ) -> Optional[DeckTreeNode]:
+ ) -> DeckTreeNode | None:
if node.deck_id == deck_id:
return node
for child in node.children:
@@ -200,7 +188,7 @@ class DeckManager(DeprecatedNamesMixin):
return match
return None
- def all(self) -> List[DeckDict]:
+ def all(self) -> list[DeckDict]:
"All decks. Expensive; prefer all_names_and_ids()"
return self.get_all_legacy()
@@ -226,7 +214,7 @@ class DeckManager(DeprecatedNamesMixin):
return len(self.all_names_and_ids())
def card_count(
- self, dids: Union[DeckId, Iterable[DeckId]], include_subdecks: bool
+ self, dids: DeckId | Iterable[DeckId], include_subdecks: bool
) -> Any:
if isinstance(dids, int):
dids = {dids}
@@ -240,7 +228,7 @@ class DeckManager(DeprecatedNamesMixin):
)
return count
- def get(self, did: Union[DeckId, str], default: bool = True) -> Optional[DeckDict]:
+ def get(self, did: DeckId | str, default: bool = True) -> DeckDict | None:
if not did:
if default:
return self.get_legacy(DEFAULT_DECK_ID)
@@ -255,7 +243,7 @@ class DeckManager(DeprecatedNamesMixin):
else:
return None
- def by_name(self, name: str) -> Optional[DeckDict]:
+ def by_name(self, name: str) -> DeckDict | None:
"""Get deck with NAME, ignoring case."""
id = self.id_for_name(name)
if id:
@@ -271,7 +259,7 @@ class DeckManager(DeprecatedNamesMixin):
def update_dict(self, deck: DeckDict) -> OpChanges:
return self.col._backend.update_deck_legacy(json=to_json_bytes(deck))
- def rename(self, deck: Union[DeckDict, DeckId], new_name: str) -> OpChanges:
+ def rename(self, deck: DeckDict | DeckId, new_name: str) -> OpChanges:
"Rename deck prefix to NAME if not exists. Updates children."
if isinstance(deck, int):
deck_id = deck
@@ -300,7 +288,7 @@ class DeckManager(DeprecatedNamesMixin):
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
return self.col._backend.update_deck_configs(input=input)
- def all_config(self) -> List[DeckConfigDict]:
+ def all_config(self) -> list[DeckConfigDict]:
"A list of all deck config."
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
@@ -318,7 +306,7 @@ class DeckManager(DeprecatedNamesMixin):
# dynamic decks have embedded conf
return deck
- def get_config(self, conf_id: DeckConfigId) -> Optional[DeckConfigDict]:
+ def get_config(self, conf_id: DeckConfigId) -> DeckConfigDict | None:
try:
return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id))
except NotFoundError:
@@ -331,7 +319,7 @@ class DeckManager(DeprecatedNamesMixin):
)
def add_config(
- self, name: str, clone_from: Optional[DeckConfigDict] = None
+ self, name: str, clone_from: DeckConfigDict | None = None
) -> DeckConfigDict:
if clone_from is not None:
conf = copy.deepcopy(clone_from)
@@ -343,7 +331,7 @@ class DeckManager(DeprecatedNamesMixin):
return conf
def add_config_returning_id(
- self, name: str, clone_from: Optional[DeckConfigDict] = None
+ self, name: str, clone_from: DeckConfigDict | None = None
) -> DeckConfigId:
return self.add_config(name, clone_from)["id"]
@@ -363,7 +351,7 @@ class DeckManager(DeprecatedNamesMixin):
deck["conf"] = id
self.save(deck)
- def decks_using_config(self, conf: DeckConfigDict) -> List[DeckId]:
+ def decks_using_config(self, conf: DeckConfigDict) -> list[DeckId]:
dids = []
for deck in self.all():
if "conf" in deck and deck["conf"] == conf["id"]:
@@ -389,13 +377,13 @@ class DeckManager(DeprecatedNamesMixin):
return deck["name"]
return self.col.tr.decks_no_deck()
- def name_if_exists(self, did: DeckId) -> Optional[str]:
+ def name_if_exists(self, did: DeckId) -> str | None:
deck = self.get(did, default=False)
if deck:
return deck["name"]
return None
- def cids(self, did: DeckId, children: bool = False) -> List[CardId]:
+ def cids(self, did: DeckId, children: bool = False) -> list[CardId]:
if not children:
return self.col.db.list("select id from cards where did=?", did)
dids = [did]
@@ -403,7 +391,7 @@ class DeckManager(DeprecatedNamesMixin):
dids.append(id)
return self.col.db.list(f"select id from cards where did in {ids2str(dids)}")
- def for_card_ids(self, cids: List[CardId]) -> List[DeckId]:
+ def for_card_ids(self, cids: list[CardId]) -> list[DeckId]:
return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
# Deck selection
@@ -419,7 +407,7 @@ class DeckManager(DeprecatedNamesMixin):
def current(self) -> DeckDict:
return self.get(self.selected())
- def active(self) -> List[DeckId]:
+ def active(self) -> list[DeckId]:
# some add-ons assume this will always be non-empty
return self.col.sched.active_decks or [DeckId(1)]
@@ -435,7 +423,7 @@ class DeckManager(DeprecatedNamesMixin):
#############################################################
@staticmethod
- def path(name: str) -> List[str]:
+ def path(name: str) -> list[str]:
return name.split("::")
@classmethod
@@ -443,21 +431,21 @@ class DeckManager(DeprecatedNamesMixin):
return cls.path(name)[-1]
@classmethod
- def immediate_parent_path(cls, name: str) -> List[str]:
+ def immediate_parent_path(cls, name: str) -> list[str]:
return cls.path(name)[:-1]
@classmethod
- def immediate_parent(cls, name: str) -> Optional[str]:
+ def immediate_parent(cls, name: str) -> str | None:
parent_path = cls.immediate_parent_path(name)
if parent_path:
return "::".join(parent_path)
return None
@classmethod
- def key(cls, deck: DeckDict) -> List[str]:
+ def key(cls, deck: DeckDict) -> list[str]:
return cls.path(deck["name"])
- def children(self, did: DeckId) -> List[Tuple[str, DeckId]]:
+ def children(self, did: DeckId) -> list[tuple[str, DeckId]]:
"All children of did, as (name, id)."
name = self.get(did)["name"]
actv = []
@@ -472,24 +460,24 @@ class DeckManager(DeprecatedNamesMixin):
DeckId(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix)
)
- def deck_and_child_ids(self, deck_id: DeckId) -> List[DeckId]:
+ def deck_and_child_ids(self, deck_id: DeckId) -> list[DeckId]:
parent_name = self.name(deck_id)
out = [deck_id]
out.extend(self.child_ids(parent_name))
return out
def parents(
- self, did: DeckId, name_map: Optional[Dict[str, DeckDict]] = None
- ) -> List[DeckDict]:
+ self, did: DeckId, name_map: dict[str, DeckDict] | None = None
+ ) -> list[DeckDict]:
"All parents of did."
# get parent and grandparent names
- parents_names: List[str] = []
+ parents_names: list[str] = []
for part in self.immediate_parent_path(self.get(did)["name"]):
if not parents_names:
parents_names.append(part)
else:
parents_names.append(f"{parents_names[-1]}::{part}")
- parents: List[DeckDict] = []
+ parents: list[DeckDict] = []
# convert to objects
for parent_name in parents_names:
if name_map:
@@ -499,13 +487,13 @@ class DeckManager(DeprecatedNamesMixin):
parents.append(deck)
return parents
- def parents_by_name(self, name: str) -> List[DeckDict]:
+ def parents_by_name(self, name: str) -> list[DeckDict]:
"All existing parents of name"
if "::" not in name:
return []
names = self.immediate_parent_path(name)
head = []
- parents: List[DeckDict] = []
+ parents: list[DeckDict] = []
while names:
head.append(names.pop(0))
@@ -524,7 +512,7 @@ class DeckManager(DeprecatedNamesMixin):
self.select(did)
return did
- def is_filtered(self, did: Union[DeckId, str]) -> bool:
+ def is_filtered(self, did: DeckId | str) -> bool:
return bool(self.get(did)["dyn"])
# Legacy
@@ -546,11 +534,11 @@ class DeckManager(DeprecatedNamesMixin):
self.remove([did])
@deprecated(replaced_by=all_names_and_ids)
- def name_map(self) -> Dict[str, DeckDict]:
+ def name_map(self) -> dict[str, DeckDict]:
return {d["name"]: d for d in self.all()}
@deprecated(info="use col.set_deck() instead")
- def set_deck(self, cids: List[CardId], did: DeckId) -> None:
+ def set_deck(self, cids: list[CardId], did: DeckId) -> None:
self.col.set_deck(card_ids=cids, deck_id=did)
self.col.db.execute(
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
@@ -560,11 +548,11 @@ class DeckManager(DeprecatedNamesMixin):
)
@deprecated(replaced_by=all_names_and_ids)
- def all_ids(self) -> List[str]:
+ def all_ids(self) -> list[str]:
return [str(x.id) for x in self.all_names_and_ids()]
@deprecated(replaced_by=all_names_and_ids)
- def all_names(self, dyn: bool = True, force_default: bool = True) -> List[str]:
+ def all_names(self, dyn: bool = True, force_default: bool = True) -> list[str]:
return [
x.name
for x in self.all_names_and_ids(
diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py
index 4b3b9f8f0..0c8da5710 100644
--- a/pylib/anki/exporting.py
+++ b/pylib/anki/exporting.py
@@ -3,6 +3,8 @@
# pylint: disable=invalid-name
+from __future__ import annotations
+
import json
import os
import re
@@ -10,7 +12,7 @@ import shutil
import unicodedata
import zipfile
from io import BufferedWriter
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+from typing import Any, Optional, Sequence
from zipfile import ZipFile
from anki import hooks
@@ -21,7 +23,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML
class Exporter:
- includeHTML: Union[bool, None] = None
+ includeHTML: bool | None = None
ext: Optional[str] = None
includeTags: Optional[bool] = None
includeSched: Optional[bool] = None
@@ -31,7 +33,7 @@ class Exporter:
self,
col: Collection,
did: Optional[DeckId] = None,
- cids: Optional[List[CardId]] = None,
+ cids: Optional[list[CardId]] = None,
) -> None:
self.col = col.weakref()
self.did = did
@@ -177,7 +179,7 @@ where cards.id in %s)"""
class AnkiExporter(Exporter):
ext = ".anki2"
- includeSched: Union[bool, None] = False
+ includeSched: bool | None = False
includeMedia = True
def __init__(self, col: Collection) -> None:
@@ -187,7 +189,7 @@ class AnkiExporter(Exporter):
def key(col: Collection) -> str:
return col.tr.exporting_anki_20_deck()
- def deckIds(self) -> List[DeckId]:
+ def deckIds(self) -> list[DeckId]:
if self.cids:
return self.col.decks.for_card_ids(self.cids)
elif self.did:
@@ -210,7 +212,7 @@ class AnkiExporter(Exporter):
cids = self.cardIds()
# copy cards, noting used nids
nids = {}
- data: List[Sequence] = []
+ data: list[Sequence] = []
for row in self.src.db.execute(
"select * from cards where id in " + ids2str(cids)
):
@@ -344,7 +346,7 @@ class AnkiPackageExporter(AnkiExporter):
z.writestr("media", json.dumps(media))
z.close()
- def doExport(self, z: ZipFile, path: str) -> Dict[str, str]: # type: ignore
+ def doExport(self, z: ZipFile, path: str) -> dict[str, str]: # type: ignore
# export into the anki2 file
colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile)
@@ -368,7 +370,7 @@ class AnkiPackageExporter(AnkiExporter):
shutil.rmtree(path.replace(".apkg", ".media"))
return media
- def _exportMedia(self, z: ZipFile, files: List[str], fdir: str) -> Dict[str, str]:
+ def _exportMedia(self, z: ZipFile, files: list[str], fdir: str) -> dict[str, str]:
media = {}
for c, file in enumerate(files):
cStr = str(c)
@@ -445,13 +447,13 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter):
##########################################################################
-def exporters(col: Collection) -> List[Tuple[str, Any]]:
+def exporters(col: Collection) -> list[tuple[str, Any]]:
def id(obj):
if callable(obj.key):
key_str = obj.key(col)
else:
key_str = obj.key
- return ("%s (*%s)" % (key_str, obj.ext), obj)
+ return (f"{key_str} (*{obj.ext})", obj)
exps = [
id(AnkiCollectionPackageExporter),
diff --git a/pylib/anki/find.py b/pylib/anki/find.py
index 446fd8d45..3724b64d4 100644
--- a/pylib/anki/find.py
+++ b/pylib/anki/find.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Optional, Set
+from typing import TYPE_CHECKING
from anki.hooks import *
from anki.notes import NoteId
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
class Finder:
- def __init__(self, col: Optional[Collection]) -> None:
+ def __init__(self, col: Collection | None) -> None:
self.col = col.weakref()
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
@@ -34,7 +34,7 @@ def findReplace(
src: str,
dst: str,
regex: bool = False,
- field: Optional[str] = None,
+ field: str | None = None,
fold: bool = True,
) -> int:
"Find and replace fields in a note. Returns changed note count."
@@ -58,7 +58,7 @@ def fieldNamesForNotes(col: Collection, nids: List[NoteId]) -> List[str]:
def fieldNames(col: Collection, downcase: bool = True) -> List:
- fields: Set[str] = set()
+ fields: set[str] = set()
for m in col.models.all():
for f in m["flds"]:
name = f["name"].lower() if downcase else f["name"]
diff --git a/pylib/anki/hooks.py b/pylib/anki/hooks.py
index 87ca01ee8..8efb78bcf 100644
--- a/pylib/anki/hooks.py
+++ b/pylib/anki/hooks.py
@@ -12,8 +12,6 @@ modifying it.
from __future__ import annotations
-from typing import Any, Callable, Dict, List
-
import decorator
# You can find the definitions in ../tools/genhooks.py
@@ -22,7 +20,7 @@ from anki.hooks_gen import *
# Legacy hook handling
##############################################################################
-_hooks: Dict[str, List[Callable[..., Any]]] = {}
+_hooks: dict[str, list[Callable[..., Any]]] = {}
def runHook(hook: str, *args: Any) -> None:
diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py
index a26536496..6643deb4d 100644
--- a/pylib/anki/httpclient.py
+++ b/pylib/anki/httpclient.py
@@ -9,7 +9,7 @@ from __future__ import annotations
import io
import os
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable
import requests
from requests import Response
@@ -24,9 +24,9 @@ class HttpClient:
verify = True
timeout = 60
# args are (upload_bytes_in_chunk, download_bytes_in_chunk)
- progress_hook: Optional[ProgressCallback] = None
+ progress_hook: ProgressCallback | None = None
- def __init__(self, progress_hook: Optional[ProgressCallback] = None) -> None:
+ def __init__(self, progress_hook: ProgressCallback | None = None) -> None:
self.progress_hook = progress_hook
self.session = requests.Session()
@@ -44,9 +44,7 @@ class HttpClient:
def __del__(self) -> None:
self.close()
- def post(
- self, url: str, data: bytes, headers: Optional[Dict[str, str]]
- ) -> Response:
+ def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response:
headers["User-Agent"] = self._agentName()
return self.session.post(
url,
@@ -57,7 +55,7 @@ class HttpClient:
verify=self.verify,
) # pytype: disable=wrong-arg-types
- def get(self, url: str, headers: Dict[str, str] = None) -> Response:
+ def get(self, url: str, headers: dict[str, str] = None) -> Response:
if headers is None:
headers = {}
headers["User-Agent"] = self._agentName()
diff --git a/pylib/anki/importing/__init__.py b/pylib/anki/importing/__init__.py
index 7c35ba545..c5636f6b1 100644
--- a/pylib/anki/importing/__init__.py
+++ b/pylib/anki/importing/__init__.py
@@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import Any, Callable, Sequence, Tuple, Type, Union
+from typing import Any, Callable, Sequence, Type, Union
from anki.collection import Collection
from anki.importing.anki2 import Anki2Importer
@@ -14,7 +14,7 @@ from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
from anki.lang import TR
-def importers(col: Collection) -> Sequence[Tuple[str, Type[Importer]]]:
+def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
return (
(col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter),
(
diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py
index cddacd787..7b5a5c46b 100644
--- a/pylib/anki/importing/anki2.py
+++ b/pylib/anki/importing/anki2.py
@@ -5,7 +5,7 @@
import os
import unicodedata
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Optional
from anki.cards import CardId
from anki.collection import Collection
@@ -37,7 +37,7 @@ class Anki2Importer(Importer):
super().__init__(col, file)
# set later, defined here for typechecking
- self._decks: Dict[DeckId, DeckId] = {}
+ self._decks: dict[DeckId, DeckId] = {}
self.source_needs_upgrade = False
def run(self, media: None = None, importing_v2: bool = True) -> None:
@@ -80,14 +80,14 @@ class Anki2Importer(Importer):
# Notes
######################################################################
- def _logNoteRow(self, action: str, noteRow: List[str]) -> None:
+ def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
self.log.append(
- "[%s] %s" % (action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
+ "[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
)
def _importNotes(self) -> None:
# build guid -> (id,mod,mid) hash & map of existing note ids
- self._notes: Dict[str, Tuple[NoteId, int, NotetypeId]] = {}
+ self._notes: dict[str, tuple[NoteId, int, NotetypeId]] = {}
existing = {}
for id, guid, mod, mid in self.dst.db.execute(
"select id, guid, mod, mid from notes"
@@ -96,7 +96,7 @@ class Anki2Importer(Importer):
existing[id] = True
# we ignore updates to changed schemas. we need to note the ignored
# guids, so we avoid importing invalid cards
- self._ignoredGuids: Dict[str, bool] = {}
+ self._ignoredGuids: dict[str, bool] = {}
# iterate over source collection
add = []
update = []
@@ -194,7 +194,7 @@ class Anki2Importer(Importer):
# determine if note is a duplicate, and adjust mid and/or guid as required
# returns true if note should be added
- def _uniquifyNote(self, note: List[Any]) -> bool:
+ def _uniquifyNote(self, note: list[Any]) -> bool:
origGuid = note[GUID]
srcMid = note[MID]
dstMid = self._mid(srcMid)
@@ -218,7 +218,7 @@ class Anki2Importer(Importer):
def _prepareModels(self) -> None:
"Prepare index of schema hashes."
- self._modelMap: Dict[NotetypeId, NotetypeId] = {}
+ self._modelMap: dict[NotetypeId, NotetypeId] = {}
def _mid(self, srcMid: NotetypeId) -> Any:
"Return local id for remote MID."
@@ -308,7 +308,7 @@ class Anki2Importer(Importer):
if self.source_needs_upgrade:
self.src.upgrade_to_v2_scheduler()
# build map of (guid, ord) -> cid and used id cache
- self._cards: Dict[Tuple[str, int], CardId] = {}
+ self._cards: dict[tuple[str, int], CardId] = {}
existing = {}
for guid, ord, cid in self.dst.db.execute(
"select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id"
diff --git a/pylib/anki/importing/apkg.py b/pylib/anki/importing/apkg.py
index 6fd26bbfb..049f54c37 100644
--- a/pylib/anki/importing/apkg.py
+++ b/pylib/anki/importing/apkg.py
@@ -7,14 +7,14 @@ import json
import os
import unicodedata
import zipfile
-from typing import Any, Dict, Optional
+from typing import Any, Optional
from anki.importing.anki2 import Anki2Importer
from anki.utils import tmpfile
class AnkiPackageImporter(Anki2Importer):
- nameToNum: Dict[str, str]
+ nameToNum: dict[str, str]
zip: Optional[zipfile.ZipFile]
def run(self) -> None: # type: ignore
diff --git a/pylib/anki/importing/base.py b/pylib/anki/importing/base.py
index 4ee6626a8..7a4ea1aff 100644
--- a/pylib/anki/importing/base.py
+++ b/pylib/anki/importing/base.py
@@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import Any, List, Optional
+from typing import Any, Optional
from anki.collection import Collection
from anki.utils import maxID
@@ -18,7 +18,7 @@ class Importer:
def __init__(self, col: Collection, file: str) -> None:
self.file = file
- self.log: List[str] = []
+ self.log: list[str] = []
self.col = col.weakref()
self.total = 0
self.dst = None
diff --git a/pylib/anki/importing/csvfile.py b/pylib/anki/importing/csvfile.py
index 961018772..41ed93614 100644
--- a/pylib/anki/importing/csvfile.py
+++ b/pylib/anki/importing/csvfile.py
@@ -1,9 +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 csv
import re
-from typing import Any, List, Optional, TextIO, Union
+from typing import Any, Optional, TextIO
from anki.collection import Collection
from anki.importing.noteimp import ForeignNote, NoteImporter
@@ -19,12 +21,12 @@ class TextImporter(NoteImporter):
self.lines = None
self.fileobj: Optional[TextIO] = None
self.delimiter: Optional[str] = None
- self.tagsToAdd: List[str] = []
+ self.tagsToAdd: list[str] = []
self.numFields = 0
self.dialect: Optional[Any]
- self.data: Optional[Union[str, List[str]]]
+ self.data: Optional[str | list[str]]
- def foreignNotes(self) -> List[ForeignNote]:
+ def foreignNotes(self) -> list[ForeignNote]:
self.open()
# process all lines
log = []
@@ -144,7 +146,7 @@ class TextImporter(NoteImporter):
# pylint: disable=no-member
zuper.__del__(self) # type: ignore
- def noteFromFields(self, fields: List[str]) -> ForeignNote:
+ def noteFromFields(self, fields: list[str]) -> ForeignNote:
note = ForeignNote()
note.fields.extend([x for x in fields])
note.tags.extend(self.tagsToAdd)
diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py
index eca2ed90d..0718dd018 100644
--- a/pylib/anki/importing/noteimp.py
+++ b/pylib/anki/importing/noteimp.py
@@ -3,9 +3,11 @@
# pylint: disable=invalid-name
+from __future__ import annotations
+
import html
import unicodedata
-from typing import Dict, List, Optional, Tuple, Union
+from typing import Optional, Union
from anki.collection import Collection
from anki.config import Config
@@ -22,9 +24,9 @@ from anki.utils import (
timestampID,
)
-TagMappedUpdate = Tuple[int, int, str, str, NoteId, str, str]
-TagModifiedUpdate = Tuple[int, int, str, str, NoteId, str]
-NoTagUpdate = Tuple[int, int, str, NoteId, str]
+TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
+TagModifiedUpdate = tuple[int, int, str, str, NoteId, str]
+NoTagUpdate = tuple[int, int, str, NoteId, str]
Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate]
# Stores a list of fields, tags and deck
@@ -35,10 +37,10 @@ class ForeignNote:
"An temporary object storing fields and attributes."
def __init__(self) -> None:
- self.fields: List[str] = []
- self.tags: List[str] = []
+ self.fields: list[str] = []
+ self.tags: list[str] = []
self.deck = None
- self.cards: Dict[int, ForeignCard] = {} # map of ord -> card
+ self.cards: dict[int, ForeignCard] = {} # map of ord -> card
self.fieldsStr = ""
@@ -75,7 +77,7 @@ class NoteImporter(Importer):
needDelimiter = False
allowHTML = False
importMode = UPDATE_MODE
- mapping: Optional[List[str]]
+ mapping: Optional[list[str]]
tagModified: Optional[str]
def __init__(self, col: Collection, file: str) -> None:
@@ -109,11 +111,11 @@ class NoteImporter(Importer):
def mappingOk(self) -> bool:
return self.model["flds"][0]["name"] in self.mapping
- def foreignNotes(self) -> List:
+ def foreignNotes(self) -> list:
"Return a list of foreign notes for importing."
return []
- def importNotes(self, notes: List[ForeignNote]) -> None:
+ def importNotes(self, notes: list[ForeignNote]) -> None:
"Convert each card into a note, apply attributes and add to col."
assert self.mappingOk()
# note whether tags are mapped
@@ -122,7 +124,7 @@ class NoteImporter(Importer):
if f == "_tags":
self._tagsMapped = True
# gather checks for duplicate comparison
- csums: Dict[str, List[NoteId]] = {}
+ csums: dict[str, list[NoteId]] = {}
for csum, id in self.col.db.execute(
"select csum, id from notes where mid = ?", self.model["id"]
):
@@ -130,18 +132,18 @@ class NoteImporter(Importer):
csums[csum].append(id)
else:
csums[csum] = [id]
- firsts: Dict[str, bool] = {}
+ firsts: dict[str, bool] = {}
fld0idx = self.mapping.index(self.model["flds"][0]["name"])
self._fmap = self.col.models.field_map(self.model)
self._nextID = NoteId(timestampID(self.col.db, "notes"))
# loop through the notes
- updates: List[Updates] = []
+ updates: list[Updates] = []
updateLog = []
new = []
- self._ids: List[NoteId] = []
- self._cards: List[Tuple] = []
+ self._ids: list[NoteId] = []
+ self._cards: list[tuple] = []
dupeCount = 0
- dupes: List[str] = []
+ dupes: list[str] = []
for n in notes:
for c, field in enumerate(n.fields):
if not self.allowHTML:
@@ -232,7 +234,7 @@ class NoteImporter(Importer):
def newData(
self, n: ForeignNote
- ) -> Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]:
+ ) -> tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]:
id = self._nextID
self._nextID = NoteId(self._nextID + 1)
self._ids.append(id)
@@ -256,8 +258,8 @@ class NoteImporter(Importer):
def addNew(
self,
- rows: List[
- Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]
+ rows: list[
+ tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]
],
) -> None:
self.col.db.executemany(
@@ -265,7 +267,7 @@ class NoteImporter(Importer):
)
def updateData(
- self, n: ForeignNote, id: NoteId, sflds: List[str]
+ self, n: ForeignNote, id: NoteId, sflds: list[str]
) -> Optional[Updates]:
self._ids.append(id)
self.processFields(n, sflds)
@@ -280,7 +282,7 @@ class NoteImporter(Importer):
else:
return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr)
- def addUpdates(self, rows: List[Updates]) -> None:
+ def addUpdates(self, rows: list[Updates]) -> None:
changes = self.col.db.scalar("select total_changes()")
if self._tagsMapped:
self.col.db.executemany(
@@ -307,7 +309,7 @@ where id = ? and flds != ?""",
self.updateCount = changes2 - changes
def processFields(
- self, note: ForeignNote, fields: Optional[List[str]] = None
+ self, note: ForeignNote, fields: Optional[list[str]] = None
) -> None:
if not fields:
fields = [""] * len(self.model["flds"])
diff --git a/pylib/anki/importing/supermemo_xml.py b/pylib/anki/importing/supermemo_xml.py
index 7d91e95c5..b3f1ad539 100644
--- a/pylib/anki/importing/supermemo_xml.py
+++ b/pylib/anki/importing/supermemo_xml.py
@@ -9,7 +9,7 @@ import sys
import time
import unicodedata
from string import capwords
-from typing import List, Optional, Union
+from typing import Optional, Union
from xml.dom import minidom
from xml.dom.minidom import Element, Text
@@ -185,7 +185,7 @@ class SupermemoXmlImporter(NoteImporter):
## DEFAULT IMPORTER METHODS
- def foreignNotes(self) -> List[ForeignNote]:
+ def foreignNotes(self) -> list[ForeignNote]:
# Load file and parse it by minidom
self.loadSource(self.file)
@@ -415,7 +415,7 @@ class SupermemoXmlImporter(NoteImporter):
self.logger("-" * 45, level=3)
for key in list(smel.keys()):
self.logger(
- "\t%s %s" % ((key + ":").ljust(15), smel[key]), level=3
+ "\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
)
else:
self.logger("Element skiped \t- no valid Q and A ...", level=3)
diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py
index b0f8a6412..d159eaf44 100644
--- a/pylib/anki/lang.py
+++ b/pylib/anki/lang.py
@@ -6,7 +6,6 @@ from __future__ import annotations
import locale
import re
import weakref
-from typing import Optional, Tuple
import anki
import anki._backend
@@ -153,7 +152,7 @@ currentLang = "en"
# not reference this, and should use col.tr instead. The global
# instance exists for legacy reasons, and as a convenience for the
# Qt code.
-current_i18n: Optional[anki._backend.RustBackend] = None
+current_i18n: anki._backend.RustBackend | None = None
tr_legacyglobal = anki._backend.Translations(None)
@@ -174,7 +173,7 @@ def set_lang(lang: str) -> None:
tr_legacyglobal.backend = weakref.ref(current_i18n)
-def get_def_lang(lang: Optional[str] = None) -> Tuple[int, str]:
+def get_def_lang(lang: str | None = None) -> tuple[int, str]:
"""Return lang converted to name used on disk and its index, defaulting to system language
or English if not available."""
try:
diff --git a/pylib/anki/latex.py b/pylib/anki/latex.py
index 8e9c31966..98dc90bcd 100644
--- a/pylib/anki/latex.py
+++ b/pylib/anki/latex.py
@@ -7,7 +7,7 @@ import html
import os
import re
from dataclasses import dataclass
-from typing import Any, List, Optional, Tuple
+from typing import Any
import anki
from anki import card_rendering_pb2, hooks
@@ -41,7 +41,7 @@ class ExtractedLatex:
@dataclass
class ExtractedLatexOutput:
html: str
- latex: List[ExtractedLatex]
+ latex: list[ExtractedLatex]
@staticmethod
def from_proto(
@@ -80,7 +80,7 @@ def render_latex_returning_errors(
model: NotetypeDict,
col: anki.collection.Collection,
expand_clozes: bool = False,
-) -> Tuple[str, List[str]]:
+) -> tuple[str, list[str]]:
"""Returns (text, errors).
errors will be non-empty if LaTeX failed to render."""
@@ -111,7 +111,7 @@ def _save_latex_image(
header: str,
footer: str,
svg: bool,
-) -> Optional[str]:
+) -> str | None:
# add header/footer
latex = f"{header}\n{extracted.latex_body}\n{footer}"
# it's only really secure if run in a jail, but these are the most common
diff --git a/pylib/anki/media.py b/pylib/anki/media.py
index daae446d0..a491f2436 100644
--- a/pylib/anki/media.py
+++ b/pylib/anki/media.py
@@ -8,7 +8,7 @@ import pprint
import re
import sys
import time
-from typing import Any, Callable, List, Optional, Tuple
+from typing import Any, Callable
from anki import media_pb2
from anki.consts import *
@@ -19,7 +19,7 @@ from anki.template import av_tags_to_native
from anki.utils import intTime
-def media_paths_from_col_path(col_path: str) -> Tuple[str, str]:
+def media_paths_from_col_path(col_path: str) -> tuple[str, str]:
media_folder = re.sub(r"(?i)\.(anki2)$", ".media", col_path)
media_db = f"{media_folder}.db2"
return (media_folder, media_db)
@@ -50,7 +50,7 @@ class MediaManager:
def __init__(self, col: anki.collection.Collection, server: bool) -> None:
self.col = col.weakref()
- self._dir: Optional[str] = None
+ self._dir: str | None = None
if server:
return
# media directory
@@ -88,7 +88,7 @@ class MediaManager:
# may have been deleted
pass
- def dir(self) -> Optional[str]:
+ def dir(self) -> str | None:
return self._dir
def force_resync(self) -> None:
@@ -106,7 +106,7 @@ class MediaManager:
def strip_av_tags(self, text: str) -> str:
return self.col._backend.strip_av_tags(text)
- def _extract_filenames(self, text: str) -> List[str]:
+ def _extract_filenames(self, text: str) -> list[str]:
"This only exists do support a legacy function; do not use."
out = self.col._backend.extract_av_tags(text=text, question_side=True)
return [
@@ -148,7 +148,7 @@ class MediaManager:
def have(self, fname: str) -> bool:
return os.path.exists(os.path.join(self.dir(), fname))
- def trash_files(self, fnames: List[str]) -> None:
+ def trash_files(self, fnames: list[str]) -> None:
"Move provided files to the trash."
self.col._backend.trash_media_files(fnames)
@@ -157,7 +157,7 @@ class MediaManager:
def filesInStr(
self, mid: NotetypeId, string: str, includeRemote: bool = False
- ) -> List[str]:
+ ) -> list[str]:
l = []
model = self.col.models.get(mid)
# handle latex
@@ -204,8 +204,8 @@ class MediaManager:
return output
def render_all_latex(
- self, progress_cb: Optional[Callable[[int], bool]] = None
- ) -> Optional[Tuple[int, str]]:
+ self, progress_cb: Callable[[int], bool] | None = None
+ ) -> tuple[int, str] | None:
"""Render any LaTeX that is missing.
If a progress callback is provided and it returns false, the operation
@@ -260,7 +260,7 @@ class MediaManager:
addFile = add_file
- def writeData(self, opath: str, data: bytes, typeHint: Optional[str] = None) -> str:
+ def writeData(self, opath: str, data: bytes, typeHint: str | None = None) -> str:
fname = os.path.basename(opath)
if typeHint:
fname = self.add_extension_based_on_mime(fname, typeHint)
diff --git a/pylib/anki/models.py b/pylib/anki/models.py
index 1f172f9b8..7f34872aa 100644
--- a/pylib/anki/models.py
+++ b/pylib/anki/models.py
@@ -9,7 +9,7 @@ import copy
import pprint
import sys
import time
-from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union
+from typing import Any, NewType, Sequence, Union
import anki # pylint: disable=unused-import
from anki import notetypes_pb2
@@ -29,10 +29,10 @@ ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
# legacy types
-NotetypeDict = Dict[str, Any]
+NotetypeDict = dict[str, Any]
NoteType = NotetypeDict
-FieldDict = Dict[str, Any]
-TemplateDict = Dict[str, Union[str, int, None]]
+FieldDict = dict[str, Any]
+TemplateDict = dict[str, Union[str, int, None]]
NotetypeId = NewType("NotetypeId", int)
sys.modules["anki.models"].NoteType = NotetypeDict # type: ignore
@@ -97,7 +97,7 @@ class ModelManager(DeprecatedNamesMixin):
# need to cache responses from the backend. Please do not
# access the cache directly!
- _cache: Dict[NotetypeId, NotetypeDict] = {}
+ _cache: dict[NotetypeId, NotetypeDict] = {}
def _update_cache(self, notetype: NotetypeDict) -> None:
self._cache[notetype["id"]] = notetype
@@ -106,7 +106,7 @@ class ModelManager(DeprecatedNamesMixin):
if ntid in self._cache:
del self._cache[ntid]
- def _get_cached(self, ntid: NotetypeId) -> Optional[NotetypeDict]:
+ def _get_cached(self, ntid: NotetypeId) -> NotetypeDict | None:
return self._cache.get(ntid)
def _clear_cache(self) -> None:
@@ -142,13 +142,13 @@ class ModelManager(DeprecatedNamesMixin):
# Retrieving and creating models
#############################################################
- def id_for_name(self, name: str) -> Optional[NotetypeId]:
+ def id_for_name(self, name: str) -> NotetypeId | None:
try:
return NotetypeId(self.col._backend.get_notetype_id_by_name(name))
except NotFoundError:
return None
- def get(self, id: NotetypeId) -> Optional[NotetypeDict]:
+ def get(self, id: NotetypeId) -> NotetypeDict | None:
"Get model with ID, or None."
# deal with various legacy input types
if id is None:
@@ -165,11 +165,11 @@ class ModelManager(DeprecatedNamesMixin):
return None
return notetype
- def all(self) -> List[NotetypeDict]:
+ def all(self) -> list[NotetypeDict]:
"Get all models."
return [self.get(NotetypeId(nt.id)) for nt in self.all_names_and_ids()]
- def by_name(self, name: str) -> Optional[NotetypeDict]:
+ def by_name(self, name: str) -> NotetypeDict | None:
"Get model with NAME."
id = self.id_for_name(name)
if id:
@@ -231,7 +231,7 @@ class ModelManager(DeprecatedNamesMixin):
# Tools
##################################################
- def nids(self, ntid: NotetypeId) -> List[anki.notes.NoteId]:
+ def nids(self, ntid: NotetypeId) -> list[anki.notes.NoteId]:
"Note ids for M."
if isinstance(ntid, dict):
# legacy callers passed in note type
@@ -261,11 +261,11 @@ class ModelManager(DeprecatedNamesMixin):
# Fields
##################################################
- def field_map(self, notetype: NotetypeDict) -> Dict[str, Tuple[int, FieldDict]]:
+ def field_map(self, notetype: NotetypeDict) -> dict[str, tuple[int, FieldDict]]:
"Mapping of field name -> (ord, field)."
return {f["name"]: (f["ord"], f) for f in notetype["flds"]}
- def field_names(self, notetype: NotetypeDict) -> List[str]:
+ def field_names(self, notetype: NotetypeDict) -> list[str]:
return [f["name"] for f in notetype["flds"]]
def sort_idx(self, notetype: NotetypeDict) -> int:
@@ -394,10 +394,10 @@ and notes.mid = ? and cards.ord = ?""",
def change( # pylint: disable=invalid-name
self,
notetype: NotetypeDict,
- nids: List[anki.notes.NoteId],
+ nids: list[anki.notes.NoteId],
newModel: NotetypeDict,
- fmap: Dict[int, Optional[int]],
- cmap: Optional[Dict[int, Optional[int]]],
+ fmap: dict[int, int | None],
+ cmap: dict[int, int | None] | None,
) -> None:
# - maps are ord->ord, and there should not be duplicate targets
self.col.mod_schema(check=True)
@@ -424,8 +424,8 @@ and notes.mid = ? and cards.ord = ?""",
)
def _convert_legacy_map(
- self, old_to_new: Dict[int, Optional[int]], new_count: int
- ) -> List[int]:
+ self, old_to_new: dict[int, int | None], new_count: int
+ ) -> list[int]:
"Convert old->new map to list of old indexes"
new_to_old = {v: k for k, v in old_to_new.items() if v is not None}
out = []
@@ -458,7 +458,7 @@ and notes.mid = ? and cards.ord = ?""",
@deprecated(info="use note.cloze_numbers_in_fields()")
def _availClozeOrds(
self, notetype: NotetypeDict, flds: str, allow_empty: bool = True
- ) -> List[int]:
+ ) -> list[int]:
import anki.notes_pb2
note = anki.notes_pb2.Note(fields=[flds])
@@ -515,11 +515,11 @@ and notes.mid = ? and cards.ord = ?""",
self.col.set_config("curModel", m["id"])
@deprecated(replaced_by=all_names_and_ids)
- def all_names(self) -> List[str]:
+ def all_names(self) -> list[str]:
return [n.name for n in self.all_names_and_ids()]
@deprecated(replaced_by=all_names_and_ids)
- def ids(self) -> List[NotetypeId]:
+ def ids(self) -> list[NotetypeId]:
return [NotetypeId(n.id) for n in self.all_names_and_ids()]
@deprecated(info="no longer required")
diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py
index 2c357d6b7..04cb0637f 100644
--- a/pylib/anki/notes.py
+++ b/pylib/anki/notes.py
@@ -6,7 +6,7 @@
from __future__ import annotations
import copy
-from typing import Any, List, NewType, Optional, Sequence, Tuple, Union
+from typing import Any, NewType, Sequence
import anki # pylint: disable=unused-import
from anki import hooks, notes_pb2
@@ -33,8 +33,8 @@ class Note(DeprecatedNamesMixin):
def __init__(
self,
col: anki.collection.Collection,
- model: Optional[Union[NotetypeDict, NotetypeId]] = None,
- id: Optional[NoteId] = None,
+ model: NotetypeDict | NotetypeId | None = None,
+ id: NoteId | None = None,
) -> None:
assert not (model and id)
notetype_id = model["id"] if isinstance(model, dict) else model
@@ -119,13 +119,13 @@ class Note(DeprecatedNamesMixin):
card._note = self
return card
- def cards(self) -> List[anki.cards.Card]:
+ def cards(self) -> list[anki.cards.Card]:
return [self.col.getCard(id) for id in self.card_ids()]
def card_ids(self) -> Sequence[anki.cards.CardId]:
return self.col.card_ids_of_note(self.id)
- def note_type(self) -> Optional[NotetypeDict]:
+ def note_type(self) -> NotetypeDict | None:
return self.col.models.get(self.mid)
_note_type = property(note_type)
@@ -136,13 +136,13 @@ class Note(DeprecatedNamesMixin):
# Dict interface
##################################################
- def keys(self) -> List[str]:
+ def keys(self) -> list[str]:
return list(self._fmap.keys())
- def values(self) -> List[str]:
+ def values(self) -> list[str]:
return self.fields
- def items(self) -> List[Tuple[str, str]]:
+ def items(self) -> list[tuple[str, str]]:
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
def _field_index(self, key: str) -> int:
diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py
index defd25599..e209075f9 100644
--- a/pylib/anki/scheduler/base.py
+++ b/pylib/anki/scheduler/base.py
@@ -15,7 +15,7 @@ BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
-from typing import List, Optional, Sequence
+from typing import Sequence
from anki import config_pb2
from anki.cards import CardId
@@ -115,7 +115,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
return self.col._backend.restore_buried_and_suspended_cards(ids)
- def unbury_cards(self, ids: List[CardId]) -> OpChanges:
+ def unbury_cards(self, ids: list[CardId]) -> OpChanges:
return self.col._backend.restore_buried_and_suspended_cards(ids)
def unbury_deck(
@@ -162,12 +162,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self,
card_ids: Sequence[CardId],
days: str,
- config_key: Optional[Config.String.V] = None,
+ config_key: Config.String.V | None = None,
) -> OpChanges:
"""Set cards to be due in `days`, turning them into review cards if necessary.
`days` can be of the form '5' or '5..7'
If `config_key` is provided, provided days will be remembered in config."""
- key: Optional[config_pb2.OptionalStringConfigKey]
+ key: config_pb2.OptionalStringConfigKey | None
if config_key is not None:
key = config_pb2.OptionalStringConfigKey(key=config_key)
else:
@@ -179,7 +179,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
config_key=key, # type: ignore
)
- def resetCards(self, ids: List[CardId]) -> None:
+ def resetCards(self, ids: list[CardId]) -> None:
"Completely reset cards for export."
sids = ids2str(ids)
assert self.col.db
@@ -229,7 +229,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self.orderCards(did)
# for post-import
- def maybeRandomizeDeck(self, did: Optional[DeckId] = None) -> None:
+ def maybeRandomizeDeck(self, did: DeckId | None = None) -> None:
if not did:
did = self.col.decks.selected()
conf = self.col.decks.config_dict_for_deck_id(did)
@@ -240,7 +240,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# legacy
def sortCards(
self,
- cids: List[CardId],
+ cids: list[CardId],
start: int = 1,
step: int = 1,
shuffle: bool = False,
diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py
index 85afb717d..99088ddb6 100644
--- a/pylib/anki/scheduler/legacy.py
+++ b/pylib/anki/scheduler/legacy.py
@@ -3,7 +3,7 @@
# pylint: disable=invalid-name
-from typing import List, Optional, Tuple
+from typing import Optional
from anki.cards import Card, CardId
from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN
@@ -17,7 +17,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
"Legacy aliases and helpers. These will go away in the future."
def reschedCards(
- self, card_ids: List[CardId], min_interval: int, max_interval: int
+ self, card_ids: list[CardId], min_interval: int, max_interval: int
) -> None:
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
@@ -77,7 +77,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
self.col.usn(),
)
- def remFromDyn(self, cids: List[CardId]) -> None:
+ def remFromDyn(self, cids: list[CardId]) -> None:
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
# used by v2 scheduler and some add-ons
@@ -104,7 +104,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
elif type == "time":
self.update_stats(did, milliseconds_delta=cnt)
- def deckDueTree(self) -> List:
+ def deckDueTree(self) -> list:
"List of (base name, did, rev, lrn, new, children)"
print(
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
@@ -116,7 +116,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
def _cardConf(self, card: Card) -> DeckConfigDict:
return self.col.decks.config_dict_for_deck_id(card.did)
- def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
+ def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
return (ivl, ivl)
# simple aliases
diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py
index 0a4ddccc3..aabdc1ab3 100644
--- a/pylib/anki/scheduler/v1.py
+++ b/pylib/anki/scheduler/v1.py
@@ -8,7 +8,6 @@ from __future__ import annotations
import random
import time
from heapq import *
-from typing import Any, List, Optional, Tuple, Union
import anki
from anki import hooks
@@ -93,7 +92,7 @@ class Scheduler(V2):
card.usn = self.col.usn()
card.flush()
- def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
+ def counts(self, card: Card | None = None) -> tuple[int, int, int]:
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
idx = self.countIdx(card)
@@ -127,7 +126,7 @@ class Scheduler(V2):
# Getting the next card
##########################################################################
- def _getCard(self) -> Optional[Card]:
+ def _getCard(self) -> Card | None:
"Return the next due card id, or None."
# learning card due?
c = self._getLrnCard()
@@ -179,12 +178,12 @@ and due <= ? limit %d"""
def _resetLrn(self) -> None:
self._resetLrnCount()
- self._lrnQueue: List[Any] = []
- self._lrnDayQueue: List[Any] = []
+ self._lrnQueue: list[Any] = []
+ self._lrnDayQueue: list[Any] = []
self._lrnDids = self.col.decks.active()[:]
# sub-day learning
- def _fillLrn(self) -> Union[bool, List[Any]]:
+ def _fillLrn(self) -> bool | list[Any]:
if not self.lrnCount:
return False
if self._lrnQueue:
@@ -202,7 +201,7 @@ limit %d"""
self._lrnQueue.sort()
return self._lrnQueue
- def _getLrnCard(self, collapse: bool = False) -> Optional[Card]:
+ def _getLrnCard(self, collapse: bool = False) -> Card | None:
if self._fillLrn():
cutoff = time.time()
if collapse:
@@ -374,7 +373,7 @@ limit %d"""
time.sleep(0.01)
log()
- def removeLrn(self, ids: Optional[List[int]] = None) -> None:
+ def removeLrn(self, ids: list[int] | None = None) -> None:
"Remove cards from the learning queues."
if ids:
extra = f" and id in {ids2str(ids)}"
@@ -429,7 +428,7 @@ and due <= ? limit ?)""",
return self._deckNewLimit(did, self._deckRevLimitSingle)
def _resetRev(self) -> None:
- self._revQueue: List[Any] = []
+ self._revQueue: list[Any] = []
self._revDids = self.col.decks.active()[:]
def _fillRev(self, recursing: bool = False) -> bool:
diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py
index 750bb4635..69cc13176 100644
--- a/pylib/anki/scheduler/v2.py
+++ b/pylib/anki/scheduler/v2.py
@@ -8,7 +8,7 @@ from __future__ import annotations
import random
import time
from heapq import *
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
+from typing import Any, Callable, cast
import anki # pylint: disable=unused-import
from anki import hooks, scheduler_pb2
@@ -23,7 +23,7 @@ CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
# legacy type alias
-QueueConfig = Dict[str, Any]
+QueueConfig = dict[str, Any]
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
@@ -49,11 +49,11 @@ class Scheduler(SchedulerBaseWithLegacy):
self.reps = 0
self._haveQueues = False
self._lrnCutoff = 0
- self._active_decks: List[DeckId] = []
+ self._active_decks: list[DeckId] = []
self._current_deck_id = DeckId(1)
@property
- def active_decks(self) -> List[DeckId]:
+ def active_decks(self) -> list[DeckId]:
"Caller must make sure to make a copy."
return self._active_decks
@@ -96,7 +96,7 @@ class Scheduler(SchedulerBaseWithLegacy):
self.revCount = node.review_count
self._immediate_learn_count = node.learn_count
- def getCard(self) -> Optional[Card]:
+ def getCard(self) -> Card | None:
"""Pop the next card from the queue. None if finished."""
self._checkDay()
if not self._haveQueues:
@@ -109,7 +109,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return card
return None
- def _getCard(self) -> Optional[Card]:
+ def _getCard(self) -> Card | None:
"""Return the next due card, or None."""
# learning card due?
c = self._getLrnCard()
@@ -153,7 +153,7 @@ class Scheduler(SchedulerBaseWithLegacy):
def _resetNew(self) -> None:
self._newDids = self.col.decks.active()[:]
- self._newQueue: List[CardId] = []
+ self._newQueue: list[CardId] = []
self._updateNewCardRatio()
def _fillNew(self, recursing: bool = False) -> bool:
@@ -188,7 +188,7 @@ class Scheduler(SchedulerBaseWithLegacy):
self._resetNew()
return self._fillNew(recursing=True)
- def _getNewCard(self) -> Optional[Card]:
+ def _getNewCard(self) -> Card | None:
if self._fillNew():
self.newCount -= 1
return self.col.getCard(self._newQueue.pop())
@@ -204,7 +204,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return
self.newCardModulus = 0
- def _timeForNewCard(self) -> Optional[bool]:
+ def _timeForNewCard(self) -> bool | None:
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
@@ -219,7 +219,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return None
def _deckNewLimit(
- self, did: DeckId, fn: Optional[Callable[[DeckDict], int]] = None
+ self, did: DeckId, fn: Callable[[DeckDict], int] | None = None
) -> int:
if not fn:
fn = self._deckNewLimitSingle
@@ -310,12 +310,12 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
def _resetLrn(self) -> None:
self._updateLrnCutoff(force=True)
self._resetLrnCount()
- self._lrnQueue: List[Tuple[int, CardId]] = []
- self._lrnDayQueue: List[CardId] = []
+ self._lrnQueue: list[tuple[int, CardId]] = []
+ self._lrnDayQueue: list[CardId] = []
self._lrnDids = self.col.decks.active()[:]
# sub-day learning
- def _fillLrn(self) -> Union[bool, List[Any]]:
+ def _fillLrn(self) -> bool | list[Any]:
if not self.lrnCount:
return False
if self._lrnQueue:
@@ -329,12 +329,12 @@ limit %d"""
% (self._deckLimit(), self.reportLimit),
cutoff,
)
- self._lrnQueue = [cast(Tuple[int, CardId], tuple(e)) for e in self._lrnQueue]
+ self._lrnQueue = [cast(tuple[int, CardId], tuple(e)) for e in self._lrnQueue]
# as it arrives sorted by did first, we need to sort it
self._lrnQueue.sort()
return self._lrnQueue
- def _getLrnCard(self, collapse: bool = False) -> Optional[Card]:
+ def _getLrnCard(self, collapse: bool = False) -> Card | None:
self._maybeResetLrn(force=collapse and self.lrnCount == 0)
if self._fillLrn():
cutoff = time.time()
@@ -348,7 +348,7 @@ limit %d"""
return None
# daily learning
- def _fillLrnDay(self) -> Optional[bool]:
+ def _fillLrnDay(self) -> bool | None:
if not self.lrnCount:
return False
if self._lrnDayQueue:
@@ -378,7 +378,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
# shouldn't reach here
return False
- def _getLrnDayCard(self) -> Optional[Card]:
+ def _getLrnDayCard(self) -> Card | None:
if self._fillLrnDay():
self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop())
@@ -391,7 +391,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
d = self.col.decks.get(self.col.decks.selected(), default=False)
return self._deckRevLimitSingle(d)
- def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int:
+ def _deckRevLimitSingle(self, d: dict[str, Any]) -> int:
# invalid deck selected?
if not d:
return 0
@@ -405,7 +405,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
return hooks.scheduler_review_limit_for_single_deck(lim, d)
def _resetRev(self) -> None:
- self._revQueue: List[CardId] = []
+ self._revQueue: list[CardId] = []
def _fillRev(self, recursing: bool = False) -> bool:
"True if a review card can be fetched."
@@ -439,7 +439,7 @@ limit ?"""
self._resetRev()
return self._fillRev(recursing=True)
- def _getRevCard(self) -> Optional[Card]:
+ def _getRevCard(self) -> Card | None:
if self._fillRev():
self.revCount -= 1
return self.col.getCard(self._revQueue.pop())
@@ -601,7 +601,7 @@ limit ?"""
self._rescheduleLrnCard(card, conf, delay=delay)
def _rescheduleLrnCard(
- self, card: Card, conf: QueueConfig, delay: Optional[int] = None
+ self, card: Card, conf: QueueConfig, delay: int | None = None
) -> Any:
# normal delay for the current step?
if delay is None:
@@ -690,9 +690,9 @@ limit ?"""
def _leftToday(
self,
- delays: List[int],
+ delays: list[int],
left: int,
- now: Optional[int] = None,
+ now: int | None = None,
) -> int:
"The number of steps that can be completed by the day cutoff."
if not now:
@@ -927,7 +927,7 @@ limit ?"""
min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max)
- def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
+ def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
if ivl < 2:
return (1, 1)
elif ivl == 2:
@@ -1080,7 +1080,7 @@ limit ?"""
##########################################################################
def _burySiblings(self, card: Card) -> None:
- toBury: List[CardId] = []
+ toBury: list[CardId] = []
nconf = self._newConf(card)
buryNew = nconf.get("bury", True)
rconf = self._revConf(card)
@@ -1115,7 +1115,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
# Review-related UI helpers
##########################################################################
- def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
+ def counts(self, card: Card | None = None) -> tuple[int, int, int]:
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
idx = self.countIdx(card)
diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py
index 60463dbce..2850a2b84 100644
--- a/pylib/anki/scheduler/v3.py
+++ b/pylib/anki/scheduler/v3.py
@@ -12,7 +12,7 @@ as '2' internally.
from __future__ import annotations
-from typing import List, Literal, Sequence, Tuple
+from typing import Literal, Optional, Sequence
from anki import scheduler_pb2
from anki.cards import Card
@@ -113,7 +113,7 @@ class Scheduler(SchedulerBaseWithLegacy):
"Don't use this, it is a stop-gap until this code is refactored."
return not self.get_queued_cards().cards
- def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
+ def counts(self, card: Optional[Card] = None) -> tuple[int, int, int]:
info = self.get_queued_cards()
return (info.new_count, info.learning_count, info.review_count)
@@ -230,7 +230,7 @@ class Scheduler(SchedulerBaseWithLegacy):
# called by col.decks.active(), which add-ons are using
@property
- def active_decks(self) -> List[DeckId]:
+ def active_decks(self) -> list[DeckId]:
try:
return self.col.db.list("select id from active_decks")
except DBError:
diff --git a/pylib/anki/sound.py b/pylib/anki/sound.py
index a3bcfe99d..3d375f716 100644
--- a/pylib/anki/sound.py
+++ b/pylib/anki/sound.py
@@ -11,7 +11,7 @@ from __future__ import annotations
import re
from dataclasses import dataclass
-from typing import List, Union
+from typing import Union
@dataclass
@@ -23,10 +23,10 @@ class TTSTag:
field_text: str
lang: str
- voices: List[str]
+ voices: list[str]
speed: float
# each arg should be in the form 'foo=bar'
- other_args: List[str]
+ other_args: list[str]
@dataclass
diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py
index 9ef02c026..04b3fe33e 100644
--- a/pylib/anki/stats.py
+++ b/pylib/anki/stats.py
@@ -8,7 +8,7 @@ from __future__ import annotations
import datetime
import json
import time
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+from typing import Sequence
import anki.cards
import anki.collection
@@ -37,12 +37,12 @@ class CardStats:
# legacy
- def addLine(self, k: str, v: Union[int, str]) -> None:
+ def addLine(self, k: str, v: int | str) -> None:
self.txt += self.makeLine(k, v)
- def makeLine(self, k: str, v: Union[str, int]) -> str:
+ def makeLine(self, k: str, v: str | int) -> str:
txt = "
"
- txt += "%s | %s |
" % (k, v)
+ txt += f"{k}{v} | "
return txt
def date(self, tm: float) -> str:
@@ -183,7 +183,7 @@ from revlog where id > ? """
# Due and cumulative due
######################################################################
- def get_start_end_chunk(self, by: str = "review") -> Tuple[int, Optional[int], int]:
+ def get_start_end_chunk(self, by: str = "review") -> tuple[int, int | None, int]:
start = 0
if self.type == PERIOD_MONTH:
end, chunk = 31, 1
@@ -245,7 +245,7 @@ from revlog where id > ? """
return txt
def _dueInfo(self, tot: int, num: int) -> str:
- i: List[str] = []
+ i: list[str] = []
self._line(
i,
"Total",
@@ -264,7 +264,7 @@ and due = ?"""
return self._lineTbl(i)
def _due(
- self, start: Optional[int] = None, end: Optional[int] = None, chunk: int = 1
+ self, start: int | None = None, end: int | None = None, chunk: int = 1
) -> Any:
lim = ""
if start is not None:
@@ -293,7 +293,7 @@ group by day order by day"""
data = self._added(days, chunk)
if not data:
return ""
- conf: Dict[str, Any] = dict(
+ conf: dict[str, Any] = dict(
xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right", min=0)],
)
@@ -311,12 +311,12 @@ group by day order by day"""
txt = self._title("Added", "The number of new cards you have added.")
txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards")
# total and per day average
- tot = sum([i[1] for i in data])
+ tot = sum(i[1] for i in data)
period = self._periodDays()
if not period:
# base off date of earliest added card
period = self._deckAge("add")
- i: List[str] = []
+ i: list[str] = []
self._line(i, "Total", "%d cards" % tot)
self._line(i, "Average", self._avgDay(tot, period, "cards"))
txt += self._lineTbl(i)
@@ -328,7 +328,7 @@ group by day order by day"""
data = self._done(days, chunk)
if not data:
return ""
- conf: Dict[str, Any] = dict(
+ conf: dict[str, Any] = dict(
xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right", min=0)],
)
@@ -384,20 +384,20 @@ group by day order by day"""
def _ansInfo(
self,
- totd: List[Tuple[int, float]],
+ totd: list[tuple[int, float]],
studied: int,
first: int,
unit: str,
convHours: bool = False,
- total: Optional[int] = None,
- ) -> Tuple[str, int]:
+ total: int | None = None,
+ ) -> tuple[str, int]:
assert totd
tot = totd[-1][1]
period = self._periodDays()
if not period:
# base off earliest repetition date
period = self._deckAge("review")
- i: List[str] = []
+ i: list[str] = []
self._line(
i,
"Days studied",
@@ -432,12 +432,12 @@ group by day order by day"""
def _splitRepData(
self,
- data: List[Tuple[Any, ...]],
- spec: Sequence[Tuple[int, str, str]],
- ) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]:
- sep: Dict[int, Any] = {}
+ data: list[tuple[Any, ...]],
+ spec: Sequence[tuple[int, str, str]],
+ ) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
+ sep: dict[int, Any] = {}
totcnt = {}
- totd: Dict[int, Any] = {}
+ totd: dict[int, Any] = {}
alltot = []
allcnt: float = 0
for (n, col, lab) in spec:
@@ -471,7 +471,7 @@ group by day order by day"""
)
return (ret, alltot)
- def _added(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
+ def _added(self, num: int | None = 7, chunk: int = 1) -> Any:
lims = []
if num is not None:
lims.append(
@@ -498,7 +498,7 @@ group by day order by day"""
chunk,
)
- def _done(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
+ def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
lims = []
if num is not None:
lims.append(
@@ -605,12 +605,12 @@ group by day order by day)"""
yaxes=[dict(), dict(position="right", max=105)],
),
)
- i: List[str] = []
+ i: list[str] = []
self._line(i, "Average interval", self.col.format_timespan(avg * 86400))
self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400))
return txt + self._lineTbl(i)
- def _ivls(self) -> Tuple[List[Any], int]:
+ def _ivls(self) -> tuple[list[Any], int]:
start, end, chunk = self.get_start_end_chunk()
lim = "and grp <= %d" % end if end else ""
data = [
@@ -643,7 +643,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
# 3 + 4 + 4 + spaces on sides and middle = 15
# yng starts at 1+3+1 = 5
# mtr starts at 5+4+1 = 10
- d: Dict[str, List] = {"lrn": [], "yng": [], "mtr": []}
+ d: dict[str, list] = {"lrn": [], "yng": [], "mtr": []}
types = ("lrn", "yng", "mtr")
eases = self._eases()
for (type, ease, cnt) in eases:
@@ -685,7 +685,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
txt += self._easeInfo(eases)
return txt
- def _easeInfo(self, eases: List[Tuple[int, int, int]]) -> str:
+ def _easeInfo(self, eases: list[tuple[int, int, int]]) -> str:
types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
for (type, ease, cnt) in eases:
if ease == 1:
@@ -752,7 +752,7 @@ order by thetype, ease"""
shifted = []
counts = []
mcount = 0
- trend: List[Tuple[int, int]] = []
+ trend: list[tuple[int, int]] = []
peak = 0
for d in data:
hour = (d[0] - 4) % 24
@@ -852,9 +852,9 @@ group by hour having count() > 30 order by hour"""
("Suspended+Buried", colSusp),
)
):
- d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col))
+ d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col))
# text data
- i: List[str] = []
+ i: list[str] = []
(c, f) = self.col.db.first(
"""
select count(id), count(distinct nid) from cards
@@ -880,9 +880,7 @@ when you answer "good" on a review."""
)
return txt
- def _line(
- self, i: List[str], a: str, b: Union[int, str], bold: bool = True
- ) -> None:
+ def _line(self, i: list[str], a: str, b: int | str, bold: bool = True) -> None:
# T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
colon = ":"
if bold:
@@ -896,7 +894,7 @@ when you answer "good" on a review."""
% (a, colon, b)
)
- def _lineTbl(self, i: List[str]) -> str:
+ def _lineTbl(self, i: list[str]) -> str:
return ""
def _factors(self) -> Any:
@@ -945,7 +943,7 @@ from cards where did in %s"""
self,
id: str,
data: Any,
- conf: Optional[Any] = None,
+ conf: Any | None = None,
type: str = "bars",
xunit: int = 1,
ylabel: str = "Cards",
@@ -1069,7 +1067,7 @@ $(function () {
)
def _title(self, title: str, subtitle: str = "") -> str:
- return "%s
%s" % (title, subtitle)
+ return f"{title}
{subtitle}"
def _deckAge(self, by: str) -> int:
lim = self._revlogLimit()
@@ -1089,7 +1087,7 @@ $(function () {
period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400)))
return period
- def _periodDays(self) -> Optional[int]:
+ def _periodDays(self) -> int | None:
start, end, chunk = self.get_start_end_chunk()
if end is None:
return None
diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py
index eb5dad1a2..ff93f5bb4 100644
--- a/pylib/anki/stdmodels.py
+++ b/pylib/anki/stdmodels.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import Any, Callable, List, Tuple
+from typing import Any, Callable
import anki.collection
import anki.models
@@ -15,7 +15,7 @@ StockNotetypeKind = notetypes_pb2.StockNotetype.Kind
# add-on authors can add ("note type name", function)
# to this list to have it shown in the add/clone note type screen
-models: List[Tuple] = []
+models: list[tuple] = []
def _get_stock_notetype(
@@ -26,9 +26,9 @@ def _get_stock_notetype(
def get_stock_notetypes(
col: anki.collection.Collection,
-) -> List[Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]:
- out: List[
- Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
+) -> list[tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]:
+ out: list[
+ tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
] = []
# add standard
for kind in [
diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py
index 9e6852eda..2e7c2a916 100644
--- a/pylib/anki/syncserver/__init__.py
+++ b/pylib/anki/syncserver/__init__.py
@@ -116,7 +116,7 @@ def after_full_sync() -> None:
def get_method(
method_str: str,
-) -> Optional[SyncServerMethodRequest.Method.V]: # pylint: disable=no-member
+) -> SyncServerMethodRequest.Method.V | None: # pylint: disable=no-member
s = method_str
if s == "hostKey":
return Method.HOST_KEY
diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py
index c1fd64b2a..425987ebc 100644
--- a/pylib/anki/tags.py
+++ b/pylib/anki/tags.py
@@ -13,7 +13,7 @@ from __future__ import annotations
import pprint
import re
-from typing import Collection, List, Match, Optional, Sequence
+from typing import Collection, Match, Sequence
import anki # pylint: disable=unused-import
import anki.collection
@@ -33,7 +33,7 @@ class TagManager:
self.col = col.weakref()
# legacy add-on code expects a List return type
- def all(self) -> List[str]:
+ def all(self) -> list[str]:
return list(self.col._backend.all_tags())
def __repr__(self) -> str:
@@ -50,7 +50,7 @@ class TagManager:
def clear_unused_tags(self) -> OpChangesWithCount:
return self.col._backend.clear_unused_tags()
- def byDeck(self, did: DeckId, children: bool = False) -> List[str]:
+ def byDeck(self, did: DeckId, children: bool = False) -> list[str]:
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children:
query = f"{basequery} AND c.did=?"
@@ -123,11 +123,11 @@ class TagManager:
# String-based utilities
##########################################################################
- def split(self, tags: str) -> List[str]:
+ def split(self, tags: str) -> list[str]:
"Parse a string and return a list of tags."
return [t for t in tags.replace("\u3000", " ").split(" ") if t]
- def join(self, tags: List[str]) -> str:
+ def join(self, tags: list[str]) -> str:
"Join tags into a single string, with leading and trailing spaces."
if not tags:
return ""
@@ -164,30 +164,30 @@ class TagManager:
##########################################################################
# this is now a no-op - the tags are canonified when the note is saved
- def canonify(self, tagList: List[str]) -> List[str]:
+ def canonify(self, tagList: list[str]) -> list[str]:
return tagList
- def inList(self, tag: str, tags: List[str]) -> bool:
+ def inList(self, tag: str, tags: list[str]) -> bool:
"True if TAG is in TAGS. Ignore case."
return tag.lower() in [t.lower() for t in tags]
# legacy
##########################################################################
- def registerNotes(self, nids: Optional[List[int]] = None) -> None:
+ def registerNotes(self, nids: list[int] | None = None) -> None:
self.clear_unused_tags()
def register(
- self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
+ self, tags: Collection[str], usn: int | None = None, clear: bool = False
) -> None:
print("tags.register() is deprecated and no longer works")
- def bulkAdd(self, ids: List[NoteId], tags: str, add: bool = True) -> None:
+ def bulkAdd(self, ids: list[NoteId], tags: str, add: bool = True) -> None:
"Add tags in bulk. TAGS is space-separated."
if add:
self.bulk_add(ids, tags)
else:
self.bulk_remove(ids, tags)
- def bulkRem(self, ids: List[NoteId], tags: str) -> None:
+ def bulkRem(self, ids: list[NoteId], tags: str) -> None:
self.bulkAdd(ids, tags, False)
diff --git a/pylib/anki/template.py b/pylib/anki/template.py
index ab4a57a4b..b4d4fb206 100644
--- a/pylib/anki/template.py
+++ b/pylib/anki/template.py
@@ -29,7 +29,7 @@ template_legacy.py file, using the legacy addHook() system.
from __future__ import annotations
from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+from typing import Any, Sequence, Union
import anki
from anki import card_rendering_pb2, hooks
@@ -50,10 +50,10 @@ CARD_BLANK_HELP = (
class TemplateReplacement:
field_name: str
current_text: str
- filters: List[str]
+ filters: list[str]
-TemplateReplacementList = List[Union[str, TemplateReplacement]]
+TemplateReplacementList = list[Union[str, TemplateReplacement]]
@dataclass
@@ -105,7 +105,7 @@ def av_tag_to_native(tag: card_rendering_pb2.AVTag) -> AVTag:
)
-def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> List[AVTag]:
+def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> list[AVTag]:
return list(map(av_tag_to_native, tags))
@@ -125,7 +125,7 @@ class TemplateRenderContext:
note: Note,
card: Card,
notetype: NotetypeDict,
- template: Dict,
+ template: dict,
fill_empty: bool,
) -> TemplateRenderContext:
return TemplateRenderContext(
@@ -144,7 +144,7 @@ class TemplateRenderContext:
note: Note,
browser: bool = False,
notetype: NotetypeDict = None,
- template: Optional[Dict] = None,
+ template: dict | None = None,
fill_empty: bool = False,
) -> None:
self._col = col.weakref()
@@ -153,7 +153,7 @@ class TemplateRenderContext:
self._browser = browser
self._template = template
self._fill_empty = fill_empty
- self._fields: Optional[Dict] = None
+ self._fields: dict | None = None
self._latex_svg = False
if not notetype:
self._note_type = note.note_type()
@@ -162,12 +162,12 @@ class TemplateRenderContext:
# if you need to store extra state to share amongst rendering
# hooks, you can insert it into this dictionary
- self.extra_state: Dict[str, Any] = {}
+ self.extra_state: dict[str, Any] = {}
def col(self) -> anki.collection.Collection:
return self._col
- def fields(self) -> Dict[str, str]:
+ def fields(self) -> dict[str, str]:
print(".fields() is obsolete, use .note() or .card()")
if not self._fields:
# fields from note
@@ -269,8 +269,8 @@ class TemplateRenderOutput:
"Stores the rendered templates and extracted AV tags."
question_text: str
answer_text: str
- question_av_tags: List[AVTag]
- answer_av_tags: List[AVTag]
+ question_av_tags: list[AVTag]
+ answer_av_tags: list[AVTag]
css: str = ""
def question_and_style(self) -> str:
@@ -281,7 +281,7 @@ class TemplateRenderOutput:
# legacy
-def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
+def templates_for_card(card: Card, browser: bool) -> tuple[str, str]:
template = card.template()
if browser:
q, a = template.get("bqfmt"), template.get("bafmt")
@@ -295,7 +295,7 @@ def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
def apply_custom_filters(
rendered: TemplateReplacementList,
ctx: TemplateRenderContext,
- front_side: Optional[str],
+ front_side: str | None,
) -> str:
"Complete rendering by applying any pending custom filters."
# template already fully rendered?
diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py
index e59629af0..3f0a7f476 100644
--- a/pylib/anki/utils.py
+++ b/pylib/anki/utils.py
@@ -17,11 +17,11 @@ import time
import traceback
from contextlib import contextmanager
from hashlib import sha1
-from typing import Any, Iterable, Iterator, List, Optional, Union
+from typing import Any, Iterable, Iterator
from anki.dbproxy import DBProxy
-_tmpdir: Optional[str]
+_tmpdir: str | None
try:
# pylint: disable=c-extension-no-member
@@ -89,7 +89,7 @@ def htmlToTextLine(s: str) -> str:
##############################################################################
-def ids2str(ids: Iterable[Union[int, str]]) -> str:
+def ids2str(ids: Iterable[int | str]) -> str:
"""Given a list of integers, return a string '(int1,int2,...)'."""
return f"({','.join(str(i) for i in ids)})"
@@ -140,11 +140,11 @@ def guid64() -> str:
##############################################################################
-def joinFields(list: List[str]) -> str:
+def joinFields(list: list[str]) -> str:
return "\x1f".join(list)
-def splitFields(string: str) -> List[str]:
+def splitFields(string: str) -> list[str]:
return string.split("\x1f")
@@ -152,7 +152,7 @@ def splitFields(string: str) -> List[str]:
##############################################################################
-def checksum(data: Union[bytes, str]) -> str:
+def checksum(data: bytes | str) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
return sha1(data).hexdigest()
@@ -218,7 +218,7 @@ def noBundledLibs() -> Iterator[None]:
os.environ["LD_LIBRARY_PATH"] = oldlpath
-def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int:
+def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
"Execute a command. If WAIT, return exit code."
# ensure we don't open a separate window for forking process on windows
if isWin:
@@ -262,7 +262,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ':*?"<>|'
-def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
+def invalidFilename(str: str, dirsep: bool = True) -> str | None:
for c in invalidFilenameChars:
if c in str:
return c
diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py
index f201ae47c..dbad433b5 100644
--- a/pylib/tests/test_models.py
+++ b/pylib/tests/test_models.py
@@ -268,7 +268,9 @@ def test_chained_mods():
a1 = "sentence"
q2 = 'en chaine'
a2 = "chained"
- note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
+ note[
+ "Text"
+ ] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format(
q1,
a1,
q2,
diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py
index fbf5cbb0c..98af76223 100644
--- a/pylib/tests/test_schedv2.py
+++ b/pylib/tests/test_schedv2.py
@@ -4,7 +4,7 @@
import copy
import os
import time
-from typing import Tuple
+from typing import Dict
import pytest
@@ -433,7 +433,7 @@ def test_reviews():
assert "leech" in c.note().tags
-def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]:
+def review_limits_setup() -> tuple[anki.collection.Collection, Dict]:
col = getEmptyCol()
parent = col.decks.get(col.decks.id("parent"))
diff --git a/pylib/tools/hookslib.py b/pylib/tools/hookslib.py
index c4331b22b..fb601f231 100644
--- a/pylib/tools/hookslib.py
+++ b/pylib/tools/hookslib.py
@@ -21,7 +21,7 @@ class Hook:
name: str
# string of the typed arguments passed to the callback, eg
# ["kind: str", "val: int"]
- args: List[str] = None
+ args: list[str] = None
# string of the return type. if set, hook is a filter.
return_type: Optional[str] = None
# if add-ons may be relying on the legacy hook name, add it here
@@ -41,7 +41,7 @@ class Hook:
types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
- def arg_names(self) -> List[str]:
+ def arg_names(self) -> list[str]:
names = []
for arg in self.args or []:
if not arg:
@@ -64,7 +64,7 @@ class Hook:
def list_code(self) -> str:
return f"""\
- _hooks: List[{self.callable()}] = []
+ _hooks: list[{self.callable()}] = []
"""
def code(self) -> str:
@@ -153,7 +153,7 @@ class {self.classname()}:
return f"{out}\n\n"
-def write_file(path: str, hooks: List[Hook], prefix: str, suffix: str):
+def write_file(path: str, hooks: list[Hook], prefix: str, suffix: str):
hooks.sort(key=attrgetter("name"))
code = f"{prefix}\n"
for hook in hooks:
diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py
index 2a84a181c..c74149823 100644
--- a/qt/aqt/__init__.py
+++ b/qt/aqt/__init__.py
@@ -1,6 +1,8 @@
# 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 argparse
import builtins
import cProfile
@@ -10,7 +12,7 @@ import os
import sys
import tempfile
import traceback
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
+from typing import Any, Callable, Optional, cast
import anki.lang
from anki._backend import RustBackend
@@ -90,7 +92,7 @@ from aqt import stats, about, preferences, mediasync # isort:skip
class DialogManager:
- _dialogs: Dict[str, list] = {
+ _dialogs: dict[str, list] = {
"AddCards": [addcards.AddCards, None],
"AddonsDialog": [addons.AddonsDialog, None],
"Browser": [browser.Browser, None],
@@ -267,7 +269,7 @@ class AnkiApp(QApplication):
KEY = f"anki{checksum(getpass.getuser())}"
TMOUT = 30000
- def __init__(self, argv: List[str]) -> None:
+ def __init__(self, argv: list[str]) -> None:
QApplication.__init__(self, argv)
self._argv = argv
@@ -328,7 +330,7 @@ class AnkiApp(QApplication):
return QApplication.event(self, evt)
-def parseArgs(argv: List[str]) -> Tuple[argparse.Namespace, List[str]]:
+def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
"Returns (opts, args)."
# py2app fails to strip this in some instances, then anki dies
# as there's no such profile
@@ -444,7 +446,7 @@ def run() -> None:
)
-def _run(argv: Optional[List[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
+def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
"""Start AnkiQt application or reuse an existing instance if one exists.
If the function is invoked with exec=False, the AnkiQt will not enter
diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py
index 56bcc28a2..1e418bfe7 100644
--- a/qt/aqt/addcards.py
+++ b/qt/aqt/addcards.py
@@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import Callable, List, Optional
+from typing import Callable, Optional
import aqt.editor
import aqt.forms
@@ -50,7 +50,7 @@ class AddCards(QDialog):
self.setupEditor()
self.setupButtons()
self._load_new_note()
- self.history: List[NoteId] = []
+ self.history: list[NoteId] = []
self._last_added_note: Optional[Note] = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add")
diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py
index 3db0dd9db..12ffe8fcc 100644
--- a/qt/aqt/addons.py
+++ b/qt/aqt/addons.py
@@ -11,7 +11,7 @@ from collections import defaultdict
from concurrent.futures import Future
from dataclasses import dataclass
from datetime import datetime
-from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
+from typing import IO, Any, Callable, Iterable, Union
from urllib.parse import parse_qs, urlparse
from zipfile import ZipFile
@@ -53,7 +53,7 @@ class AbortAddonImport(Exception):
@dataclass
class InstallOk:
name: str
- conflicts: List[str]
+ conflicts: list[str]
compatible: bool
@@ -75,13 +75,13 @@ class DownloadOk:
@dataclass
class DownloadError:
# set if result was not 200
- status_code: Optional[int] = None
+ status_code: int | None = None
# set if an exception occurred
- exception: Optional[Exception] = None
+ exception: Exception | None = None
# first arg is add-on id
-DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]]
+DownloadLogEntry = tuple[int, Union[DownloadError, InstallError, InstallOk]]
@dataclass
@@ -101,21 +101,21 @@ current_point_version = anki.utils.pointVersion()
@dataclass
class AddonMeta:
dir_name: str
- provided_name: Optional[str]
+ provided_name: str | None
enabled: bool
installed_at: int
- conflicts: List[str]
+ conflicts: list[str]
min_point_version: int
max_point_version: int
branch_index: int
- human_version: Optional[str]
+ human_version: str | None
update_enabled: bool
- homepage: Optional[str]
+ homepage: str | None
def human_name(self) -> str:
return self.provided_name or self.dir_name
- def ankiweb_id(self) -> Optional[int]:
+ def ankiweb_id(self) -> int | None:
m = ANKIWEB_ID_RE.match(self.dir_name)
if m:
return int(m.group(0))
@@ -134,13 +134,13 @@ class AddonMeta:
def is_latest(self, server_update_time: int) -> bool:
return self.installed_at >= server_update_time
- def page(self) -> Optional[str]:
+ def page(self) -> str | None:
if self.ankiweb_id():
return f"{aqt.appShared}info/{self.dir_name}"
return self.homepage
@staticmethod
- def from_json_meta(dir_name: str, json_meta: Dict[str, Any]) -> AddonMeta:
+ def from_json_meta(dir_name: str, json_meta: dict[str, Any]) -> AddonMeta:
return AddonMeta(
dir_name=dir_name,
provided_name=json_meta.get("name"),
@@ -207,7 +207,7 @@ class AddonManager:
sys.path.insert(0, self.addonsFolder())
# in new code, you may want all_addon_meta() instead
- def allAddons(self) -> List[str]:
+ def allAddons(self) -> list[str]:
l = []
for d in os.listdir(self.addonsFolder()):
path = self.addonsFolder(d)
@@ -222,7 +222,7 @@ class AddonManager:
def all_addon_meta(self) -> Iterable[AddonMeta]:
return map(self.addon_meta, self.allAddons())
- def addonsFolder(self, dir: Optional[str] = None) -> str:
+ def addonsFolder(self, dir: str | None = None) -> str:
root = self.mw.pm.addonFolder()
if dir is None:
return root
@@ -280,7 +280,7 @@ class AddonManager:
return os.path.join(self.addonsFolder(dir), "meta.json")
# in new code, use self.addon_meta() instead
- def addonMeta(self, dir: str) -> Dict[str, Any]:
+ def addonMeta(self, dir: str) -> dict[str, Any]:
path = self._addonMetaPath(dir)
try:
with open(path, encoding="utf8") as f:
@@ -293,12 +293,12 @@ class AddonManager:
return dict()
# in new code, use write_addon_meta() instead
- def writeAddonMeta(self, dir: str, meta: Dict[str, Any]) -> None:
+ def writeAddonMeta(self, dir: str, meta: dict[str, Any]) -> None:
path = self._addonMetaPath(dir)
with open(path, "w", encoding="utf8") as f:
json.dump(meta, f)
- def toggleEnabled(self, dir: str, enable: Optional[bool] = None) -> None:
+ def toggleEnabled(self, dir: str, enable: bool | None = None) -> None:
addon = self.addon_meta(dir)
should_enable = enable if enable is not None else not addon.enabled
if should_enable is True:
@@ -316,7 +316,7 @@ class AddonManager:
addon.enabled = should_enable
self.write_addon_meta(addon)
- def ankiweb_addons(self) -> List[int]:
+ def ankiweb_addons(self) -> list[int]:
ids = []
for meta in self.all_addon_meta():
if meta.ankiweb_id() is not None:
@@ -332,7 +332,7 @@ class AddonManager:
def addonName(self, dir: str) -> str:
return self.addon_meta(dir).human_name()
- def addonConflicts(self, dir: str) -> List[str]:
+ def addonConflicts(self, dir: str) -> list[str]:
return self.addon_meta(dir).conflicts
def annotatedName(self, dir: str) -> str:
@@ -345,8 +345,8 @@ class AddonManager:
# Conflict resolution
######################################################################
- def allAddonConflicts(self) -> Dict[str, List[str]]:
- all_conflicts: Dict[str, List[str]] = defaultdict(list)
+ def allAddonConflicts(self) -> dict[str, list[str]]:
+ all_conflicts: dict[str, list[str]] = defaultdict(list)
for addon in self.all_addon_meta():
if not addon.enabled:
continue
@@ -354,7 +354,7 @@ class AddonManager:
all_conflicts[other_dir].append(addon.dir_name)
return all_conflicts
- def _disableConflicting(self, dir: str, conflicts: List[str] = None) -> List[str]:
+ def _disableConflicting(self, dir: str, conflicts: list[str] = None) -> list[str]:
conflicts = conflicts or self.addonConflicts(dir)
installed = self.allAddons()
@@ -371,7 +371,7 @@ class AddonManager:
# Installing and deleting add-ons
######################################################################
- def readManifestFile(self, zfile: ZipFile) -> Dict[Any, Any]:
+ def readManifestFile(self, zfile: ZipFile) -> dict[Any, Any]:
try:
with zfile.open("manifest.json") as f:
data = json.loads(f.read())
@@ -385,8 +385,8 @@ class AddonManager:
return manifest
def install(
- self, file: Union[IO, str], manifest: Dict[str, Any] = None
- ) -> Union[InstallOk, InstallError]:
+ self, file: IO | str, manifest: dict[str, Any] = None
+ ) -> InstallOk | InstallError:
"""Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest'
dictionary"""
@@ -463,8 +463,8 @@ class AddonManager:
######################################################################
def processPackages(
- self, paths: List[str], parent: QWidget = None
- ) -> Tuple[List[str], List[str]]:
+ self, paths: list[str], parent: QWidget = None
+ ) -> tuple[list[str], list[str]]:
log = []
errs = []
@@ -493,7 +493,7 @@ class AddonManager:
def _installationErrorReport(
self, result: InstallError, base: str, mode: str = "download"
- ) -> List[str]:
+ ) -> list[str]:
messages = {
"zip": tr.addons_corrupt_addon_file(),
@@ -511,7 +511,7 @@ class AddonManager:
def _installationSuccessReport(
self, result: InstallOk, base: str, mode: str = "download"
- ) -> List[str]:
+ ) -> list[str]:
name = result.name or base
if mode == "download":
@@ -536,8 +536,8 @@ class AddonManager:
# Updating
######################################################################
- def extract_update_info(self, items: List[Dict]) -> List[UpdateInfo]:
- def extract_one(item: Dict) -> UpdateInfo:
+ def extract_update_info(self, items: list[dict]) -> list[UpdateInfo]:
+ def extract_one(item: dict) -> UpdateInfo:
id = item["id"]
meta = self.addon_meta(str(id))
branch_idx = meta.branch_index
@@ -545,7 +545,7 @@ class AddonManager:
return list(map(extract_one, items))
- def update_supported_versions(self, items: List[UpdateInfo]) -> None:
+ def update_supported_versions(self, items: list[UpdateInfo]) -> None:
for item in items:
self.update_supported_version(item)
@@ -581,7 +581,7 @@ class AddonManager:
if updated:
self.write_addon_meta(addon)
- def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]:
+ def updates_required(self, items: list[UpdateInfo]) -> list[UpdateInfo]:
"""Return ids of add-ons requiring an update."""
need_update = []
for item in items:
@@ -600,10 +600,10 @@ class AddonManager:
# Add-on Config
######################################################################
- _configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {}
- _configUpdatedActions: Dict[str, Callable[[Any], None]] = {}
+ _configButtonActions: dict[str, Callable[[], bool | None]] = {}
+ _configUpdatedActions: dict[str, Callable[[Any], None]] = {}
- def addonConfigDefaults(self, dir: str) -> Optional[Dict[str, Any]]:
+ def addonConfigDefaults(self, dir: str) -> dict[str, Any] | None:
path = os.path.join(self.addonsFolder(dir), "config.json")
try:
with open(path, encoding="utf8") as f:
@@ -622,7 +622,7 @@ class AddonManager:
def addonFromModule(self, module: str) -> str:
return module.split(".")[0]
- def configAction(self, addon: str) -> Callable[[], Optional[bool]]:
+ def configAction(self, addon: str) -> Callable[[], bool | None]:
return self._configButtonActions.get(addon)
def configUpdatedAction(self, addon: str) -> Callable[[Any], None]:
@@ -649,7 +649,7 @@ class AddonManager:
# Add-on Config API
######################################################################
- def getConfig(self, module: str) -> Optional[Dict[str, Any]]:
+ def getConfig(self, module: str) -> dict[str, Any] | None:
addon = self.addonFromModule(module)
# get default config
config = self.addonConfigDefaults(addon)
@@ -661,7 +661,7 @@ class AddonManager:
config.update(userConf)
return config
- def setConfigAction(self, module: str, fn: Callable[[], Optional[bool]]) -> None:
+ def setConfigAction(self, module: str, fn: Callable[[], bool | None]) -> None:
addon = self.addonFromModule(module)
self._configButtonActions[addon] = fn
@@ -700,7 +700,7 @@ class AddonManager:
# Web Exports
######################################################################
- _webExports: Dict[str, str] = {}
+ _webExports: dict[str, str] = {}
def setWebExports(self, module: str, pattern: str) -> None:
addon = self.addonFromModule(module)
@@ -825,18 +825,18 @@ class AddonsDialog(QDialog):
gui_hooks.addons_dialog_did_change_selected_addon(self, addon)
return
- def selectedAddons(self) -> List[str]:
+ def selectedAddons(self) -> list[str]:
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
return [self.addons[idx].dir_name for idx in idxs]
- def onlyOneSelected(self) -> Optional[str]:
+ def onlyOneSelected(self) -> str | None:
dirs = self.selectedAddons()
if len(dirs) != 1:
showInfo(tr.addons_please_select_a_single_addon_first())
return None
return dirs[0]
- def selected_addon_meta(self) -> Optional[AddonMeta]:
+ def selected_addon_meta(self) -> AddonMeta | None:
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
if len(idxs) != 1:
showInfo(tr.addons_please_select_a_single_addon_first())
@@ -887,14 +887,14 @@ class AddonsDialog(QDialog):
if obj.ids:
download_addons(self, self.mgr, obj.ids, self.after_downloading)
- def after_downloading(self, log: List[DownloadLogEntry]) -> None:
+ def after_downloading(self, log: list[DownloadLogEntry]) -> None:
self.redrawAddons()
if log:
show_log_to_user(self, log)
else:
tooltip(tr.addons_no_updates_available())
- def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
+ def onInstallFiles(self, paths: list[str] | None = None) -> bool | None:
if not paths:
key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})"
paths_ = getFile(
@@ -943,7 +943,7 @@ class GetAddons(QDialog):
self.addonsDlg = dlg
self.mgr = dlg.mgr
self.mw = self.mgr.mw
- self.ids: List[int] = []
+ self.ids: list[int] = []
self.form = aqt.forms.getaddons.Ui_Dialog()
self.form.setupUi(self)
b = self.form.buttonBox.addButton(
@@ -974,7 +974,7 @@ class GetAddons(QDialog):
######################################################################
-def download_addon(client: HttpClient, id: int) -> Union[DownloadOk, DownloadError]:
+def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
"Fetch a single add-on from AnkiWeb."
try:
resp = client.get(
@@ -1025,7 +1025,7 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
return meta
-def download_log_to_html(log: List[DownloadLogEntry]) -> str:
+def download_log_to_html(log: list[DownloadLogEntry]) -> str:
return "
".join(map(describe_log_entry, log))
@@ -1053,7 +1053,7 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
return buf
-def download_encountered_problem(log: List[DownloadLogEntry]) -> bool:
+def download_encountered_problem(log: list[DownloadLogEntry]) -> bool:
return any(not isinstance(e[1], InstallOk) for e in log)
@@ -1099,10 +1099,10 @@ class DownloaderInstaller(QObject):
self.client.progress_hook = bg_thread_progress
def download(
- self, ids: List[int], on_done: Callable[[List[DownloadLogEntry]], None]
+ self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None]
) -> None:
self.ids = ids
- self.log: List[DownloadLogEntry] = []
+ self.log: list[DownloadLogEntry] = []
self.dl_bytes = 0
self.last_tooltip = 0
@@ -1135,7 +1135,7 @@ class DownloaderInstaller(QObject):
self.mgr.mw.progress.timer(50, lambda: self.on_done(self.log), False)
-def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
+def show_log_to_user(parent: QWidget, log: list[DownloadLogEntry]) -> None:
have_problem = download_encountered_problem(log)
if have_problem:
@@ -1153,9 +1153,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
def download_addons(
parent: QWidget,
mgr: AddonManager,
- ids: List[int],
- on_done: Callable[[List[DownloadLogEntry]], None],
- client: Optional[HttpClient] = None,
+ ids: list[int],
+ on_done: Callable[[list[DownloadLogEntry]], None],
+ client: HttpClient | None = None,
) -> None:
if client is None:
client = HttpClient()
@@ -1174,7 +1174,7 @@ class ChooseAddonsToUpdateList(QListWidget):
self,
parent: QWidget,
mgr: AddonManager,
- updated_addons: List[UpdateInfo],
+ updated_addons: list[UpdateInfo],
) -> None:
QListWidget.__init__(self, parent)
self.mgr = mgr
@@ -1266,7 +1266,7 @@ class ChooseAddonsToUpdateList(QListWidget):
return
self.check_item(self.header_item, Qt.Checked)
- def get_selected_addon_ids(self) -> List[int]:
+ def get_selected_addon_ids(self) -> list[int]:
addon_ids = []
for i in range(1, self.count()):
item = self.item(i)
@@ -1286,7 +1286,7 @@ class ChooseAddonsToUpdateList(QListWidget):
class ChooseAddonsToUpdateDialog(QDialog):
def __init__(
- self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo]
+ self, parent: QWidget, mgr: AddonManager, updated_addons: list[UpdateInfo]
) -> None:
QDialog.__init__(self, parent)
self.setWindowTitle(tr.addons_choose_update_window_title())
@@ -1312,7 +1312,7 @@ class ChooseAddonsToUpdateDialog(QDialog):
layout.addWidget(button_box)
self.setLayout(layout)
- def ask(self) -> List[int]:
+ def ask(self) -> list[int]:
"Returns a list of selected addons' ids"
ret = self.exec_()
saveGeom(self, "addonsChooseUpdate")
@@ -1323,9 +1323,9 @@ class ChooseAddonsToUpdateDialog(QDialog):
return []
-def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
+def fetch_update_info(client: HttpClient, ids: list[int]) -> list[dict]:
"""Fetch update info from AnkiWeb in one or more batches."""
- all_info: List[Dict] = []
+ all_info: list[dict] = []
while ids:
# get another chunk
@@ -1340,7 +1340,7 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
def _fetch_update_info_batch(
client: HttpClient, chunk: Iterable[str]
-) -> Iterable[Dict]:
+) -> Iterable[dict]:
"""Get update info from AnkiWeb.
Chunk must not contain more than 25 ids."""
@@ -1354,21 +1354,21 @@ def _fetch_update_info_batch(
def check_and_prompt_for_updates(
parent: QWidget,
mgr: AddonManager,
- on_done: Callable[[List[DownloadLogEntry]], None],
+ on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True,
) -> None:
- def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
+ def on_updates_received(client: HttpClient, items: list[dict]) -> None:
handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
check_for_updates(mgr, on_updates_received)
def check_for_updates(
- mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None]
+ mgr: AddonManager, on_done: Callable[[HttpClient, list[dict]], None]
) -> None:
client = HttpClient()
- def check() -> List[Dict]:
+ def check() -> list[dict]:
return fetch_update_info(client, mgr.ankiweb_addons())
def update_info_received(future: Future) -> None:
@@ -1395,7 +1395,7 @@ def check_for_updates(
def extract_update_info(
- current_point_version: int, current_branch_idx: int, info_json: Dict
+ current_point_version: int, current_branch_idx: int, info_json: dict
) -> UpdateInfo:
"Process branches to determine the updated mod time and min/max versions."
branches = info_json["branches"]
@@ -1425,8 +1425,8 @@ def handle_update_info(
parent: QWidget,
mgr: AddonManager,
client: HttpClient,
- items: List[Dict],
- on_done: Callable[[List[DownloadLogEntry]], None],
+ items: list[dict],
+ on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True,
) -> None:
update_info = mgr.extract_update_info(items)
@@ -1445,8 +1445,8 @@ def prompt_to_update(
parent: QWidget,
mgr: AddonManager,
client: HttpClient,
- updated_addons: List[UpdateInfo],
- on_done: Callable[[List[DownloadLogEntry]], None],
+ updated_addons: list[UpdateInfo],
+ on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True,
) -> None:
if not requested_by_user:
@@ -1468,7 +1468,7 @@ def prompt_to_update(
class ConfigEditor(QDialog):
- def __init__(self, dlg: AddonsDialog, addon: str, conf: Dict) -> None:
+ def __init__(self, dlg: AddonsDialog, addon: str, conf: dict) -> None:
super().__init__(dlg)
self.addon = addon
self.conf = conf
@@ -1509,7 +1509,7 @@ class ConfigEditor(QDialog):
else:
self.form.scrollArea.setVisible(False)
- def updateText(self, conf: Dict[str, Any]) -> None:
+ def updateText(self, conf: dict[str, Any]) -> None:
text = json.dumps(
conf,
ensure_ascii=False,
@@ -1584,8 +1584,8 @@ class ConfigEditor(QDialog):
def installAddonPackages(
addonsManager: AddonManager,
- paths: List[str],
- parent: Optional[QWidget] = None,
+ paths: list[str],
+ parent: QWidget | None = None,
warn: bool = False,
strictly_modal: bool = False,
advise_restart: bool = False,
diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py
index 39341f84e..34a6133fe 100644
--- a/qt/aqt/browser/browser.py
+++ b/qt/aqt/browser/browser.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import Callable, Optional, Sequence, Tuple, Union
+from typing import Callable, Sequence
import aqt
import aqt.forms
@@ -89,14 +89,14 @@ class MockModel:
class Browser(QMainWindow):
mw: AnkiQt
col: Collection
- editor: Optional[Editor]
+ editor: Editor | None
table: Table
def __init__(
self,
mw: AnkiQt,
- card: Optional[Card] = None,
- search: Optional[Tuple[Union[str, SearchNode]]] = None,
+ card: Card | None = None,
+ search: tuple[str | SearchNode] | None = None,
) -> None:
"""
card -- try to select the provided card after executing "search" or
@@ -108,8 +108,8 @@ class Browser(QMainWindow):
self.mw = mw
self.col = self.mw.col
self.lastFilter = ""
- self.focusTo: Optional[int] = None
- self._previewer: Optional[Previewer] = None
+ self.focusTo: int | None = None
+ self._previewer: Previewer | None = None
self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog()
self.form.setupUi(self)
@@ -119,8 +119,8 @@ class Browser(QMainWindow):
restoreSplitter(self.form.splitter, "editor3")
self.form.splitter.setChildrenCollapsible(False)
# set if exactly 1 row is selected; used by the previewer
- self.card: Optional[Card] = None
- self.current_card: Optional[Card] = None
+ self.card: Card | None = None
+ self.current_card: Card | None = None
self.setup_table()
self.setupMenus()
self.setupHooks()
@@ -134,7 +134,7 @@ class Browser(QMainWindow):
self.setupSearch(card, search)
def on_operation_did_execute(
- self, changes: OpChanges, handler: Optional[object]
+ self, changes: OpChanges, handler: object | None
) -> None:
focused = current_window() == self
self.table.op_executed(changes, handler, focused)
@@ -161,7 +161,7 @@ class Browser(QMainWindow):
if changes.note_text or changes.card:
self._renderPreview()
- def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
+ def on_focus_change(self, new: QWidget | None, old: QWidget | None) -> None:
if current_window() == self:
self.setUpdatesEnabled(True)
self.table.redraw_cells()
@@ -263,8 +263,8 @@ class Browser(QMainWindow):
def reopen(
self,
_mw: AnkiQt,
- card: Optional[Card] = None,
- search: Optional[Tuple[Union[str, SearchNode]]] = None,
+ card: Card | None = None,
+ search: tuple[str | SearchNode] | None = None,
) -> None:
if search is not None:
self.search_for_terms(*search)
@@ -281,8 +281,8 @@ class Browser(QMainWindow):
def setupSearch(
self,
- card: Optional[Card] = None,
- search: Optional[Tuple[Union[str, SearchNode]]] = None,
+ card: Card | None = None,
+ search: tuple[str | SearchNode] | None = None,
) -> None:
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
@@ -310,7 +310,7 @@ class Browser(QMainWindow):
self.search_for(normed)
self.update_history()
- def search_for(self, search: str, prompt: Optional[str] = None) -> None:
+ def search_for(self, search: str, prompt: str | None = None) -> None:
"""Keep track of search string so that we reuse identical search when
refreshing, rather than whatever is currently in the search field.
Optionally set the search bar to a different text than the actual search.
@@ -354,12 +354,12 @@ class Browser(QMainWindow):
without_unicode_isolation(tr_title(total=cur, selected=selected))
)
- def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None:
+ def search_for_terms(self, *search_terms: str | SearchNode) -> None:
search = self.col.build_search_string(*search_terms)
self.form.searchEdit.setEditText(search)
self.onSearchActivated()
- def _default_search(self, card: Optional[Card] = None) -> None:
+ def _default_search(self, card: Card | None = None) -> None:
default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT)
if default.strip():
search = default
@@ -674,7 +674,7 @@ class Browser(QMainWindow):
@ensure_editor_saved
def add_tags_to_selected_notes(
self,
- tags: Optional[str] = None,
+ tags: str | None = None,
) -> None:
"Shows prompt if tags not provided."
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())):
@@ -686,7 +686,7 @@ class Browser(QMainWindow):
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
- def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None:
+ def remove_tags_from_selected_notes(self, tags: str | None = None) -> None:
"Shows prompt if tags not provided."
if not (
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete())
@@ -697,7 +697,7 @@ class Browser(QMainWindow):
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
).run_in_background(initiator=self)
- def _prompt_for_tags(self, prompt: str) -> Optional[str]:
+ def _prompt_for_tags(self, prompt: str) -> str | None:
(tags, ok) = getTag(self, self.col, prompt)
if not ok:
return None
diff --git a/qt/aqt/browser/find_and_replace.py b/qt/aqt/browser/find_and_replace.py
index 254c18be1..60b2f3d77 100644
--- a/qt/aqt/browser/find_and_replace.py
+++ b/qt/aqt/browser/find_and_replace.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import List, Optional, Sequence
+from typing import Sequence
import aqt
from anki.notes import NoteId
@@ -39,7 +39,7 @@ class FindAndReplaceDialog(QDialog):
*,
mw: AnkiQt,
note_ids: Sequence[NoteId],
- field: Optional[str] = None,
+ field: str | None = None,
) -> None:
"""
If 'field' is passed, only this is added to the field selector.
@@ -48,7 +48,7 @@ class FindAndReplaceDialog(QDialog):
super().__init__(parent)
self.mw = mw
self.note_ids = note_ids
- self.field_names: List[str] = []
+ self.field_names: list[str] = []
self._field = field
if field:
diff --git a/qt/aqt/browser/find_duplicates.py b/qt/aqt/browser/find_duplicates.py
index 69b1c7420..30bdbb13e 100644
--- a/qt/aqt/browser/find_duplicates.py
+++ b/qt/aqt/browser/find_duplicates.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import html
-from typing import Any, List, Optional, Tuple
+from typing import Any
import anki
import anki.find
@@ -45,7 +45,7 @@ class FindDuplicatesDialog(QDialog):
)
form.fields.addItems(fields)
restore_combo_index_for_session(form.fields, fields, "findDupesFields")
- self._dupesButton: Optional[QPushButton] = None
+ self._dupesButton: QPushButton | None = None
# links
form.webView.set_title("find duplicates")
@@ -75,7 +75,7 @@ class FindDuplicatesDialog(QDialog):
qconnect(search.clicked, on_click)
self.show()
- def show_duplicates_report(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
+ def show_duplicates_report(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
if not self._dupesButton:
self._dupesButton = b = self.form.buttonBox.addButton(
tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole
@@ -104,7 +104,7 @@ class FindDuplicatesDialog(QDialog):
text += ""
self.form.webView.stdHtml(text, context=self)
- def _tag_duplicates(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
+ def _tag_duplicates(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
if not dupes:
return
diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py
index b325655ae..d3a4c882d 100644
--- a/qt/aqt/browser/previewer.py
+++ b/qt/aqt/browser/previewer.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import json
import re
import time
-from typing import Any, Callable, Optional, Tuple, Union
+from typing import Any, Callable
import aqt.browser
from anki.cards import Card
@@ -33,14 +33,14 @@ from aqt.theme import theme_manager
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
from aqt.webview import AnkiWebView
-LastStateAndMod = Tuple[str, int, int]
+LastStateAndMod = tuple[str, int, int]
class Previewer(QDialog):
- _last_state: Optional[LastStateAndMod] = None
+ _last_state: LastStateAndMod | None = None
_card_changed = False
- _last_render: Union[int, float] = 0
- _timer: Optional[QTimer] = None
+ _last_render: int | float = 0
+ _timer: QTimer | None = None
_show_both_sides = False
def __init__(
@@ -57,7 +57,7 @@ class Previewer(QDialog):
disable_help_button(self)
self.setWindowIcon(icon)
- def card(self) -> Optional[Card]:
+ def card(self) -> Card | None:
raise NotImplementedError
def card_changed(self) -> bool:
@@ -143,7 +143,7 @@ class Previewer(QDialog):
if cmd.startswith("play:"):
play_clicked_audio(cmd, self.card())
- def _update_flag_and_mark_icons(self, card: Optional[Card]) -> None:
+ def _update_flag_and_mark_icons(self, card: Card | None) -> None:
if card:
flag = card.user_flag()
marked = card.note(reload=True).has_tag(MARKED_TAG)
@@ -247,7 +247,7 @@ class Previewer(QDialog):
self._state = "question"
self.render_card()
- def _state_and_mod(self) -> Tuple[str, int, int]:
+ def _state_and_mod(self) -> tuple[str, int, int]:
c = self.card()
n = c.note()
n.load()
@@ -258,7 +258,7 @@ class Previewer(QDialog):
class MultiCardPreviewer(Previewer):
- def card(self) -> Optional[Card]:
+ def card(self) -> Card | None:
# need to state explicitly it's not implement to avoid W0223
raise NotImplementedError
@@ -321,14 +321,14 @@ class MultiCardPreviewer(Previewer):
class BrowserPreviewer(MultiCardPreviewer):
_last_card_id = 0
- _parent: Optional[aqt.browser.Browser]
+ _parent: aqt.browser.Browser | None
def __init__(
self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None]
) -> None:
super().__init__(parent=parent, mw=mw, on_close=on_close)
- def card(self) -> Optional[Card]:
+ def card(self) -> Card | None:
if self._parent.singleCard:
return self._parent.card
else:
diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py
index 935719401..085b43604 100644
--- a/qt/aqt/browser/sidebar/item.py
+++ b/qt/aqt/browser/sidebar/item.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from enum import Enum, auto
-from typing import Callable, Iterable, List, Optional, Union
+from typing import Callable, Iterable
from anki.collection import SearchNode
from aqt.theme import ColoredIcon
@@ -59,8 +59,8 @@ class SidebarItem:
def __init__(
self,
name: str,
- icon: Union[str, ColoredIcon],
- search_node: Optional[SearchNode] = None,
+ icon: str | ColoredIcon,
+ search_node: SearchNode | None = None,
on_expanded: Callable[[bool], None] = None,
expanded: bool = False,
item_type: SidebarItemType = SidebarItemType.CUSTOM,
@@ -75,24 +75,24 @@ class SidebarItem:
self.id = id
self.search_node = search_node
self.on_expanded = on_expanded
- self.children: List["SidebarItem"] = []
- self.tooltip: Optional[str] = None
- self._parent_item: Optional["SidebarItem"] = None
+ self.children: list[SidebarItem] = []
+ self.tooltip: str | None = None
+ self._parent_item: SidebarItem | None = None
self._expanded = expanded
- self._row_in_parent: Optional[int] = None
+ self._row_in_parent: int | None = None
self._search_matches_self = False
self._search_matches_child = False
- def add_child(self, cb: "SidebarItem") -> None:
+ def add_child(self, cb: SidebarItem) -> None:
self.children.append(cb)
cb._parent_item = self
def add_simple(
self,
name: str,
- icon: Union[str, ColoredIcon],
+ icon: str | ColoredIcon,
type: SidebarItemType,
- search_node: Optional[SearchNode],
+ search_node: SearchNode | None,
) -> SidebarItem:
"Add child sidebar item, and return it."
item = SidebarItem(
diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py
index 92a282fd3..0f9c53fc3 100644
--- a/qt/aqt/browser/sidebar/toolbar.py
+++ b/qt/aqt/browser/sidebar/toolbar.py
@@ -4,7 +4,6 @@
from __future__ import annotations
from enum import Enum, auto
-from typing import Callable, Tuple
import aqt
from aqt.qt import *
@@ -18,7 +17,7 @@ class SidebarTool(Enum):
class SidebarToolbar(QToolBar):
- _tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = (
+ _tools: tuple[tuple[SidebarTool, str, Callable[[], str]], ...] = (
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
)
diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py
index 0c68c3d98..28da752b8 100644
--- a/qt/aqt/browser/sidebar/tree.py
+++ b/qt/aqt/browser/sidebar/tree.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from enum import Enum, auto
-from typing import Dict, Iterable, List, Optional, Tuple, cast
+from typing import Iterable, cast
import aqt
from anki.collection import (
@@ -75,8 +75,8 @@ class SidebarTreeView(QTreeView):
self.browser = browser
self.mw = browser.mw
self.col = self.mw.col
- self.current_search: Optional[str] = None
- self.valid_drop_types: Tuple[SidebarItemType, ...] = ()
+ self.current_search: str | None = None
+ self.valid_drop_types: tuple[SidebarItemType, ...] = ()
self._refresh_needed = False
self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -140,7 +140,7 @@ class SidebarTreeView(QTreeView):
###########################
def op_executed(
- self, changes: OpChanges, handler: Optional[object], focused: bool
+ self, changes: OpChanges, handler: object | None, focused: bool
) -> None:
if changes.browser_sidebar and not handler is self:
self._refresh_needed = True
@@ -198,9 +198,9 @@ class SidebarTreeView(QTreeView):
def find_item(
self,
is_target: Callable[[SidebarItem], bool],
- parent: Optional[SidebarItem] = None,
- ) -> Optional[SidebarItem]:
- def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]:
+ parent: SidebarItem | None = None,
+ ) -> SidebarItem | None:
+ def find_item_rec(parent: SidebarItem) -> SidebarItem | None:
if is_target(parent):
return parent
for child in parent.children:
@@ -226,7 +226,7 @@ class SidebarTreeView(QTreeView):
def _expand_where_necessary(
self,
model: SidebarModel,
- parent: Optional[QModelIndex] = None,
+ parent: QModelIndex | None = None,
searching: bool = False,
) -> None:
scroll_to_first_match = searching
@@ -348,7 +348,7 @@ class SidebarTreeView(QTreeView):
self.valid_drop_types = tuple(valid_drop_types)
- def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool:
+ def handle_drag_drop(self, sources: list[SidebarItem], target: SidebarItem) -> bool:
if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
return self._handle_drag_drop_decks(sources, target)
if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT):
@@ -361,7 +361,7 @@ class SidebarTreeView(QTreeView):
return False
def _handle_drag_drop_decks(
- self, sources: List[SidebarItem], target: SidebarItem
+ self, sources: list[SidebarItem], target: SidebarItem
) -> bool:
deck_ids = [
DeckId(source.id)
@@ -380,7 +380,7 @@ class SidebarTreeView(QTreeView):
return True
def _handle_drag_drop_tags(
- self, sources: List[SidebarItem], target: SidebarItem
+ self, sources: list[SidebarItem], target: SidebarItem
) -> bool:
tags = [
source.full_name
@@ -402,7 +402,7 @@ class SidebarTreeView(QTreeView):
return True
def _handle_drag_drop_saved_search(
- self, sources: List[SidebarItem], _target: SidebarItem
+ self, sources: list[SidebarItem], _target: SidebarItem
) -> bool:
if len(sources) != 1 or sources[0].search_node is None:
return False
@@ -464,7 +464,7 @@ class SidebarTreeView(QTreeView):
###########################
def _root_tree(self) -> SidebarItem:
- root: Optional[SidebarItem] = None
+ root: SidebarItem | None = None
for stage in SidebarStage:
if stage == SidebarStage.ROOT:
@@ -504,7 +504,7 @@ class SidebarTreeView(QTreeView):
name: str,
icon: Union[str, ColoredIcon],
collapse_key: Config.Bool.V,
- type: Optional[SidebarItemType] = None,
+ type: SidebarItemType | None = None,
) -> SidebarItem:
def update(expanded: bool) -> None:
CollectionOp(
@@ -1112,13 +1112,13 @@ class SidebarTreeView(QTreeView):
_saved_searches_key = "savedFilters"
- def _get_saved_searches(self) -> Dict[str, str]:
+ def _get_saved_searches(self) -> dict[str, str]:
return self.col.get_config(self._saved_searches_key, {})
- def _set_saved_searches(self, searches: Dict[str, str]) -> None:
+ def _set_saved_searches(self, searches: dict[str, str]) -> None:
self.col.set_config(self._saved_searches_key, searches)
- def _get_current_search(self) -> Optional[str]:
+ def _get_current_search(self) -> str | None:
try:
return self.col.build_search_string(self.browser.current_search())
except Exception as e:
@@ -1198,24 +1198,24 @@ class SidebarTreeView(QTreeView):
# Helpers
####################################
- def _selected_items(self) -> List[SidebarItem]:
+ def _selected_items(self) -> list[SidebarItem]:
return [self.model().item_for_index(idx) for idx in self.selectedIndexes()]
- def _selected_decks(self) -> List[DeckId]:
+ def _selected_decks(self) -> list[DeckId]:
return [
DeckId(item.id)
for item in self._selected_items()
if item.item_type == SidebarItemType.DECK
]
- def _selected_saved_searches(self) -> List[str]:
+ def _selected_saved_searches(self) -> list[str]:
return [
item.name
for item in self._selected_items()
if item.item_type == SidebarItemType.SAVED_SEARCH
]
- def _selected_tags(self) -> List[str]:
+ def _selected_tags(self) -> list[str]:
return [
item.full_name
for item in self._selected_items()
diff --git a/qt/aqt/browser/table/__init__.py b/qt/aqt/browser/table/__init__.py
index 4d19ba618..a35cd8ce5 100644
--- a/qt/aqt/browser/table/__init__.py
+++ b/qt/aqt/browser/table/__init__.py
@@ -1,11 +1,10 @@
-# -*- coding: utf-8 -*-
# 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 time
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Generator, Optional, Sequence, Tuple, Union
+from typing import TYPE_CHECKING, Generator, Sequence, Union
import aqt
from anki.cards import CardId
@@ -24,10 +23,10 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]]
class SearchContext:
search: str
browser: aqt.browser.Browser
- order: Union[bool, str, Column] = True
+ order: bool | str | Column = True
reverse: bool = False
# if set, provided ids will be used instead of the regular search
- ids: Optional[Sequence[ItemId]] = None
+ ids: Sequence[ItemId] | None = None
@dataclass
@@ -41,14 +40,14 @@ class CellRow:
def __init__(
self,
- cells: Generator[Tuple[str, bool], None, None],
+ cells: Generator[tuple[str, bool], None, None],
color: BrowserRow.Color.V,
font_name: str,
font_size: int,
) -> None:
self.refreshed_at: float = time.time()
- self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
- self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color)
+ self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
+ self.color: tuple[str, str] | None = backend_color_to_aqt_color(color)
self.font_name: str = font_name or "arial"
self.font_size: int = font_size if font_size > 0 else 12
@@ -75,7 +74,7 @@ class CellRow:
return row
-def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]:
+def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> tuple[str, str] | None:
if color == BrowserRow.COLOR_MARKED:
return colors.MARKED_BG
if color == BrowserRow.COLOR_SUSPENDED:
diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py
index 96be01121..f2ab6270b 100644
--- a/qt/aqt/browser/table/model.py
+++ b/qt/aqt/browser/table/model.py
@@ -1,10 +1,9 @@
-# -*- coding: utf-8 -*-
# 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 time
-from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast
+from typing import Any, Callable, Sequence, cast
import aqt
from anki.cards import Card, CardId
@@ -41,13 +40,13 @@ class DataModel(QAbstractTableModel):
) -> None:
QAbstractTableModel.__init__(self)
self.col: Collection = col
- self.columns: Dict[str, Column] = dict(
- ((c.key, c) for c in self.col.all_browser_columns())
- )
+ self.columns: dict[str, Column] = {
+ c.key: c for c in self.col.all_browser_columns()
+ }
gui_hooks.browser_did_fetch_columns(self.columns)
self._state: ItemState = state
self._items: Sequence[ItemId] = []
- self._rows: Dict[int, CellRow] = {}
+ self._rows: dict[int, CellRow] = {}
self._block_updates = False
self._stale_cutoff = 0.0
self._on_row_state_will_change = row_state_will_change_callback
@@ -77,7 +76,7 @@ class DataModel(QAbstractTableModel):
return self._fetch_row_and_update_cache(index, item, None)
def _fetch_row_and_update_cache(
- self, index: QModelIndex, item: ItemId, old_row: Optional[CellRow]
+ self, index: QModelIndex, item: ItemId, old_row: CellRow | None
) -> CellRow:
"""Fetch a row from the backend, add it to the cache and return it.
Then fire callbacks if the row is being deleted or restored.
@@ -119,7 +118,7 @@ class DataModel(QAbstractTableModel):
)
return row
- def get_cached_row(self, index: QModelIndex) -> Optional[CellRow]:
+ def get_cached_row(self, index: QModelIndex) -> CellRow | None:
"""Get row if it is cached, regardless of staleness."""
return self._rows.get(self.get_item(index))
@@ -175,41 +174,41 @@ class DataModel(QAbstractTableModel):
def get_item(self, index: QModelIndex) -> ItemId:
return self._items[index.row()]
- def get_items(self, indices: List[QModelIndex]) -> Sequence[ItemId]:
+ def get_items(self, indices: list[QModelIndex]) -> Sequence[ItemId]:
return [self.get_item(index) for index in indices]
- def get_card_ids(self, indices: List[QModelIndex]) -> Sequence[CardId]:
+ def get_card_ids(self, indices: list[QModelIndex]) -> Sequence[CardId]:
return self._state.get_card_ids(self.get_items(indices))
- def get_note_ids(self, indices: List[QModelIndex]) -> Sequence[NoteId]:
+ def get_note_ids(self, indices: list[QModelIndex]) -> Sequence[NoteId]:
return self._state.get_note_ids(self.get_items(indices))
- def get_note_id(self, index: QModelIndex) -> Optional[NoteId]:
+ def get_note_id(self, index: QModelIndex) -> NoteId | None:
if nid_list := self._state.get_note_ids([self.get_item(index)]):
return nid_list[0]
return None
# Get row numbers from items
- def get_item_row(self, item: ItemId) -> Optional[int]:
+ def get_item_row(self, item: ItemId) -> int | None:
for row, i in enumerate(self._items):
if i == item:
return row
return None
- def get_item_rows(self, items: Sequence[ItemId]) -> List[int]:
+ def get_item_rows(self, items: Sequence[ItemId]) -> list[int]:
rows = []
for row, i in enumerate(self._items):
if i in items:
rows.append(row)
return rows
- def get_card_row(self, card_id: CardId) -> Optional[int]:
+ def get_card_row(self, card_id: CardId) -> int | None:
return self.get_item_row(self._state.get_item_from_card_id(card_id))
# Get objects (cards or notes)
- def get_card(self, index: QModelIndex) -> Optional[Card]:
+ def get_card(self, index: QModelIndex) -> Card | None:
"""Try to return the indicated, possibly deleted card."""
if not index.isValid():
return None
@@ -218,7 +217,7 @@ class DataModel(QAbstractTableModel):
except NotFoundError:
return None
- def get_note(self, index: QModelIndex) -> Optional[Note]:
+ def get_note(self, index: QModelIndex) -> Note | None:
"""Try to return the indicated, possibly deleted note."""
if not index.isValid():
return None
@@ -280,7 +279,7 @@ class DataModel(QAbstractTableModel):
self.columns[key] = addon_column_fillin(key)
return self.columns[key]
- def active_column_index(self, column: str) -> Optional[int]:
+ def active_column_index(self, column: str) -> int | None:
return (
self._state.active_columns.index(column)
if column in self._state.active_columns
@@ -317,7 +316,7 @@ class DataModel(QAbstractTableModel):
qfont.setPixelSize(row.font_size)
return qfont
elif role == Qt.TextAlignmentRole:
- align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter
+ align: Qt.AlignmentFlag | int = Qt.AlignVCenter
if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
align |= Qt.AlignHCenter
return align
@@ -329,7 +328,7 @@ class DataModel(QAbstractTableModel):
def headerData(
self, section: int, orientation: Qt.Orientation, role: int = 0
- ) -> Optional[str]:
+ ) -> str | None:
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._state.column_label(self.column_at_section(section))
return None
diff --git a/qt/aqt/browser/table/state.py b/qt/aqt/browser/table/state.py
index fc11699d8..4c7043b11 100644
--- a/qt/aqt/browser/table/state.py
+++ b/qt/aqt/browser/table/state.py
@@ -1,10 +1,9 @@
-# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from abc import ABC, abstractmethod, abstractproperty
-from typing import List, Sequence, Union, cast
+from typing import Sequence, cast
from anki.browser import BrowserConfig
from anki.cards import Card, CardId
@@ -18,7 +17,7 @@ class ItemState(ABC):
GEOMETRY_KEY_PREFIX: str
SORT_COLUMN_KEY: str
SORT_BACKWARDS_KEY: str
- _active_columns: List[str]
+ _active_columns: list[str]
def __init__(self, col: Collection) -> None:
self.col = col
@@ -57,7 +56,7 @@ class ItemState(ABC):
# abstractproperty is deprecated but used due to mypy limitations
# (https://github.com/python/mypy/issues/1362)
@abstractproperty # pylint: disable=deprecated-decorator
- def active_columns(self) -> List[str]:
+ def active_columns(self) -> list[str]:
"""Return the saved or default columns for the state."""
@abstractmethod
@@ -96,7 +95,7 @@ class ItemState(ABC):
@abstractmethod
def find_items(
- self, search: str, order: Union[bool, str, Column], reverse: bool
+ self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
"""Return the item ids fitting the given search and order."""
@@ -133,7 +132,7 @@ class CardState(ItemState):
self._active_columns = self.col.load_browser_card_columns()
@property
- def active_columns(self) -> List[str]:
+ def active_columns(self) -> list[str]:
return self._active_columns
def toggle_active_column(self, column: str) -> None:
@@ -150,7 +149,7 @@ class CardState(ItemState):
return self.get_card(item).note()
def find_items(
- self, search: str, order: Union[bool, str, Column], reverse: bool
+ self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
return self.col.find_cards(search, order, reverse)
@@ -180,7 +179,7 @@ class NoteState(ItemState):
self._active_columns = self.col.load_browser_note_columns()
@property
- def active_columns(self) -> List[str]:
+ def active_columns(self) -> list[str]:
return self._active_columns
def toggle_active_column(self, column: str) -> None:
@@ -197,7 +196,7 @@ class NoteState(ItemState):
return self.col.get_note(NoteId(item))
def find_items(
- self, search: str, order: Union[bool, str, Column], reverse: bool
+ self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]:
return self.col.find_notes(search, order, reverse)
diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py
index fd627a871..e7812e052 100644
--- a/qt/aqt/browser/table/table.py
+++ b/qt/aqt/browser/table/table.py
@@ -1,9 +1,8 @@
-# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
-from typing import Any, Callable, List, Optional, Sequence, Tuple, cast
+from typing import Any, Callable, Sequence, cast
import aqt
import aqt.forms
@@ -45,12 +44,12 @@ class Table:
self._on_row_state_will_change,
self._on_row_state_changed,
)
- self._view: Optional[QTableView] = None
+ self._view: QTableView | None = None
# cached for performance
self._len_selection = 0
- self._selected_rows: Optional[List[QModelIndex]] = None
+ self._selected_rows: list[QModelIndex] | None = None
# temporarily set for selection preservation
- self._current_item: Optional[ItemId] = None
+ self._current_item: ItemId | None = None
self._selected_items: Sequence[ItemId] = []
def set_view(self, view: QTableView) -> None:
@@ -89,13 +88,13 @@ class Table:
# Get objects
- def get_current_card(self) -> Optional[Card]:
+ def get_current_card(self) -> Card | None:
return self._model.get_card(self._current())
- def get_current_note(self) -> Optional[Note]:
+ def get_current_note(self) -> Note | None:
return self._model.get_note(self._current())
- def get_single_selected_card(self) -> Optional[Card]:
+ def get_single_selected_card(self) -> Card | None:
"""If there is only one row selected return its card, else None.
This may be a different one than the current card."""
if self.len_selection() != 1:
@@ -171,7 +170,7 @@ class Table:
self._model.redraw_cells()
def op_executed(
- self, changes: OpChanges, handler: Optional[object], focused: bool
+ self, changes: OpChanges, handler: object | None, focused: bool
) -> None:
if changes.browser_table:
self._model.mark_cache_stale()
@@ -260,7 +259,7 @@ class Table:
def _current(self) -> QModelIndex:
return self._view.selectionModel().currentIndex()
- def _selected(self) -> List[QModelIndex]:
+ def _selected(self) -> list[QModelIndex]:
if self._selected_rows is None:
self._selected_rows = self._view.selectionModel().selectedRows()
return self._selected_rows
@@ -280,7 +279,7 @@ class Table:
self._len_selection = 0
self._selected_rows = None
- def _select_rows(self, rows: List[int]) -> None:
+ def _select_rows(self, rows: list[int]) -> None:
selection = QItemSelection()
for row in rows:
selection.select(
@@ -532,9 +531,7 @@ class Table:
self._selected_items = []
self._current_item = None
- def _qualify_selected_rows(
- self, rows: List[int], current: Optional[int]
- ) -> List[int]:
+ def _qualify_selected_rows(self, rows: list[int], current: int | None) -> list[int]:
"""Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current."""
if rows:
if len(rows) < self.SELECTION_LIMIT:
@@ -544,7 +541,7 @@ class Table:
return rows[0:1]
return [current if current else 0]
- def _intersected_selection(self) -> Tuple[List[int], Optional[int]]:
+ def _intersected_selection(self) -> tuple[list[int], int | None]:
"""Return all rows of items that were in the saved selection and the row of the saved
current element if present.
"""
@@ -554,7 +551,7 @@ class Table:
)
return selected_rows, current_row
- def _toggled_selection(self) -> Tuple[List[int], Optional[int]]:
+ def _toggled_selection(self) -> tuple[list[int], int | None]:
"""Convert the items of the saved selection and current element to the new state and
return their rows.
"""
diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py
index ae8951b88..aa024bf78 100644
--- a/qt/aqt/clayout.py
+++ b/qt/aqt/clayout.py
@@ -3,7 +3,7 @@
import json
import re
from concurrent.futures import Future
-from typing import Any, Dict, List, Match, Optional
+from typing import Any, Match, Optional
import aqt
from anki.collection import OpChanges
@@ -135,7 +135,7 @@ class CardLayout(QDialog):
combo.setEnabled(not self._isCloze())
self.ignore_change_signals = False
- def _summarizedName(self, idx: int, tmpl: Dict) -> str:
+ def _summarizedName(self, idx: int, tmpl: dict) -> str:
return "{}: {}: {} -> {}".format(
idx + 1,
tmpl["name"],
@@ -146,7 +146,7 @@ class CardLayout(QDialog):
def _fieldsOnTemplate(self, fmt: str) -> str:
matches = re.findall("{{[^#/}]+?}}", fmt)
chars_allowed = 30
- field_names: List[str] = []
+ field_names: list[str] = []
for m in matches:
# strip off mustache
m = re.sub(r"[{}]", "", m)
@@ -440,7 +440,7 @@ class CardLayout(QDialog):
# Reading/writing question/answer/css
##########################################################################
- def current_template(self) -> Dict:
+ def current_template(self) -> dict:
if self._isCloze():
return self.templates[0]
return self.templates[self.ord]
@@ -592,7 +592,7 @@ class CardLayout(QDialog):
self.mw.taskman.with_progress(get_count, on_done)
- def onRemoveInner(self, template: Dict) -> None:
+ def onRemoveInner(self, template: dict) -> None:
self.mm.remove_template(self.model, template)
# ensure current ordinal is within bounds
@@ -668,7 +668,7 @@ class CardLayout(QDialog):
self._flipQA(old, old)
self.redraw_everything()
- def _flipQA(self, src: Dict, dst: Dict) -> None:
+ def _flipQA(self, src: dict, dst: dict) -> None:
m = re.match("(?s)(.+)
(.+)", src["afmt"])
if not m:
showInfo(tr.card_templates_anki_couldnt_find_the_line_between())
diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py
index 008dae830..fc65f3e33 100644
--- a/qt/aqt/deckbrowser.py
+++ b/qt/aqt/deckbrowser.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
-from typing import Any, Optional
+from typing import Any
import aqt
from anki.collection import OpChanges
@@ -79,7 +79,7 @@ class DeckBrowser:
self.refresh()
def op_executed(
- self, changes: OpChanges, handler: Optional[object], focused: bool
+ self, changes: OpChanges, handler: object | None, focused: bool
) -> bool:
if changes.study_queues and handler is not self:
self._refresh_needed = True
@@ -175,11 +175,11 @@ class DeckBrowser:
def _renderDeckTree(self, top: DeckTreeNode) -> str:
buf = """
-%s |
-%s |
+
---|
{} |
+{} |
|
-%s |
- |
""" % (
+{} |
+ | """.format(
tr.decks_deck(),
tr.actions_new(),
tr.statistics_due_count(),
diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py
index c5cf42e5d..a41f6ccee 100644
--- a/qt/aqt/deckconf.py
+++ b/qt/aqt/deckconf.py
@@ -4,7 +4,7 @@
from __future__ import annotations
from operator import itemgetter
-from typing import Any, Dict, List, Optional
+from typing import Any
from PyQt5.QtWidgets import QLineEdit
@@ -30,7 +30,7 @@ from aqt.utils import (
class DeckConf(QDialog):
- def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None:
+ def __init__(self, mw: aqt.AnkiQt, deck: dict) -> None:
QDialog.__init__(self, mw)
self.mw = mw
self.deck = deck
@@ -74,7 +74,7 @@ class DeckConf(QDialog):
def setupConfs(self) -> None:
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
- self.conf: Optional[DeckConfigDict] = None
+ self.conf: DeckConfigDict | None = None
self.loadConfs()
def loadConfs(self) -> None:
@@ -175,7 +175,7 @@ class DeckConf(QDialog):
# Loading
##################################################
- def listToUser(self, l: List[Union[int, float]]) -> str:
+ def listToUser(self, l: list[Union[int, float]]) -> str:
def num_to_user(n: Union[int, float]) -> str:
if n == round(n):
return str(int(n))
diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py
index a8e5fda46..fe68763f1 100644
--- a/qt/aqt/deckoptions.py
+++ b/qt/aqt/deckoptions.py
@@ -3,8 +3,6 @@
from __future__ import annotations
-from typing import List, Optional
-
import aqt
import aqt.deckconf
from anki.cards import Card
@@ -67,7 +65,7 @@ class DeckOptionsDialog(QDialog):
QDialog.reject(self)
-def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> None:
+def confirm_deck_then_display_options(active_card: Card | None = None) -> None:
decks = [aqt.mw.col.decks.current()]
if card := active_card:
if card.odid and card.odid != decks[0]["id"]:
@@ -83,7 +81,7 @@ def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> Non
_deck_prompt_dialog(decks)
-def _deck_prompt_dialog(decks: List[DeckDict]) -> None:
+def _deck_prompt_dialog(decks: list[DeckDict]) -> None:
diag = QDialog(aqt.mw.app.activeWindow())
diag.setWindowTitle("Anki")
box = QVBoxLayout()
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index 3ce7e5dc5..d80d04ac8 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -14,7 +14,7 @@ import urllib.parse
import urllib.request
import warnings
from random import randrange
-from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast
+from typing import Any, Callable, Match, cast
import bs4
import requests
@@ -104,14 +104,14 @@ class Editor:
self.mw = mw
self.widget = widget
self.parentWindow = parentWindow
- self.note: Optional[Note] = None
+ self.note: Note | None = None
self.addMode = addMode
- self.currentField: Optional[int] = None
+ self.currentField: int | None = None
# Similar to currentField, but not set to None on a blur. May be
# outside the bounds of the current notetype.
- self.last_field_index: Optional[int] = None
+ self.last_field_index: int | None = None
# current card, for card layout
- self.card: Optional[Card] = None
+ self.card: Card | None = None
self.setupOuter()
self.setupWeb()
self.setupShortcuts()
@@ -147,7 +147,7 @@ class Editor:
default_css=True,
)
- lefttopbtns: List[str] = []
+ lefttopbtns: list[str] = []
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
lefttopbtns_defs = [
@@ -156,7 +156,7 @@ class Editor:
]
lefttopbtns_js = "\n".join(lefttopbtns_defs)
- righttopbtns: List[str] = []
+ righttopbtns: list[str] = []
gui_hooks.editor_did_init_buttons(righttopbtns, self)
# legacy filter
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
@@ -191,9 +191,9 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def addButton(
self,
- icon: Optional[str],
+ icon: str | None,
cmd: str,
- func: Callable[["Editor"], None],
+ func: Callable[[Editor], None],
tip: str = "",
label: str = "",
id: str = None,
@@ -242,11 +242,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def _addButton(
self,
- icon: Optional[str],
+ icon: str | None,
cmd: str,
tip: str = "",
label: str = "",
- id: Optional[str] = None,
+ id: str | None = None,
toggleable: bool = False,
disables: bool = True,
rightside: bool = True,
@@ -302,7 +302,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def setupShortcuts(self) -> None:
# if a third element is provided, enable shortcut even when no field selected
- cuts: List[Tuple] = []
+ cuts: list[tuple] = []
gui_hooks.editor_did_init_shortcuts(cuts, self)
for row in cuts:
if len(row) == 2:
@@ -449,7 +449,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
######################################################################
def set_note(
- self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
+ self, note: Note | None, hide: bool = True, focusTo: int | None = None
) -> None:
"Make NOTE the current note."
self.note = note
@@ -462,7 +462,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def loadNoteKeepingFocus(self) -> None:
self.loadNote(self.currentField)
- def loadNote(self, focusTo: Optional[int] = None) -> None:
+ def loadNote(self, focusTo: int | None = None) -> None:
if not self.note:
return
@@ -488,7 +488,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f")
- js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s); setColorButtons(%s); setTags(%s); " % (
+ js = "setFields({}); setFonts({}); focusField({}); setNoteId({}); setColorButtons({}); setTags({}); ".format(
json.dumps(data),
json.dumps(self.fonts()),
json.dumps(focusTo),
@@ -510,7 +510,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
initiator=self
)
- def fonts(self) -> List[Tuple[str, int, bool]]:
+ def fonts(self) -> list[tuple[str, int, bool]]:
return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
for f in self.note.note_type()["flds"]
@@ -573,7 +573,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
),
)
- def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool:
+ def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
if not self.note:
return True
m = self.note.note_type()
@@ -700,7 +700,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# Media downloads
######################################################################
- def urlToLink(self, url: str) -> Optional[str]:
+ def urlToLink(self, url: str) -> str | None:
fname = self.urlToFile(url)
if not fname:
return None
@@ -715,7 +715,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
av_player.play_file(fname)
return f"[sound:{html.escape(fname, quote=False)}]"
- def urlToFile(self, url: str) -> Optional[str]:
+ def urlToFile(self, url: str) -> str | None:
l = url.lower()
for suffix in pics + audio:
if l.endswith(f".{suffix}"):
@@ -760,7 +760,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
fname = f"paste-{csum}{ext}"
return self._addMediaFromData(fname, data)
- def _retrieveURL(self, url: str) -> Optional[str]:
+ def _retrieveURL(self, url: str) -> str | None:
"Download file into media folder and return local filename or None."
# urllib doesn't understand percent-escaped utf8, but requires things like
# '#' to be escaped.
@@ -774,7 +774,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# fetch it into a temporary folder
self.mw.progress.start(immediate=not local, parent=self.parentWindow)
content_type = None
- error_msg: Optional[str] = None
+ error_msg: str | None = None
try:
if local:
req = urllib.request.Request(
@@ -972,7 +972,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
for name, val in list(self.note.items()):
m = re.findall(r"\{\{c(\d+)::", val)
if m:
- highest = max(highest, sorted([int(x) for x in m])[-1])
+ highest = max(highest, sorted(int(x) for x in m)[-1])
# reuse last?
if not KeyboardModifiersPressed().alt:
highest += 1
@@ -1069,7 +1069,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# Links from HTML
######################################################################
- _links: Dict[str, Callable] = dict(
+ _links: dict[str, Callable] = dict(
fields=onFields,
cards=onCardLayout,
bold=toggleBold,
@@ -1163,7 +1163,7 @@ class EditorWebView(AnkiWebView):
# returns (html, isInternal)
def _processMime(
self, mime: QMimeData, extended: bool = False, drop_event: bool = False
- ) -> Tuple[str, bool]:
+ ) -> tuple[str, bool]:
# print("html=%s image=%s urls=%s txt=%s" % (
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()))
# print("html", mime.html())
@@ -1193,7 +1193,7 @@ class EditorWebView(AnkiWebView):
return html, True
return "", False
- def _processUrls(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
+ def _processUrls(self, mime: QMimeData, extended: bool = False) -> str | None:
if not mime.hasUrls():
return None
@@ -1206,7 +1206,7 @@ class EditorWebView(AnkiWebView):
return buf
- def _processText(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
+ def _processText(self, mime: QMimeData, extended: bool = False) -> str | None:
if not mime.hasText():
return None
@@ -1245,7 +1245,7 @@ class EditorWebView(AnkiWebView):
processed.pop()
return "".join(processed)
- def _processImage(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
+ def _processImage(self, mime: QMimeData, extended: bool = False) -> str | None:
if not mime.hasImage():
return None
im = QImage(mime.imageData())
diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py
index 95ed26787..a84bc309f 100644
--- a/qt/aqt/emptycards.py
+++ b/qt/aqt/emptycards.py
@@ -5,7 +5,7 @@ from __future__ import annotations
import re
from concurrent.futures import Future
-from typing import Any, List
+from typing import Any
import aqt
from anki.cards import CardId
@@ -89,7 +89,7 @@ class EmptyCardsDialog(QDialog):
self.mw.taskman.run_in_background(delete, on_done)
def _delete_cards(self, keep_notes: bool) -> int:
- to_delete: List[CardId] = []
+ to_delete: list[CardId] = []
note: EmptyCardsReport.NoteWithEmptyCards
for note in self.report.notes:
if keep_notes and note.will_delete_note:
diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py
index 1fb1fcce3..6868373de 100644
--- a/qt/aqt/exporting.py
+++ b/qt/aqt/exporting.py
@@ -7,7 +7,6 @@ import os
import re
import time
from concurrent.futures import Future
-from typing import List, Optional
import aqt
from anki import hooks
@@ -29,21 +28,21 @@ class ExportDialog(QDialog):
def __init__(
self,
mw: aqt.main.AnkiQt,
- did: Optional[DeckId] = None,
- cids: Optional[List[CardId]] = None,
+ did: DeckId | None = None,
+ cids: list[CardId] | None = None,
):
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.col = mw.col.weakref()
self.frm = aqt.forms.exporting.Ui_ExportDialog()
self.frm.setupUi(self)
- self.exporter: Optional[Exporter] = None
+ self.exporter: Exporter | None = None
self.cids = cids
disable_help_button(self)
self.setup(did)
self.exec_()
- def setup(self, did: Optional[DeckId]) -> None:
+ def setup(self, did: DeckId | None) -> None:
self.exporters = exporters(self.col)
# if a deck specified, start with .apkg type selected
idx = 0
diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py
index da4894ba2..368f988ef 100644
--- a/qt/aqt/fields.py
+++ b/qt/aqt/fields.py
@@ -1,6 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+from typing import Optional
+
import aqt
from anki.collection import OpChanges
from anki.consts import *
diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py
index d62922d98..1876521ba 100644
--- a/qt/aqt/filtered_deck.py
+++ b/qt/aqt/filtered_deck.py
@@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import List, Optional, Tuple
+from __future__ import annotations
import aqt
from anki.collection import OpChangesWithId, SearchNode
@@ -36,8 +36,8 @@ class FilteredDeckConfigDialog(QDialog):
self,
mw: AnkiQt,
deck_id: DeckId = DeckId(0),
- search: Optional[str] = None,
- search_2: Optional[str] = None,
+ search: str | None = None,
+ search_2: str | None = None,
) -> None:
"""If 'deck_id' is non-zero, load and modify its settings.
Otherwise, build a new deck and derive settings from the current deck.
@@ -162,15 +162,13 @@ class FilteredDeckConfigDialog(QDialog):
def reopen(
self,
_mw: AnkiQt,
- search: Optional[str] = None,
- search_2: Optional[str] = None,
- _deck: Optional[DeckDict] = None,
+ search: str | None = None,
+ search_2: str | None = None,
+ _deck: DeckDict | None = None,
) -> None:
self.set_custom_searches(search, search_2)
- def set_custom_searches(
- self, search: Optional[str], search_2: Optional[str]
- ) -> None:
+ def set_custom_searches(self, search: str | None, search_2: str | None) -> None:
if search is not None:
self.form.search.setText(search)
self.form.search.setFocus()
@@ -218,12 +216,12 @@ class FilteredDeckConfigDialog(QDialog):
else:
aqt.dialogs.open("Browser", self.mw, search=(search,))
- def _second_filter(self) -> Tuple[str, ...]:
+ def _second_filter(self) -> tuple[str, ...]:
if self.form.secondFilter.isChecked():
return (self.form.search_2.text(),)
return ()
- def _learning_search_node(self) -> Tuple[SearchNode, ...]:
+ def _learning_search_node(self) -> tuple[SearchNode, ...]:
"""Return a search node that matches learning cards if the old scheduler is enabled.
If it's a rebuild, exclude cards from this filtered deck as those will be reset.
"""
@@ -238,7 +236,7 @@ class FilteredDeckConfigDialog(QDialog):
return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
return ()
- def _filtered_search_node(self) -> Tuple[SearchNode]:
+ def _filtered_search_node(self) -> tuple[SearchNode]:
"""Return a search node that matches cards in filtered decks, if applicable excluding those
in the deck being rebuild."""
if self.deck.id:
@@ -320,12 +318,12 @@ class FilteredDeckConfigDialog(QDialog):
########################################################
# fixme: remove once we drop support for v1
- def listToUser(self, values: List[Union[float, int]]) -> str:
+ def listToUser(self, values: list[Union[float, int]]) -> str:
return " ".join(
[str(int(val)) if int(val) == val else str(val) for val in values]
)
- def userToList(self, line: QLineEdit, minSize: int = 1) -> Optional[List[float]]:
+ def userToList(self, line: QLineEdit, minSize: int = 1) -> list[float] | None:
items = str(line.text()).split(" ")
ret = []
for item in items:
diff --git a/qt/aqt/flags.py b/qt/aqt/flags.py
index d2767912c..e1e209aeb 100644
--- a/qt/aqt/flags.py
+++ b/qt/aqt/flags.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Dict, List, Optional, cast
+from typing import cast
import aqt
from anki.collection import SearchNode
@@ -33,9 +33,9 @@ class Flag:
class FlagManager:
def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw
- self._flags: Optional[List[Flag]] = None
+ self._flags: list[Flag] | None = None
- def all(self) -> List[Flag]:
+ def all(self) -> list[Flag]:
"""Return a list of all flags."""
if self._flags is None:
self._load_flags()
@@ -55,7 +55,7 @@ class FlagManager:
gui_hooks.flag_label_did_change()
def _load_flags(self) -> None:
- labels = cast(Dict[str, str], self.mw.col.get_config("flagLabels", {}))
+ labels = cast(dict[str, str], self.mw.col.get_config("flagLabels", {}))
icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED)
self._flags = [
diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py
index 3ee272bb9..f8211d005 100644
--- a/qt/aqt/importing.py
+++ b/qt/aqt/importing.py
@@ -8,7 +8,7 @@ import traceback
import unicodedata
import zipfile
from concurrent.futures import Future
-from typing import Any, Dict, Optional
+from typing import Any, Optional
import anki.importing as importing
import aqt.deckchooser
@@ -34,7 +34,7 @@ from aqt.utils import (
class ChangeMap(QDialog):
- def __init__(self, mw: AnkiQt, model: Dict, current: str) -> None:
+ def __init__(self, mw: AnkiQt, model: dict, current: str) -> None:
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.model = model
diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py
index 8be89c6c0..7b450c3e7 100644
--- a/qt/aqt/legacy.py
+++ b/qt/aqt/legacy.py
@@ -5,7 +5,9 @@
Legacy support
"""
-from typing import Any, List
+from __future__ import annotations
+
+from typing import Any
import anki
import aqt
@@ -20,7 +22,7 @@ def bodyClass(col, card) -> str: # type: ignore
return theme_manager.body_classes_for_card_ord(card.ord)
-def allSounds(text) -> List: # type: ignore
+def allSounds(text) -> list: # type: ignore
print("allSounds() deprecated")
return aqt.mw.col.media._extract_filenames(text)
diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index b45c82457..6769a8686 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -13,19 +13,7 @@ import zipfile
from argparse import Namespace
from concurrent.futures import Future
from threading import Thread
-from typing import (
- Any,
- Callable,
- Dict,
- List,
- Literal,
- Optional,
- Sequence,
- TextIO,
- Tuple,
- TypeVar,
- cast,
-)
+from typing import Any, Literal, Sequence, TextIO, TypeVar, cast
import anki
import aqt
@@ -105,13 +93,13 @@ class AnkiQt(QMainWindow):
profileManager: ProfileManagerType,
backend: _RustBackend,
opts: Namespace,
- args: List[Any],
+ args: list[Any],
) -> None:
QMainWindow.__init__(self)
self.backend = backend
self.state: MainWindowState = "startup"
self.opts = opts
- self.col: Optional[Collection] = None
+ self.col: Collection | None = None
self.taskman = TaskManager(self)
self.media_syncer = MediaSyncer(self)
aqt.mw = self
@@ -230,7 +218,7 @@ class AnkiQt(QMainWindow):
self.pm.meta["firstRun"] = False
self.pm.save()
- self.pendingImport: Optional[str] = None
+ self.pendingImport: str | None = None
self.restoringBackup = False
# profile not provided on command line?
if not self.pm.name:
@@ -380,7 +368,7 @@ class AnkiQt(QMainWindow):
self.progress.start()
profiles = self.pm.profiles()
- def downgrade() -> List[str]:
+ def downgrade() -> list[str]:
return self.pm.downgrade(profiles)
def on_done(future: Future) -> None:
@@ -399,7 +387,7 @@ class AnkiQt(QMainWindow):
self.taskman.run_in_background(downgrade, on_done)
- def loadProfile(self, onsuccess: Optional[Callable] = None) -> None:
+ def loadProfile(self, onsuccess: Callable | None = None) -> None:
if not self.loadCollection():
return
@@ -678,7 +666,7 @@ class AnkiQt(QMainWindow):
self.maybe_check_for_addon_updates()
self.deckBrowser.show()
- def _selectedDeck(self) -> Optional[DeckDict]:
+ def _selectedDeck(self) -> DeckDict | None:
did = self.col.decks.selected()
if not self.col.decks.name_if_exists(did):
showInfo(tr.qt_misc_please_select_a_deck())
@@ -721,7 +709,7 @@ class AnkiQt(QMainWindow):
gui_hooks.operation_did_execute(op, None)
def on_operation_did_execute(
- self, changes: OpChanges, handler: Optional[object]
+ self, changes: OpChanges, handler: object | None
) -> None:
"Notify current screen of changes."
focused = current_window() == self
@@ -741,7 +729,7 @@ class AnkiQt(QMainWindow):
self.toolbar.update_sync_status()
def on_focus_did_change(
- self, new_focus: Optional[QWidget], _old: Optional[QWidget]
+ self, new_focus: QWidget | None, _old: QWidget | None
) -> None:
"If main window has received focus, ensure current UI state is updated."
if new_focus and new_focus.window() == self:
@@ -799,7 +787,7 @@ class AnkiQt(QMainWindow):
self,
link: str,
name: str,
- key: Optional[str] = None,
+ key: str | None = None,
class_: str = "",
id: str = "",
extra: str = "",
@@ -810,8 +798,8 @@ class AnkiQt(QMainWindow):
else:
key = ""
return """
-""" % (
+""".format(
id,
class_,
link,
@@ -878,7 +866,7 @@ title="%s" %s>%s""" % (
self.errorHandler = aqt.errors.ErrorHandler(self)
- def setupAddons(self, args: Optional[List]) -> None:
+ def setupAddons(self, args: list | None) -> None:
import aqt.addons
self.addonManager = aqt.addons.AddonManager(self)
@@ -903,7 +891,7 @@ title="%s" %s>%s""" % (
)
self.pm.set_last_addon_update_check(intTime())
- def on_updates_installed(self, log: List[DownloadLogEntry]) -> None:
+ def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
if log:
show_log_to_user(self, log)
@@ -1024,11 +1012,11 @@ title="%s" %s>%s""" % (
("y", self.on_sync_button_clicked),
]
self.applyShortcuts(globalShortcuts)
- self.stateShortcuts: List[QShortcut] = []
+ self.stateShortcuts: list[QShortcut] = []
def applyShortcuts(
- self, shortcuts: Sequence[Tuple[str, Callable]]
- ) -> List[QShortcut]:
+ self, shortcuts: Sequence[tuple[str, Callable]]
+ ) -> list[QShortcut]:
qshortcuts = []
for key, fn in shortcuts:
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
@@ -1036,7 +1024,7 @@ title="%s" %s>%s""" % (
qshortcuts.append(scut)
return qshortcuts
- def setStateShortcuts(self, shortcuts: List[Tuple[str, Callable]]) -> None:
+ def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None:
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
# legacy hook
runHook(f"{self.state}StateShortcuts", shortcuts)
@@ -1154,7 +1142,7 @@ title="%s" %s>%s""" % (
# legacy
- def onDeckConf(self, deck: Optional[DeckDict] = None) -> None:
+ def onDeckConf(self, deck: DeckDict | None = None) -> None:
pass
# Importing & exporting
@@ -1175,7 +1163,7 @@ title="%s" %s>%s""" % (
aqt.importing.onImport(self)
- def onExport(self, did: Optional[DeckId] = None) -> None:
+ def onExport(self, did: DeckId | None = None) -> None:
import aqt.exporting
aqt.exporting.ExportDialog(self, did=did)
@@ -1246,7 +1234,7 @@ title="%s" %s>%s""" % (
if self.pm.meta.get("suppressUpdate", None) != ver:
aqt.update.askAndUpdate(self, ver)
- def newMsg(self, data: Dict) -> None:
+ def newMsg(self, data: dict) -> None:
aqt.update.showMessages(self, data)
def clockIsOff(self, diff: int) -> None:
@@ -1299,7 +1287,7 @@ title="%s" %s>%s""" % (
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
gui_hooks.focus_did_change.append(self.on_focus_did_change)
- self._activeWindowOnPlay: Optional[QWidget] = None
+ self._activeWindowOnPlay: QWidget | None = None
def onOdueInvalid(self) -> None:
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
@@ -1486,12 +1474,12 @@ title="%s" %s>%s""" % (
c._render_output = None
pprint.pprint(c.__dict__)
- def _debugCard(self) -> Optional[anki.cards.Card]:
+ def _debugCard(self) -> anki.cards.Card | None:
card = self.reviewer.card
self._card_repr(card)
return card
- def _debugBrowserCard(self) -> Optional[anki.cards.Card]:
+ def _debugBrowserCard(self) -> anki.cards.Card | None:
card = aqt.dialogs._dialogs["Browser"][1].card
self._card_repr(card)
return card
@@ -1564,7 +1552,7 @@ title="%s" %s>%s""" % (
_dummy1 = windll
_dummy2 = wintypes
- def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None:
+ def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
if not self.hideMenuAccels:
return
tgt = tgt or self
diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py
index c3ed3de36..69c766cbd 100644
--- a/qt/aqt/mediacheck.py
+++ b/qt/aqt/mediacheck.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import itertools
import time
from concurrent.futures import Future
-from typing import Iterable, List, Optional, Sequence, TypeVar
+from typing import Iterable, Sequence, TypeVar
import aqt
from anki.collection import SearchNode
@@ -26,7 +26,7 @@ from aqt.utils import (
T = TypeVar("T")
-def chunked_list(l: Iterable[T], n: int) -> Iterable[List[T]]:
+def chunked_list(l: Iterable[T], n: int) -> Iterable[list[T]]:
l = iter(l)
while True:
res = list(itertools.islice(l, n))
@@ -41,11 +41,11 @@ def check_media_db(mw: aqt.AnkiQt) -> None:
class MediaChecker:
- progress_dialog: Optional[aqt.progress.ProgressDialog]
+ progress_dialog: aqt.progress.ProgressDialog | None
def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw
- self._progress_timer: Optional[QTimer] = None
+ self._progress_timer: QTimer | None = None
def check(self) -> None:
self.progress_dialog = self.mw.progress.start()
diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py
index 09e094fce..fa3cfe26b 100644
--- a/qt/aqt/mediasrv.py
+++ b/qt/aqt/mediasrv.py
@@ -11,7 +11,6 @@ import threading
import time
import traceback
from http import HTTPStatus
-from typing import Tuple
import flask
import flask_cors # type: ignore
@@ -179,7 +178,7 @@ def allroutes(pathin: str) -> Response:
)
-def _redirectWebExports(path: str) -> Tuple[str, str]:
+def _redirectWebExports(path: str) -> tuple[str, str]:
# catch /_anki references and rewrite them to web export folder
targetPath = "_anki/"
if path.startswith(targetPath):
diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py
index 72457be06..8b8e4a92d 100644
--- a/qt/aqt/mediasync.py
+++ b/qt/aqt/mediasync.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import time
from concurrent.futures import Future
from dataclasses import dataclass
-from typing import Any, Callable, List, Optional, Union
+from typing import Any, Callable, Union
import aqt
from anki.collection import Progress
@@ -30,8 +30,8 @@ class MediaSyncer:
def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw
self._syncing: bool = False
- self._log: List[LogEntryWithTime] = []
- self._progress_timer: Optional[QTimer] = None
+ self._log: list[LogEntryWithTime] = []
+ self._progress_timer: QTimer | None = None
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
def _on_progress(self) -> None:
@@ -98,7 +98,7 @@ class MediaSyncer:
self._log_and_notify(tr.sync_media_failed())
showWarning(str(exc))
- def entries(self) -> List[LogEntryWithTime]:
+ def entries(self) -> list[LogEntryWithTime]:
return self._log
def abort(self) -> None:
@@ -125,7 +125,7 @@ class MediaSyncer:
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
diag.show()
- timer: Optional[QTimer] = None
+ timer: QTimer | None = None
def check_finished() -> None:
if not self.is_syncing():
diff --git a/qt/aqt/modelchooser.py b/qt/aqt/modelchooser.py
index 0ba83e022..06e3e4a92 100644
--- a/qt/aqt/modelchooser.py
+++ b/qt/aqt/modelchooser.py
@@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import List, Optional
+
+from typing import Optional
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
@@ -75,7 +76,7 @@ class ModelChooser(QHBoxLayout):
# edit button
edit = QPushButton(tr.qt_misc_manage(), clicked=self.onEdit) # type: ignore
- def nameFunc() -> List[str]:
+ def nameFunc() -> list[str]:
return [nt.name for nt in self.deck.models.all_names_and_ids()]
ret = StudyDeck(
diff --git a/qt/aqt/models.py b/qt/aqt/models.py
index b65be9d34..a87c7e04f 100644
--- a/qt/aqt/models.py
+++ b/qt/aqt/models.py
@@ -1,9 +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
+
from concurrent.futures import Future
from operator import itemgetter
-from typing import Any, List, Optional, Sequence
+from typing import Any, Optional, Sequence
import aqt.clayout
from anki import stdmodels
@@ -239,7 +241,7 @@ class AddModel(QDialog):
self.dialog.setupUi(self)
disable_help_button(self)
# standard models
- self.notetypes: List[
+ self.notetypes: list[
Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
] = []
for (name, func) in stdmodels.get_stock_notetypes(self.col):
diff --git a/qt/aqt/mpv.py b/qt/aqt/mpv.py
index ae996a939..188a1be75 100644
--- a/qt/aqt/mpv.py
+++ b/qt/aqt/mpv.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# ------------------------------------------------------------------------------
#
# mpv.py - Control mpv from Python using JSON IPC
@@ -41,7 +40,7 @@ from distutils.spawn import ( # pylint: disable=import-error,no-name-in-module
find_executable,
)
from queue import Empty, Full, Queue
-from typing import Dict, Optional
+from typing import Optional
from anki.utils import isWin
@@ -80,7 +79,7 @@ class MPVBase:
"""
executable = find_executable("mpv")
- popenEnv: Optional[Dict[str, str]] = None
+ popenEnv: Optional[dict[str, str]] = None
default_argv = [
"--idle",
diff --git a/qt/aqt/notetypechooser.py b/qt/aqt/notetypechooser.py
index 406534e9c..fceb1ca0d 100644
--- a/qt/aqt/notetypechooser.py
+++ b/qt/aqt/notetypechooser.py
@@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import List, Optional
+
+from typing import Optional
from anki.models import NotetypeId
from aqt import AnkiQt, gui_hooks
@@ -98,7 +99,7 @@ class NotetypeChooser(QHBoxLayout):
edit = QPushButton(tr.qt_misc_manage())
qconnect(edit.clicked, self.onEdit)
- def nameFunc() -> List[str]:
+ def nameFunc() -> list[str]:
return sorted(self.mw.col.models.all_names())
ret = StudyDeck(
diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py
index 01e6d17e8..c6019abfd 100644
--- a/qt/aqt/operations/__init__.py
+++ b/qt/aqt/operations/__init__.py
@@ -4,7 +4,7 @@
from __future__ import annotations
from concurrent.futures._base import Future
-from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union
+from typing import Any, Callable, Generic, Protocol, TypeVar, Union
import aqt
from anki.collection import (
@@ -61,26 +61,26 @@ class CollectionOp(Generic[ResultWithChanges]):
passed to `failure` if it is provided.
"""
- _success: Optional[Callable[[ResultWithChanges], Any]] = None
- _failure: Optional[Callable[[Exception], Any]] = None
+ _success: Callable[[ResultWithChanges], Any] | None = None
+ _failure: Callable[[Exception], Any] | None = None
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
self._parent = parent
self._op = op
def success(
- self, success: Optional[Callable[[ResultWithChanges], Any]]
+ self, success: Callable[[ResultWithChanges], Any] | None
) -> CollectionOp[ResultWithChanges]:
self._success = success
return self
def failure(
- self, failure: Optional[Callable[[Exception], Any]]
+ self, failure: Callable[[Exception], Any] | None
) -> CollectionOp[ResultWithChanges]:
self._failure = failure
return self
- def run_in_background(self, *, initiator: Optional[object] = None) -> None:
+ def run_in_background(self, *, initiator: object | None = None) -> None:
from aqt import mw
assert mw
@@ -121,7 +121,7 @@ class CollectionOp(Generic[ResultWithChanges]):
def _fire_change_hooks_after_op_performed(
self,
result: ResultWithChanges,
- handler: Optional[object],
+ handler: object | None,
) -> None:
from aqt import mw
@@ -158,8 +158,8 @@ class QueryOp(Generic[T]):
passed to `failure` if it is provided.
"""
- _failure: Optional[Callable[[Exception], Any]] = None
- _progress: Union[bool, str] = False
+ _failure: Callable[[Exception], Any] | None = None
+ _progress: bool | str = False
def __init__(
self,
@@ -172,11 +172,11 @@ class QueryOp(Generic[T]):
self._op = op
self._success = success
- def failure(self, failure: Optional[Callable[[Exception], Any]]) -> QueryOp[T]:
+ def failure(self, failure: Callable[[Exception], Any] | None) -> QueryOp[T]:
self._failure = failure
return self
- def with_progress(self, label: Optional[str] = None) -> QueryOp[T]:
+ def with_progress(self, label: str | None = None) -> QueryOp[T]:
self._progress = label or True
return self
@@ -190,7 +190,7 @@ class QueryOp(Generic[T]):
def wrapped_op() -> T:
assert mw
if self._progress:
- label: Optional[str]
+ label: str | None
if isinstance(self._progress, str):
label = self._progress
else:
diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py
index 9edc99c9c..60e6f0be3 100644
--- a/qt/aqt/operations/deck.py
+++ b/qt/aqt/operations/deck.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import Optional, Sequence
+from typing import Sequence
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs
@@ -50,7 +50,7 @@ def add_deck_dialog(
*,
parent: QWidget,
default_text: str = "",
-) -> Optional[CollectionOp[OpChangesWithId]]:
+) -> CollectionOp[OpChangesWithId] | None:
if name := getOnlyText(
tr.decks_new_deck_name(), default=default_text, parent=parent
).strip():
diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py
index de5be2f6b..c76971a4a 100644
--- a/qt/aqt/operations/note.py
+++ b/qt/aqt/operations/note.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import Optional, Sequence
+from typing import Sequence
from anki.collection import OpChanges, OpChangesWithCount
from anki.decks import DeckId
@@ -43,7 +43,7 @@ def find_and_replace(
search: str,
replacement: str,
regex: bool,
- field_name: Optional[str],
+ field_name: str | None,
match_case: bool,
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(
diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py
index 3e8eb6c89..7f397f6c8 100644
--- a/qt/aqt/operations/scheduling.py
+++ b/qt/aqt/operations/scheduling.py
@@ -3,7 +3,7 @@
from __future__ import annotations
-from typing import Optional, Sequence
+from typing import Sequence
import aqt
from anki.cards import CardId
@@ -29,8 +29,8 @@ def set_due_date_dialog(
*,
parent: QWidget,
card_ids: Sequence[CardId],
- config_key: Optional[Config.String.V],
-) -> Optional[CollectionOp[OpChanges]]:
+ config_key: Config.String.V | None,
+) -> CollectionOp[OpChanges] | None:
assert aqt.mw
if not card_ids:
return None
@@ -77,7 +77,7 @@ def forget_cards(
def reposition_new_cards_dialog(
*, parent: QWidget, card_ids: Sequence[CardId]
-) -> Optional[CollectionOp[OpChangesWithCount]]:
+) -> CollectionOp[OpChangesWithCount] | None:
from aqt import mw
assert mw
diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py
index 43210ad73..b7587af0b 100644
--- a/qt/aqt/overview.py
+++ b/qt/aqt/overview.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Any, Callable, Dict, List, Optional, Tuple
+from typing import Any, Callable
import aqt
from anki.collection import OpChanges
@@ -72,7 +72,7 @@ class Overview:
self.refresh()
def op_executed(
- self, changes: OpChanges, handler: Optional[object], focused: bool
+ self, changes: OpChanges, handler: object | None, focused: bool
) -> bool:
if changes.study_queues:
self._refresh_needed = True
@@ -115,7 +115,7 @@ class Overview:
openLink(url)
return False
- def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
+ def _shortcutKeys(self) -> list[tuple[str, Callable]]:
return [
("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
("r", self.rebuild_current_filtered_deck),
@@ -202,7 +202,7 @@ class Overview:
def _show_finished_screen(self) -> None:
self.web.load_ts_page("congrats")
- def _desc(self, deck: Dict[str, Any]) -> str:
+ def _desc(self, deck: dict[str, Any]) -> str:
if deck["dyn"]:
desc = tr.studying_this_is_a_special_deck_for()
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
@@ -219,19 +219,19 @@ class Overview:
dyn = ""
return f'{desc}
'
- def _table(self) -> Optional[str]:
+ def _table(self) -> str | None:
counts = list(self.mw.col.sched.counts())
but = self.mw.button
return """
-%s: | %s |
-%s: | %s |
-%s: | %s |
+{}: | {} |
+{}: | {} |
+{}: | {} |
|
-%s |
""" % (
+{}""".format(
tr.actions_new(),
counts[0],
tr.scheduling_learning(),
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index 27a692c4e..0b22a983f 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -9,7 +9,7 @@ import random
import shutil
import traceback
from enum import Enum
-from typing import Any, Dict, List, Optional
+from typing import Any
from send2trash import send2trash
@@ -62,7 +62,7 @@ class VideoDriver(Enum):
return VideoDriver.Software
@staticmethod
- def all_for_platform() -> List[VideoDriver]:
+ def all_for_platform() -> list[VideoDriver]:
all = [VideoDriver.OpenGL]
if isWin:
all.append(VideoDriver.ANGLE)
@@ -81,7 +81,7 @@ metaConf = dict(
defaultLang=None,
)
-profileConf: Dict[str, Any] = dict(
+profileConf: dict[str, Any] = dict(
# profile
mainWindowGeom=None,
mainWindowState=None,
@@ -118,12 +118,12 @@ class AnkiRestart(SystemExit):
class ProfileManager:
- def __init__(self, base: Optional[str] = None) -> None: #
+ def __init__(self, base: str | None = None) -> None: #
## Settings which should be forgotten each Anki restart
- self.session: Dict[str, Any] = {}
- self.name: Optional[str] = None
- self.db: Optional[DB] = None
- self.profile: Optional[Dict] = None
+ self.session: dict[str, Any] = {}
+ self.name: str | None = None
+ self.db: DB | None = None
+ self.profile: dict | None = None
# instantiate base folder
self.base: str
self._setBaseFolder(base)
@@ -245,8 +245,8 @@ class ProfileManager:
# Profile load/save
######################################################################
- def profiles(self) -> List:
- def names() -> List:
+ def profiles(self) -> list:
+ def names() -> list:
return self.db.list("select name from profiles where name != '_global'")
n = names()
@@ -393,7 +393,7 @@ class ProfileManager:
# Downgrade
######################################################################
- def downgrade(self, profiles: List[str]) -> List[str]:
+ def downgrade(self, profiles: list[str]) -> list[str]:
"Downgrade all profiles. Return a list of profiles that couldn't be opened."
problem_profiles = []
for name in profiles:
@@ -420,7 +420,7 @@ class ProfileManager:
os.makedirs(path)
return path
- def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
+ def _setBaseFolder(self, cmdlineBase: str | None) -> None:
if cmdlineBase:
self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"):
@@ -612,13 +612,13 @@ create table if not exists profiles
# Profile-specific
######################################################################
- def set_sync_key(self, val: Optional[str]) -> None:
+ def set_sync_key(self, val: str | None) -> None:
self.profile["syncKey"] = val
- def set_sync_username(self, val: Optional[str]) -> None:
+ def set_sync_username(self, val: str | None) -> None:
self.profile["syncUser"] = val
- def set_host_number(self, val: Optional[int]) -> None:
+ def set_host_number(self, val: int | None) -> None:
self.profile["hostNum"] = val or 0
def media_syncing_enabled(self) -> bool:
@@ -627,7 +627,7 @@ create table if not exists profiles
def auto_syncing_enabled(self) -> bool:
return self.profile["autoSync"]
- def sync_auth(self) -> Optional[SyncAuth]:
+ def sync_auth(self) -> SyncAuth | None:
hkey = self.profile.get("syncKey")
if not hkey:
return None
diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py
index 1e11d10f3..2d8b8e290 100644
--- a/qt/aqt/progress.py
+++ b/qt/aqt/progress.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import time
-from typing import Callable, Optional
import aqt.forms
from aqt.qt import *
@@ -19,9 +18,9 @@ class ProgressManager:
self.app = mw.app
self.inDB = False
self.blockUpdates = False
- self._show_timer: Optional[QTimer] = None
- self._busy_cursor_timer: Optional[QTimer] = None
- self._win: Optional[ProgressDialog] = None
+ self._show_timer: QTimer | None = None
+ self._busy_cursor_timer: QTimer | None = None
+ self._win: ProgressDialog | None = None
self._levels = 0
# Safer timers
@@ -74,10 +73,10 @@ class ProgressManager:
self,
max: int = 0,
min: int = 0,
- label: Optional[str] = None,
- parent: Optional[QWidget] = None,
+ label: str | None = None,
+ parent: QWidget | None = None,
immediate: bool = False,
- ) -> Optional[ProgressDialog]:
+ ) -> ProgressDialog | None:
self._levels += 1
if self._levels > 1:
return None
@@ -112,11 +111,11 @@ class ProgressManager:
def update(
self,
- label: Optional[str] = None,
- value: Optional[int] = None,
+ label: str | None = None,
+ value: int | None = None,
process: bool = True,
maybeShow: bool = True,
- max: Optional[int] = None,
+ max: int | None = None,
) -> None:
# print self._min, self._counter, self._max, label, time.time() - self._lastTime
if not self.mw.inMainThread():
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index c32aedf60..57613b99f 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -11,7 +11,7 @@ import re
import unicodedata as ucd
from dataclasses import dataclass
from enum import Enum, auto
-from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast
+from typing import Any, Callable, Literal, Match, Sequence, cast
from PyQt5.QtCore import Qt
@@ -85,7 +85,7 @@ class V3CardInfo:
def top_card(self) -> QueuedCards.QueuedCard:
return self.queued_cards.cards[0]
- def counts(self) -> Tuple[int, List[int]]:
+ def counts(self) -> tuple[int, list[int]]:
"Returns (idx, counts)."
counts = [
self.queued_cards.new_count,
@@ -117,16 +117,16 @@ class Reviewer:
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
self.web = mw.web
- self.card: Optional[Card] = None
- self.cardQueue: List[Card] = []
- self.previous_card: Optional[Card] = None
+ self.card: Card | None = None
+ self.cardQueue: list[Card] = []
+ self.previous_card: Card | None = None
self.hadCardQueue = False
- self._answeredIds: List[CardId] = []
- self._recordedAudio: Optional[str] = None
+ self._answeredIds: list[CardId] = []
+ self._recordedAudio: str | None = None
self.typeCorrect: str = None # web init happens before this is set
- self.state: Optional[str] = None
- self._refresh_needed: Optional[RefreshNeeded] = None
- self._v3: Optional[V3CardInfo] = None
+ self.state: str | None = None
+ self._refresh_needed: RefreshNeeded | None = None
+ self._v3: V3CardInfo | None = None
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech)
@@ -141,7 +141,7 @@ class Reviewer:
self.refresh_if_needed()
# this is only used by add-ons
- def lastCard(self) -> Optional[Card]:
+ def lastCard(self) -> Card | None:
if self._answeredIds:
if not self.card or self._answeredIds[-1] != self.card.id:
try:
@@ -167,7 +167,7 @@ class Reviewer:
self._refresh_needed = None
def op_executed(
- self, changes: OpChanges, handler: Optional[object], focused: bool
+ self, changes: OpChanges, handler: object | None, focused: bool
) -> bool:
if handler is not self:
if changes.study_queues:
@@ -234,7 +234,7 @@ class Reviewer:
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
self.card.start_timer()
- def get_next_states(self) -> Optional[NextStates]:
+ def get_next_states(self) -> NextStates | None:
if v3 := self._v3:
return v3.next_states
else:
@@ -434,7 +434,7 @@ class Reviewer:
def _shortcutKeys(
self,
- ) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
+ ) -> Sequence[Union[tuple[str, Callable], tuple[Qt.Key, Callable]]]:
return [
("e", self.mw.onEditCurrent),
(" ", self.onEnterKey),
@@ -587,7 +587,7 @@ class Reviewer:
# can't pass a string in directly, and can't use re.escape as it
# escapes too much
s = """
-%s""" % (
+{}""".format(
self.typeFont,
self.typeSize,
res,
@@ -620,23 +620,23 @@ class Reviewer:
def tokenizeComparison(
self, given: str, correct: str
- ) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
+ ) -> tuple[list[tuple[bool, str]], list[tuple[bool, str]]]:
# compare in NFC form so accents appear correct
given = ucd.normalize("NFC", given)
correct = ucd.normalize("NFC", correct)
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
- givenElems: List[Tuple[bool, str]] = []
- correctElems: List[Tuple[bool, str]] = []
+ givenElems: list[tuple[bool, str]] = []
+ correctElems: list[tuple[bool, str]] = []
givenPoint = 0
correctPoint = 0
offby = 0
- def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
+ def logBad(old: int, new: int, s: str, array: list[tuple[bool, str]]) -> None:
if old != new:
array.append((False, s[old:new]))
def logGood(
- start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
+ start: int, cnt: int, s: str, array: list[tuple[bool, str]]
) -> None:
if cnt:
array.append((True, s[start : start + cnt]))
@@ -737,8 +737,8 @@ time = %(time)d;
def _showAnswerButton(self) -> None:
middle = """
-%s
-""" % (
+{}
+""".format(
self._remaining(),
tr.actions_shortcut_key(val=tr.studying_space()),
tr.studying_show_answer(),
@@ -763,10 +763,10 @@ time = %(time)d;
if not self.mw.col.conf["dueCounts"]:
return ""
- counts: List[Union[int, str]]
+ counts: list[Union[int, str]]
if v3 := self._v3:
idx, counts_ = v3.counts()
- counts = cast(List[Union[int, str]], counts_)
+ counts = cast(list[Union[int, str]], counts_)
else:
# v1/v2 scheduler
if self.hadCardQueue:
@@ -790,10 +790,10 @@ time = %(time)d;
else:
return 2
- def _answerButtonList(self) -> Tuple[Tuple[int, str], ...]:
+ def _answerButtonList(self) -> tuple[tuple[int, str], ...]:
button_count = self.mw.col.sched.answerButtons(self.card)
if button_count == 2:
- buttons_tuple: Tuple[Tuple[int, str], ...] = (
+ buttons_tuple: tuple[tuple[int, str], ...] = (
(1, tr.studying_again()),
(2, tr.studying_good()),
)
@@ -847,7 +847,7 @@ time = %(time)d;
buf += ""
return buf
- def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str:
+ def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str:
if not self.mw.col.conf["estTimes"]:
return ""
if v3_labels:
@@ -859,7 +859,7 @@ time = %(time)d;
# Leeches
##########################################################################
- def onLeech(self, card: Optional[Card] = None) -> None:
+ def onLeech(self, card: Card | None = None) -> None:
# for now
s = tr.studying_card_was_a_leech()
# v3 scheduler doesn't report this
@@ -891,7 +891,7 @@ time = %(time)d;
##########################################################################
# note the shortcuts listed here also need to be defined above
- def _contextMenu(self) -> List[Any]:
+ def _contextMenu(self) -> list[Any]:
currentFlag = self.card and self.card.user_flag()
opts = [
[
diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py
index 9b287b667..f00568fc7 100644
--- a/qt/aqt/sound.py
+++ b/qt/aqt/sound.py
@@ -15,7 +15,7 @@ import wave
from abc import ABC, abstractmethod
from concurrent.futures import Future
from operator import itemgetter
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast
+from typing import TYPE_CHECKING, Any, Callable, cast
from markdown import markdown
@@ -60,7 +60,7 @@ class Player(ABC):
"""
@abstractmethod
- def rank_for_tag(self, tag: AVTag) -> Optional[int]:
+ def rank_for_tag(self, tag: AVTag) -> int | None:
"""How suited this player is to playing tag.
AVPlayer will choose the player that returns the highest rank
@@ -105,7 +105,7 @@ def is_audio_file(fname: str) -> bool:
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
default_rank = 0
- def rank_for_tag(self, tag: AVTag) -> Optional[int]:
+ def rank_for_tag(self, tag: AVTag) -> int | None:
if isinstance(tag, SoundOrVideoTag):
return self.default_rank
else:
@@ -115,7 +115,7 @@ class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
class SoundPlayer(Player): # pylint: disable=abstract-method
default_rank = 0
- def rank_for_tag(self, tag: AVTag) -> Optional[int]:
+ def rank_for_tag(self, tag: AVTag) -> int | None:
if isinstance(tag, SoundOrVideoTag) and is_audio_file(tag.filename):
return self.default_rank
else:
@@ -125,7 +125,7 @@ class SoundPlayer(Player): # pylint: disable=abstract-method
class VideoPlayer(Player): # pylint: disable=abstract-method
default_rank = 0
- def rank_for_tag(self, tag: AVTag) -> Optional[int]:
+ def rank_for_tag(self, tag: AVTag) -> int | None:
if isinstance(tag, SoundOrVideoTag) and not is_audio_file(tag.filename):
return self.default_rank
else:
@@ -137,16 +137,16 @@ class VideoPlayer(Player): # pylint: disable=abstract-method
class AVPlayer:
- players: List[Player] = []
+ players: list[Player] = []
# when a new batch of audio is played, should the currently playing
# audio be stopped?
interrupt_current_audio = True
def __init__(self) -> None:
- self._enqueued: List[AVTag] = []
- self.current_player: Optional[Player] = None
+ self._enqueued: list[AVTag] = []
+ self.current_player: Player | None = None
- def play_tags(self, tags: List[AVTag]) -> None:
+ def play_tags(self, tags: list[AVTag]) -> None:
"""Clear the existing queue, then start playing provided tags."""
self.clear_queue_and_maybe_interrupt()
self._enqueued = tags[:]
@@ -185,7 +185,7 @@ class AVPlayer:
if self.current_player:
self.current_player.stop()
- def _pop_next(self) -> Optional[AVTag]:
+ def _pop_next(self) -> AVTag | None:
if not self._enqueued:
return None
return self._enqueued.pop(0)
@@ -212,7 +212,7 @@ class AVPlayer:
else:
tooltip(f"no players found for {tag}")
- def _best_player_for_tag(self, tag: AVTag) -> Optional[Player]:
+ def _best_player_for_tag(self, tag: AVTag) -> Player | None:
ranked = []
for p in self.players:
rank = p.rank_for_tag(tag)
@@ -234,7 +234,7 @@ av_player = AVPlayer()
# return modified command array that points to bundled command, and return
# required environment
-def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]:
+def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
cmd = cmd[:]
env = os.environ.copy()
if "LD_LIBRARY_PATH" in env:
@@ -275,13 +275,13 @@ def retryWait(proc: subprocess.Popen) -> int:
class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
"A player that invokes a new process for each tag to play."
- args: List[str] = []
- env: Optional[Dict[str, str]] = None
+ args: list[str] = []
+ env: dict[str, str] | None = None
def __init__(self, taskman: TaskManager) -> None:
self._taskman = taskman
self._terminate_flag = False
- self._process: Optional[subprocess.Popen] = None
+ self._process: subprocess.Popen | None = None
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
self._terminate_flag = False
@@ -388,7 +388,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
def __init__(self, base_path: str) -> None:
mpvPath, self.popenEnv = _packagedCmd(["mpv"])
self.executable = mpvPath[0]
- self._on_done: Optional[OnDoneCallback] = None
+ self._on_done: OnDoneCallback | None = None
self.default_argv += [f"--config-dir={base_path}"]
super().__init__(window_id=None, debug=False)
@@ -635,7 +635,7 @@ except:
PYAU_CHANNELS = 1
-PYAU_INPUT_INDEX: Optional[int] = None
+PYAU_INPUT_INDEX: int | None = None
class PyAudioThreadedRecorder(threading.Thread):
@@ -839,7 +839,7 @@ def playFromText(text: Any) -> None:
# legacy globals
_player = play
_queueEraser = clearAudioQueue
-mpvManager: Optional["MpvManager"] = None
+mpvManager: MpvManager | None = None
# add everything from this module into anki.sound for backwards compat
_exports = [i for i in locals().items() if not i[0].startswith("__")]
diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py
index 199106e94..f1a2953c9 100644
--- a/qt/aqt/studydeck.py
+++ b/qt/aqt/studydeck.py
@@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import List, Optional
+from typing import Optional
import aqt
from anki.collection import OpChangesWithId
@@ -34,7 +34,7 @@ class StudyDeck(QDialog):
cancel: bool = True,
parent: Optional[QWidget] = None,
dyn: bool = False,
- buttons: Optional[List[Union[str, QPushButton]]] = None,
+ buttons: Optional[list[Union[str, QPushButton]]] = None,
geomKey: str = "default",
) -> None:
QDialog.__init__(self, parent or mw)
diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py
index cd594911d..bc97b44fd 100644
--- a/qt/aqt/switch.py
+++ b/qt/aqt/switch.py
@@ -1,7 +1,5 @@
-# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-from typing import Tuple
from aqt import colors
from aqt.qt import *
@@ -22,8 +20,8 @@ class Switch(QAbstractButton):
radius: int = 10,
left_label: str = "",
right_label: str = "",
- left_color: Tuple[str, str] = colors.FLAG4_BG,
- right_color: Tuple[str, str] = colors.FLAG3_BG,
+ left_color: tuple[str, str] = colors.FLAG4_BG,
+ right_color: tuple[str, str] = colors.FLAG3_BG,
parent: QWidget = None,
) -> None:
super().__init__(parent=parent)
diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py
index c1e3b4573..80eb550c1 100644
--- a/qt/aqt/sync.py
+++ b/qt/aqt/sync.py
@@ -6,7 +6,7 @@ from __future__ import annotations
import enum
import os
from concurrent.futures import Future
-from typing import Callable, Tuple
+from typing import Callable
import aqt
from anki.errors import Interrupted, SyncError, SyncErrorKind
@@ -288,7 +288,7 @@ def ask_user_to_decide_direction() -> FullSyncChoice:
def get_id_and_pass_from_user(
mw: aqt.main.AnkiQt, username: str = "", password: str = ""
-) -> Tuple[str, str]:
+) -> tuple[str, str]:
diag = QDialog(mw)
diag.setWindowTitle("Anki")
disable_help_button(diag)
diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py
index df01f3acf..b616b1737 100644
--- a/qt/aqt/tagedit.py
+++ b/qt/aqt/tagedit.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import re
-from typing import Iterable, List, Optional, Union
+from typing import Iterable
from anki.collection import Collection
from aqt import gui_hooks
@@ -12,14 +12,14 @@ from aqt.qt import *
class TagEdit(QLineEdit):
- _completer: Union[QCompleter, TagCompleter]
+ _completer: QCompleter | TagCompleter
lostFocus = pyqtSignal()
# 0 = tags, 1 = decks
def __init__(self, parent: QWidget, type: int = 0) -> None:
QLineEdit.__init__(self, parent)
- self.col: Optional[Collection] = None
+ self.col: Collection | None = None
self.model = QStringListModel()
self.type = type
if type == 0:
@@ -112,11 +112,11 @@ class TagCompleter(QCompleter):
edit: TagEdit,
) -> None:
QCompleter.__init__(self, model, parent)
- self.tags: List[str] = []
+ self.tags: list[str] = []
self.edit = edit
- self.cursor: Optional[int] = None
+ self.cursor: int | None = None
- def splitPath(self, tags: str) -> List[str]:
+ def splitPath(self, tags: str) -> list[str]:
stripped_tags = tags.strip()
stripped_tags = re.sub(" +", " ", stripped_tags)
self.tags = self.edit.col.tags.split(stripped_tags)
diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py
index d0d4706a8..f380f7819 100644
--- a/qt/aqt/taglimit.py
+++ b/qt/aqt/taglimit.py
@@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html
-from typing import List, Optional
+
+from typing import Optional
import aqt
from anki.lang import with_collapsed_whitespace
@@ -14,7 +15,7 @@ class TagLimit(QDialog):
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
QDialog.__init__(self, parent, Qt.Window)
self.tags: str = ""
- self.tags_list: List[str] = []
+ self.tags_list: list[str] = []
self.mw = mw
self.parent_: Optional[CustomStudy] = parent
self.deck = self.parent_.deck
diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py
index 18c1bdbfc..3c62679ab 100644
--- a/qt/aqt/taskman.py
+++ b/qt/aqt/taskman.py
@@ -12,7 +12,7 @@ from __future__ import annotations
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from threading import Lock
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from PyQt5.QtCore import QObject, pyqtSignal
@@ -29,7 +29,7 @@ class TaskManager(QObject):
QObject.__init__(self)
self.mw = mw.weakref()
self._executor = ThreadPoolExecutor()
- self._closures: List[Closure] = []
+ self._closures: list[Closure] = []
self._closures_lock = Lock()
qconnect(self._closures_pending, self._on_closures_pending)
@@ -42,8 +42,8 @@ class TaskManager(QObject):
def run_in_background(
self,
task: Callable,
- on_done: Optional[Callable[[Future], None]] = None,
- args: Optional[Dict[str, Any]] = None,
+ on_done: Callable[[Future], None] | None = None,
+ args: dict[str, Any] | None = None,
) -> Future:
"""Use QueryOp()/CollectionOp() in new code.
@@ -76,9 +76,9 @@ class TaskManager(QObject):
def with_progress(
self,
task: Callable,
- on_done: Optional[Callable[[Future], None]] = None,
- parent: Optional[QWidget] = None,
- label: Optional[str] = None,
+ on_done: Callable[[Future], None] | None = None,
+ parent: QWidget | None = None,
+ label: str | None = None,
immediate: bool = False,
) -> None:
"Use QueryOp()/CollectionOp() in new code."
diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py
index 75a10e96f..b3faad78e 100644
--- a/qt/aqt/theme.py
+++ b/qt/aqt/theme.py
@@ -5,7 +5,6 @@ from __future__ import annotations
import platform
from dataclasses import dataclass
-from typing import Dict, Optional, Tuple, Union
from anki.utils import isMac
from aqt import QApplication, colors, gui_hooks, isWin
@@ -17,7 +16,7 @@ from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt
class ColoredIcon:
path: str
# (day, night)
- color: Tuple[str, str]
+ color: tuple[str, str]
def current_color(self, night_mode: bool) -> str:
if night_mode:
@@ -25,17 +24,17 @@ class ColoredIcon:
else:
return self.color[0]
- def with_color(self, color: Tuple[str, str]) -> ColoredIcon:
+ def with_color(self, color: tuple[str, str]) -> ColoredIcon:
return ColoredIcon(path=self.path, color=color)
class ThemeManager:
_night_mode_preference = False
- _icon_cache_light: Dict[str, QIcon] = {}
- _icon_cache_dark: Dict[str, QIcon] = {}
+ _icon_cache_light: dict[str, QIcon] = {}
+ _icon_cache_dark: dict[str, QIcon] = {}
_icon_size = 128
- _dark_mode_available: Optional[bool] = None
- default_palette: Optional[QPalette] = None
+ _dark_mode_available: bool | None = None
+ default_palette: QPalette | None = None
# Qt applies a gradient to the buttons in dark mode
# from about #505050 to #606060.
@@ -65,7 +64,7 @@ class ThemeManager:
night_mode = property(get_night_mode, set_night_mode)
- def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon:
+ def icon_from_resources(self, path: str | ColoredIcon) -> QIcon:
"Fetch icon from Qt resources, and invert if in night mode."
if self.night_mode:
cache = self._icon_cache_light
@@ -101,7 +100,7 @@ class ThemeManager:
return cache.setdefault(path, icon)
- def body_class(self, night_mode: Optional[bool] = None) -> str:
+ def body_class(self, night_mode: bool | None = None) -> str:
"Returns space-separated class list for platform/theme."
classes = []
if isWin:
@@ -120,17 +119,17 @@ class ThemeManager:
return " ".join(classes)
def body_classes_for_card_ord(
- self, card_ord: int, night_mode: Optional[bool] = None
+ self, card_ord: int, night_mode: bool | None = None
) -> str:
"Returns body classes used when showing a card."
return f"card card{card_ord+1} {self.body_class(night_mode)}"
- def color(self, colors: Tuple[str, str]) -> str:
+ def color(self, colors: tuple[str, str]) -> str:
"""Given day/night colors, return the correct one for the current theme."""
idx = 1 if self.night_mode else 0
return colors[idx]
- def qcolor(self, colors: Tuple[str, str]) -> QColor:
+ def qcolor(self, colors: tuple[str, str]) -> QColor:
return QColor(self.color(colors))
def apply_style(self, app: QApplication) -> None:
@@ -166,27 +165,27 @@ QToolTip {
if not self.macos_dark_mode():
buf += """
-QScrollBar { background-color: %s; }
-QScrollBar::handle { background-color: %s; border-radius: 5px; }
+QScrollBar {{ background-color: {}; }}
+QScrollBar::handle {{ background-color: {}; border-radius: 5px; }}
-QScrollBar:horizontal { height: 12px; }
-QScrollBar::handle:horizontal { min-width: 50px; }
+QScrollBar:horizontal {{ height: 12px; }}
+QScrollBar::handle:horizontal {{ min-width: 50px; }}
-QScrollBar:vertical { width: 12px; }
-QScrollBar::handle:vertical { min-height: 50px; }
+QScrollBar:vertical {{ width: 12px; }}
+QScrollBar::handle:vertical {{ min-height: 50px; }}
-QScrollBar::add-line {
+QScrollBar::add-line {{
border: none;
background: none;
-}
+}}
-QScrollBar::sub-line {
+QScrollBar::sub-line {{
border: none;
background: none;
-}
+}}
-QTabWidget { background-color: %s; }
-""" % (
+QTabWidget {{ background-color: {}; }}
+""".format(
self.color(colors.WINDOW_BG),
# fushion-button-hover-bg
"#656565",
diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py
index a4b06a104..b12ab94cb 100644
--- a/qt/aqt/toolbar.py
+++ b/qt/aqt/toolbar.py
@@ -2,7 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
-from typing import Any, Dict, Optional
+from typing import Any
import aqt
from anki.sync import SyncStatus
@@ -29,7 +29,7 @@ class Toolbar:
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
self.mw = mw
self.web = web
- self.link_handlers: Dict[str, Callable] = {
+ self.link_handlers: dict[str, Callable] = {
"study": self._studyLinkHandler,
}
self.web.setFixedHeight(30)
@@ -38,8 +38,8 @@ class Toolbar:
def draw(
self,
buf: str = "",
- web_context: Optional[Any] = None,
- link_handler: Optional[Callable[[str], Any]] = None,
+ web_context: Any | None = None,
+ link_handler: Callable[[str], Any] | None = None,
) -> None:
web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler
@@ -65,8 +65,8 @@ class Toolbar:
cmd: str,
label: str,
func: Callable,
- tip: Optional[str] = None,
- id: Optional[str] = None,
+ tip: str | None = None,
+ id: str | None = None,
) -> str:
"""Generates HTML link element and registers link handler
@@ -218,8 +218,8 @@ class BottomBar(Toolbar):
def draw(
self,
buf: str = "",
- web_context: Optional[Any] = None,
- link_handler: Optional[Callable[[str], Any]] = None,
+ web_context: Any | None = None,
+ link_handler: Callable[[str], Any] | None = None,
) -> None:
# note: some screens may override this
web_context = web_context or BottomToolbar(self)
diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py
index fb8e52035..4a49c4f4d 100644
--- a/qt/aqt/tts.py
+++ b/qt/aqt/tts.py
@@ -36,7 +36,7 @@ import threading
from concurrent.futures import Future
from dataclasses import dataclass
from operator import attrgetter
-from typing import Any, List, Optional, cast
+from typing import Any, cast
import anki
from anki import hooks
@@ -61,17 +61,17 @@ class TTSVoiceMatch:
class TTSPlayer:
default_rank = 0
- _available_voices: Optional[List[TTSVoice]] = None
+ _available_voices: list[TTSVoice] | None = None
- def get_available_voices(self) -> List[TTSVoice]:
+ def get_available_voices(self) -> list[TTSVoice]:
return []
- def voices(self) -> List[TTSVoice]:
+ def voices(self) -> list[TTSVoice]:
if self._available_voices is None:
self._available_voices = self.get_available_voices()
return self._available_voices
- def voice_for_tag(self, tag: TTSTag) -> Optional[TTSVoiceMatch]:
+ def voice_for_tag(self, tag: TTSTag) -> TTSVoiceMatch | None:
avail_voices = self.voices()
rank = self.default_rank
@@ -103,7 +103,7 @@ class TTSPlayer:
class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
# mypy gets confused if rank_for_tag is defined in TTSPlayer
- def rank_for_tag(self, tag: AVTag) -> Optional[int]:
+ def rank_for_tag(self, tag: AVTag) -> int | None:
if not isinstance(tag, TTSTag):
return None
@@ -118,10 +118,10 @@ class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
##########################################################################
-def all_tts_voices() -> List[TTSVoice]:
+def all_tts_voices() -> list[TTSVoice]:
from aqt.sound import av_player
- all_voices: List[TTSVoice] = []
+ all_voices: list[TTSVoice] = []
for p in av_player.players:
getter = getattr(p, "voices", None)
if not getter:
@@ -185,7 +185,7 @@ class MacTTSPlayer(TTSProcessPlayer):
self._process.stdin.close()
self._wait_for_termination(tag)
- def get_available_voices(self) -> List[TTSVoice]:
+ def get_available_voices(self) -> list[TTSVoice]:
cmd = subprocess.run(
["say", "-v", "?"], capture_output=True, check=True, encoding="utf8"
)
@@ -197,7 +197,7 @@ class MacTTSPlayer(TTSProcessPlayer):
voices.append(voice)
return voices
- def _parse_voice_line(self, line: str) -> Optional[TTSVoice]:
+ def _parse_voice_line(self, line: str) -> TTSVoice | None:
m = self.VOICE_HELP_LINE_RE.match(line)
if not m:
return None
@@ -484,7 +484,7 @@ if isWin:
except:
speaker = None
- def get_available_voices(self) -> List[TTSVoice]:
+ def get_available_voices(self) -> list[TTSVoice]:
if self.speaker is None:
return []
return list(map(self._voice_to_object, self.speaker.GetVoices()))
@@ -533,7 +533,7 @@ if isWin:
id: Any
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
- voice_list: List[Any] = []
+ voice_list: list[Any] = []
tmppath = os.path.join(tmpdir(), "tts.wav")
def import_voices(self) -> None:
@@ -545,7 +545,7 @@ if isWin:
print("winrt tts voices unavailable:", e)
self.voice_list = []
- def get_available_voices(self) -> List[TTSVoice]:
+ def get_available_voices(self) -> list[TTSVoice]:
t = threading.Thread(target=self.import_voices)
t.start()
t.join()
diff --git a/qt/aqt/update.py b/qt/aqt/update.py
index 231660259..d0f5ee144 100644
--- a/qt/aqt/update.py
+++ b/qt/aqt/update.py
@@ -2,7 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import time
-from typing import Any, Dict
+from typing import Any
import requests
@@ -24,7 +24,7 @@ class LatestVersionFinder(QThread):
self.main = main
self.config = main.pm.meta
- def _data(self) -> Dict[str, Any]:
+ def _data(self) -> dict[str, Any]:
return {
"ver": versionWithBuild(),
"os": platDesc(),
@@ -73,6 +73,6 @@ def askAndUpdate(mw: aqt.AnkiQt, ver: str) -> None:
openLink(aqt.appWebsite)
-def showMessages(mw: aqt.AnkiQt, data: Dict) -> None:
+def showMessages(mw: aqt.AnkiQt, data: dict) -> None:
showText(data["msg"], parent=mw, type="html")
mw.pm.meta["lastMsg"] = data["msgId"]
diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py
index 99c486b2b..b120ae17f 100644
--- a/qt/aqt/utils.py
+++ b/qt/aqt/utils.py
@@ -7,18 +7,7 @@ import re
import subprocess
import sys
from functools import wraps
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- List,
- Literal,
- Optional,
- Sequence,
- Tuple,
- Union,
- cast,
-)
+from typing import TYPE_CHECKING, Any, Literal, Sequence, cast
from PyQt5.QtWidgets import (
QAction,
@@ -79,7 +68,7 @@ def openHelp(section: HelpPageArgument) -> None:
openLink(link)
-def openLink(link: Union[str, QUrl]) -> None:
+def openLink(link: str | QUrl) -> None:
tooltip(tr.qt_misc_loading(), period=1000)
with noBundledLibs():
QDesktopServices.openUrl(QUrl(link))
@@ -87,10 +76,10 @@ def openLink(link: Union[str, QUrl]) -> None:
def showWarning(
text: str,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
help: HelpPageArgument = "",
title: str = "Anki",
- textFormat: Optional[TextFormat] = None,
+ textFormat: TextFormat | None = None,
) -> int:
"Show a small warning with an OK button."
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
@@ -98,10 +87,10 @@ def showWarning(
def showCritical(
text: str,
- parent: Optional[QDialog] = None,
+ parent: QDialog | None = None,
help: str = "",
title: str = "Anki",
- textFormat: Optional[TextFormat] = None,
+ textFormat: TextFormat | None = None,
) -> int:
"Show a small critical error with an OK button."
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
@@ -109,12 +98,12 @@ def showCritical(
def showInfo(
text: str,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
help: HelpPageArgument = "",
type: str = "info",
title: str = "Anki",
- textFormat: Optional[TextFormat] = None,
- customBtns: Optional[List[QMessageBox.StandardButton]] = None,
+ textFormat: TextFormat | None = None,
+ customBtns: list[QMessageBox.StandardButton] | None = None,
) -> int:
"Show a small info window with an OK button."
parent_widget: QWidget
@@ -157,16 +146,16 @@ def showInfo(
def showText(
txt: str,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
type: str = "text",
run: bool = True,
- geomKey: Optional[str] = None,
+ geomKey: str | None = None,
minWidth: int = 500,
minHeight: int = 400,
title: str = "Anki",
copyBtn: bool = False,
plain_text_edit: bool = False,
-) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
+) -> tuple[QDialog, QDialogButtonBox] | None:
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
diag = QDialog(parent)
@@ -174,7 +163,7 @@ def showText(
disable_help_button(diag)
layout = QVBoxLayout(diag)
diag.setLayout(layout)
- text: Union[QPlainTextEdit, QTextBrowser]
+ text: QPlainTextEdit | QTextBrowser
if plain_text_edit:
# used by the importer
text = QPlainTextEdit()
@@ -228,7 +217,7 @@ def askUser(
parent: QWidget = None,
help: HelpPageArgument = None,
defaultno: bool = False,
- msgfunc: Optional[Callable] = None,
+ msgfunc: Callable | None = None,
title: str = "Anki",
) -> bool:
"Show a yes/no question. Return true if yes."
@@ -256,13 +245,13 @@ class ButtonedDialog(QMessageBox):
def __init__(
self,
text: str,
- buttons: List[str],
- parent: Optional[QWidget] = None,
+ buttons: list[str],
+ parent: QWidget | None = None,
help: HelpPageArgument = None,
title: str = "Anki",
):
QMessageBox.__init__(self, parent)
- self._buttons: List[QPushButton] = []
+ self._buttons: list[QPushButton] = []
self.setWindowTitle(title)
self.help = help
self.setIcon(QMessageBox.Warning)
@@ -289,8 +278,8 @@ class ButtonedDialog(QMessageBox):
def askUserDialog(
text: str,
- buttons: List[str],
- parent: Optional[QWidget] = None,
+ buttons: list[str],
+ parent: QWidget | None = None,
help: HelpPageArgument = None,
title: str = "Anki",
) -> ButtonedDialog:
@@ -303,10 +292,10 @@ def askUserDialog(
class GetTextDialog(QDialog):
def __init__(
self,
- parent: Optional[QWidget],
+ parent: QWidget | None,
question: str,
help: HelpPageArgument = None,
- edit: Optional[QLineEdit] = None,
+ edit: QLineEdit | None = None,
default: str = "",
title: str = "Anki",
minWidth: int = 400,
@@ -350,14 +339,14 @@ class GetTextDialog(QDialog):
def getText(
prompt: str,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
help: HelpPageArgument = None,
- edit: Optional[QLineEdit] = None,
+ edit: QLineEdit | None = None,
default: str = "",
title: str = "Anki",
- geomKey: Optional[str] = None,
+ geomKey: str | None = None,
**kwargs: Any,
-) -> Tuple[str, int]:
+) -> tuple[str, int]:
"Returns (string, succeeded)."
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
@@ -384,7 +373,7 @@ def getOnlyText(*args: Any, **kwargs: Any) -> str:
# fixme: these utilities could be combined into a single base class
# unused by Anki, but used by add-ons
def chooseList(
- prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
+ prompt: str, choices: list[str], startrow: int = 0, parent: Any = None
) -> int:
if not parent:
parent = aqt.mw.app.activeWindow()
@@ -408,7 +397,7 @@ def chooseList(
def getTag(
parent: QWidget, deck: Collection, question: str, **kwargs: Any
-) -> Tuple[str, int]:
+) -> tuple[str, int]:
from aqt.tagedit import TagEdit
te = TagEdit(parent)
@@ -433,12 +422,12 @@ def getFile(
parent: QWidget,
title: str,
# single file returned unless multi=True
- cb: Optional[Callable[[Union[str, Sequence[str]]], None]],
+ cb: Callable[[str | Sequence[str]], None] | None,
filter: str = "*.*",
- dir: Optional[str] = None,
- key: Optional[str] = None,
+ dir: str | None = None,
+ key: str | None = None,
multi: bool = False, # controls whether a single or multiple files is returned
-) -> Optional[Union[Sequence[str], str]]:
+) -> Sequence[str] | str | None:
"Ask the user for a file."
assert not dir or not key
if not dir:
@@ -480,7 +469,7 @@ def getSaveFile(
dir_description: str,
key: str,
ext: str,
- fname: Optional[str] = None,
+ fname: str | None = None,
) -> str:
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
variable. The file dialog will default to open with FNAME."""
@@ -520,7 +509,7 @@ def saveGeom(widget: QWidget, key: str) -> None:
def restoreGeom(
- widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
+ widget: QWidget, key: str, offset: int | None = None, adjustSize: bool = False
) -> None:
key += "Geom"
if aqt.mw.pm.profile.get(key):
@@ -562,12 +551,12 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
widget.move(x, y)
-def saveState(widget: Union[QFileDialog, QMainWindow], key: str) -> None:
+def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
key += "State"
aqt.mw.pm.profile[key] = widget.saveState()
-def restoreState(widget: Union[QFileDialog, QMainWindow], key: str) -> None:
+def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None:
key += "State"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
@@ -614,7 +603,7 @@ def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
def restore_combo_index_for_session(
- widget: QComboBox, history: List[str], key: str
+ widget: QComboBox, history: list[str], key: str
) -> None:
textKey = f"{key}ComboActiveText"
indexKey = f"{key}ComboActiveIndex"
@@ -625,7 +614,7 @@ def restore_combo_index_for_session(
widget.setCurrentIndex(index)
-def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
+def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str:
name += "BoxHistory"
text_input = comboBox.lineEdit().text()
if text_input in history:
@@ -639,7 +628,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> st
return text_input
-def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
+def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]:
name += "BoxHistory"
history = aqt.mw.pm.profile.get(name, [])
comboBox.addItems([""] + history)
@@ -693,7 +682,7 @@ def downArrow() -> str:
return "▾"
-def current_window() -> Optional[QWidget]:
+def current_window() -> QWidget | None:
if widget := QApplication.focusWidget():
return widget.window()
else:
@@ -703,14 +692,14 @@ def current_window() -> Optional[QWidget]:
# Tooltips
######################################################################
-_tooltipTimer: Optional[QTimer] = None
-_tooltipLabel: Optional[QLabel] = None
+_tooltipTimer: QTimer | None = None
+_tooltipLabel: QLabel | None = None
def tooltip(
msg: str,
period: int = 3000,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
x_offset: int = 0,
y_offset: int = 100,
) -> None:
@@ -785,7 +774,7 @@ class MenuList:
print(
"MenuList will be removed; please copy it into your add-on's code if you need it."
)
- self.children: List[MenuListChild] = []
+ self.children: list[MenuListChild] = []
def addItem(self, title: str, func: Callable) -> MenuItem:
item = MenuItem(title, func)
@@ -800,7 +789,7 @@ class MenuList:
self.children.append(submenu)
return submenu
- def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
+ def addChild(self, child: SubMenu | QAction | MenuList) -> None:
self.children.append(child)
def renderTo(self, qmenu: QMenu) -> None:
@@ -894,7 +883,7 @@ Add-ons, last update check: {}
######################################################################
# adapted from version detection in qutebrowser
-def opengl_vendor() -> Optional[str]:
+def opengl_vendor() -> str | None:
old_context = QOpenGLContext.currentContext()
old_surface = None if old_context is None else old_context.surface()
diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py
index 5e5f50e7a..49e53cc01 100644
--- a/qt/aqt/webview.py
+++ b/qt/aqt/webview.py
@@ -5,7 +5,7 @@ import dataclasses
import json
import re
import sys
-from typing import Any, Callable, List, Optional, Sequence, Tuple, cast
+from typing import Any, Callable, Optional, Sequence, cast
import anki
from anki.lang import is_rtl
@@ -206,8 +206,8 @@ class WebContent:
body: str = ""
head: str = ""
- css: List[str] = dataclasses.field(default_factory=lambda: [])
- js: List[str] = dataclasses.field(default_factory=lambda: [])
+ css: list[str] = dataclasses.field(default_factory=lambda: [])
+ js: list[str] = dataclasses.field(default_factory=lambda: [])
# Main web view
@@ -232,7 +232,7 @@ class AnkiWebView(QWebEngineView):
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True
- self._pendingActions: List[Tuple[str, Sequence[Any]]] = []
+ self._pendingActions: list[tuple[str, Sequence[Any]]] = []
self.requiresCol = True
self.setPage(self._page)
@@ -415,22 +415,22 @@ border-radius:5px; font-family: Helvetica }"""
font = f'font-size:14px;font-family:"{family}";'
button_style = """
/* Buttons */
-button{
- background-color: %(color_btn)s;
- font-family:"%(family)s"; }
-button:focus{ border-color: %(color_hl)s }
-button:active, button:active:hover { background-color: %(color_hl)s; color: %(color_hl_txt)s;}
+button{{
+ background-color: {color_btn};
+ font-family:"{family}"; }}
+button:focus{{ border-color: {color_hl} }}
+button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}}
/* Input field focus outline */
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
-div[contenteditable="true"]:focus {
+div[contenteditable="true"]:focus {{
outline: 0 none;
- border-color: %(color_hl)s;
-}""" % {
- "family": family,
- "color_btn": color_btn,
- "color_hl": color_hl,
- "color_hl_txt": color_hl_txt,
- }
+ border-color: {color_hl};
+}}""".format(
+ family=family,
+ color_btn=color_btn,
+ color_hl=color_hl,
+ color_hl_txt=color_hl_txt,
+ )
zoom = self.zoomFactor()
@@ -454,8 +454,8 @@ html {{ {font} }}
def stdHtml(
self,
body: str,
- css: Optional[List[str]] = None,
- js: Optional[List[str]] = None,
+ css: Optional[list[str]] = None,
+ js: Optional[list[str]] = None,
head: str = "",
context: Optional[Any] = None,
default_css: bool = True,
diff --git a/qt/aqt/winpaths.py b/qt/aqt/winpaths.py
index ce5530a35..e53a47c06 100644
--- a/qt/aqt/winpaths.py
+++ b/qt/aqt/winpaths.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
System File Locations
Retrieves common system path names on Windows XP/Vista
@@ -15,7 +13,7 @@ __author__ = "Ryan Ginstrom"
__description__ = "Retrieves common Windows system paths as Unicode strings"
-class PathConstants(object):
+class PathConstants:
"""
Define constants here to avoid dependency on shellcon.
Put it in a class to avoid polluting namespace