run pyupgrade over codebase [python upgrade required]

This adds Python 3.9 and 3.10 typing syntax to files that import
attributions from __future___. Python 3.9 should be able to cope with
the 3.10 syntax, but Python 3.8 will no longer work.

On Windows/Mac, install the latest Python 3.9 version from python.org.
There are currently no orjson wheels for Python 3.10 on Windows/Mac,
which will break the build unless you have Rust installed separately.

On Linux, modern distros should have Python 3.9 available already. If
you're on an older distro, you'll need to build Python from source first.
This commit is contained in:
Damien Elmes 2021-10-03 18:59:42 +10:00
parent 8833175bb4
commit b9251290ca
98 changed files with 926 additions and 971 deletions

View file

@ -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

View file

@ -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]
)

View file

@ -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:

View file

@ -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

View file

@ -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():

View file

@ -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(

View file

@ -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]

View file

@ -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(),

View file

@ -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)

View file

@ -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

View file

@ -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(

View file

@ -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),

View file

@ -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"]

View file

@ -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:

View file

@ -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()

View file

@ -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),
(

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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"])

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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:

View file

@ -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,

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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 = "<tr><td align=left style='padding-right: 3px;'>"
txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v)
txt += f"<b>{k}</b></td><td>{v}</td></tr>"
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 "<table width=400>" + "".join(i) + "</table>"
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 "<h1>%s</h1>%s" % (title, subtitle)
return f"<h1>{title}</h1>{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

View file

@ -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 [

View file

@ -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

View file

@ -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)

View file

@ -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?

View file

@ -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

View file

@ -268,7 +268,9 @@ def test_chained_mods():
a1 = "<b>sentence</b>"
q2 = '<span style="color:red">en chaine</span>'
a2 = "<i>chained</i>"
note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
note[
"Text"
] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format(
q1,
a1,
q2,

View file

@ -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"))

View file

@ -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:

View file

@ -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

View file

@ -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")

View file

@ -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 "<br>".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,

View file

@ -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

View file

@ -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:

View file

@ -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 += "</ol>"
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

View file

@ -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:

View file

@ -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(

View file

@ -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),
)

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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.
"""

View file

@ -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)(.+)<hr id=answer>(.+)", src["afmt"])
if not m:
showInfo(tr.card_templates_anki_couldnt_find_the_line_between())

View file

@ -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 = """
<tr><th colspan=5 align=start>%s</th>
<th class=count>%s</th>
<tr><th colspan=5 align=start>{}</th>
<th class=count>{}</th>
<th class=count></th>
<th class=count>%s</th>
<th class=optscol></th></tr>""" % (
<th class=count>{}</th>
<th class=optscol></th></tr>""".format(
tr.decks_deck(),
tr.actions_new(),
tr.statistics_due_count(),

View file

@ -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))

View file

@ -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()

View file

@ -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())

View file

@ -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:

View file

@ -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

View file

@ -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 *

View file

@ -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:

View file

@ -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 = [

View file

@ -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

View file

@ -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)

View file

@ -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 """
<button id="%s" class="%s" onclick="pycmd('%s');return false;"
title="%s" %s>%s</button>""" % (
<button id="{}" class="{}" onclick="pycmd('{}');return false;"
title="{}" {}>{}</button>""".format(
id,
class_,
link,
@ -878,7 +866,7 @@ title="%s" %s>%s</button>""" % (
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</button>""" % (
)
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</button>""" % (
("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</button>""" % (
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</button>""" % (
# 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</button>""" % (
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</button>""" % (
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</button>""" % (
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</button>""" % (
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</button>""" % (
_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

View file

@ -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()

View file

@ -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):

View file

@ -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():

View file

@ -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(

View file

@ -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):

View file

@ -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",

View file

@ -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(

View file

@ -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:

View file

@ -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():

View file

@ -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(

View file

@ -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

View file

@ -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'<div class="descfont descmid description {dyn}">{desc}</div>'
def _table(self) -> Optional[str]:
def _table(self) -> str | None:
counts = list(self.mw.col.sched.counts())
but = self.mw.button
return """
<table width=400 cellpadding=5>
<tr><td align=center valign=top>
<table cellspacing=5>
<tr><td>%s:</td><td><b><span class=new-count>%s</span></b></td></tr>
<tr><td>%s:</td><td><b><span class=learn-count>%s</span></b></td></tr>
<tr><td>%s:</td><td><b><span class=review-count>%s</span></b></td></tr>
<tr><td>{}:</td><td><b><span class=new-count>{}</span></b></td></tr>
<tr><td>{}:</td><td><b><span class=learn-count>{}</span></b></td></tr>
<tr><td>{}:</td><td><b><span class=review-count>{}</span></b></td></tr>
</table>
</td><td align=center>
%s</td></tr></table>""" % (
{}</td></tr></table>""".format(
tr.actions_new(),
counts[0],
tr.scheduling_learning(),

View file

@ -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

View file

@ -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():

View file

@ -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 = """
<span style="font-family: '%s'; font-size: %spx">%s</span>""" % (
<span style="font-family: '{}'; font-size: {}px">{}</span>""".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 = """
<span class=stattxt>%s</span><br>
<button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % (
<span class=stattxt>{}</span><br>
<button title="{}" id="ansbut" class="focus" onclick='pycmd("ans");'>{}</button>""".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 += "</tr></table>"
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 "<div class=spacer></div>"
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 = [
[

View file

@ -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("__")]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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."

View file

@ -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",

View file

@ -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)

View file

@ -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()

View file

@ -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"]

View file

@ -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()

View file

@ -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,

View file

@ -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