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 sys
import traceback import traceback
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Sequence, Union
from weakref import ref from weakref import ref
from markdown import markdown from markdown import markdown
@ -59,7 +59,7 @@ class RustBackend(RustBackendGenerated):
def __init__( def __init__(
self, self,
langs: Optional[List[str]] = None, langs: list[str] | None = None,
server: bool = False, server: bool = False,
) -> None: ) -> None:
# pick up global defaults if not provided # pick up global defaults if not provided
@ -74,12 +74,12 @@ class RustBackend(RustBackendGenerated):
def db_query( def db_query(
self, sql: str, args: Sequence[ValueForDB], first_row_only: bool self, sql: str, args: Sequence[ValueForDB], first_row_only: bool
) -> List[DBRow]: ) -> list[DBRow]:
return self._db_command( return self._db_command(
dict(kind="query", sql=sql, args=args, first_row_only=first_row_only) 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)) return self._db_command(dict(kind="executemany", sql=sql, args=args))
def db_begin(self) -> None: def db_begin(self) -> None:
@ -91,7 +91,7 @@ class RustBackend(RustBackendGenerated):
def db_rollback(self) -> None: def db_rollback(self) -> None:
return self._db_command(dict(kind="rollback")) 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) bytes_input = to_json_bytes(input)
try: try:
return from_json_bytes(self._backend.db_command(bytes_input)) return from_json_bytes(self._backend.db_command(bytes_input))
@ -102,7 +102,7 @@ class RustBackend(RustBackendGenerated):
raise backend_exception_to_pylib(err) raise backend_exception_to_pylib(err)
def translate( 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: ) -> str:
return self.translate_string( return self.translate_string(
translate_string_in( translate_string_in(
@ -133,7 +133,7 @@ class RustBackend(RustBackendGenerated):
def translate_string_in( 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: ) -> i18n_pb2.TranslateStringRequest:
args = {} args = {}
for (k, v) in kwargs.items(): for (k, v) in kwargs.items():
@ -147,10 +147,10 @@ def translate_string_in(
class Translations(GeneratedTranslations): class Translations(GeneratedTranslations):
def __init__(self, backend: Optional[ref[RustBackend]]): def __init__(self, backend: ref[RustBackend] | None):
self.backend = backend 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" "Mimic the old col.tr / TR interface"
if "pytest" not in sys.modules: if "pytest" not in sys.modules:
traceback.print_stack(file=sys.stdout) traceback.print_stack(file=sys.stdout)
@ -162,7 +162,7 @@ class Translations(GeneratedTranslations):
) )
def _translate( 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: ) -> str:
return self.backend().translate( return self.backend().translate(
module_index=module, message_index=message, **args module_index=module, message_index=message, **args

View file

@ -50,7 +50,7 @@ def methods() -> str:
return "\n".join(out) + "\n" return "\n".join(out) + "\n"
def get_arg_types(args: List[Variable]) -> str: def get_arg_types(args: list[Variable]) -> str:
return ", ".join( return ", ".join(
[f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args] [f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args]
@ -68,7 +68,7 @@ def arg_kind(arg: Variable) -> str:
return "str" return "str"
def get_args(args: List[Variable]) -> str: def get_args(args: list[Variable]) -> str:
return ", ".join( return ", ".join(
[f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args] [f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args]
) )

View file

@ -7,11 +7,11 @@ import functools
import os import os
import pathlib import pathlib
import traceback 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 import stringcase
VariableTarget = Tuple[Any, str] VariableTarget = tuple[Any, str]
DeprecatedAliasTarget = Union[Callable, VariableTarget] DeprecatedAliasTarget = Union[Callable, VariableTarget]
@ -43,7 +43,7 @@ class DeprecatedNamesMixin:
# the @no_type_check lines are required to prevent mypy allowing arbitrary # the @no_type_check lines are required to prevent mypy allowing arbitrary
# attributes on the consuming class # attributes on the consuming class
_deprecated_aliases: Dict[str, str] = {} _deprecated_aliases: dict[str, str] = {}
@no_type_check @no_type_check
def __getattr__(self, name: str) -> Any: 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()} 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`.""" """Print a deprecation warning, telling users to use `replaced_by`, or show `doc`."""
def decorator(func: Callable) -> Callable: def decorator(func: Callable) -> Callable:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

View file

@ -3,7 +3,6 @@
import os import os
import sys import sys
from typing import Dict
def _build_info_path() -> str: def _build_info_path() -> str:
@ -19,7 +18,7 @@ def _build_info_path() -> str:
raise Exception("missing buildinfo.txt") raise Exception("missing buildinfo.txt")
def _get_build_info() -> Dict[str, str]: def _get_build_info() -> dict[str, str]:
info = {} info = {}
with open(_build_info_path(), encoding="utf8") as file: with open(_build_info_path(), encoding="utf8") as file:
for line in file.readlines(): for line in file.readlines():

View file

@ -7,7 +7,6 @@ from __future__ import annotations
import pprint import pprint
import time import time
from typing import List, NewType, Optional
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki import cards_pb2, hooks from anki import cards_pb2, hooks
@ -34,7 +33,7 @@ BackendCard = cards_pb2.Card
class Card(DeprecatedNamesMixin): class Card(DeprecatedNamesMixin):
_note: Optional[Note] _note: Note | None
lastIvl: int lastIvl: int
ord: int ord: int
nid: anki.notes.NoteId nid: anki.notes.NoteId
@ -47,12 +46,12 @@ class Card(DeprecatedNamesMixin):
def __init__( def __init__(
self, self,
col: anki.collection.Collection, col: anki.collection.Collection,
id: Optional[CardId] = None, id: CardId | None = None,
backend_card: Optional[BackendCard] = None, backend_card: BackendCard | None = None,
) -> None: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.timer_started: Optional[float] = None self.timer_started: float | None = None
self._render_output: Optional[anki.template.TemplateRenderOutput] = None self._render_output: anki.template.TemplateRenderOutput | None = None
if id: if id:
# existing card # existing card
self.id = id self.id = id
@ -126,10 +125,10 @@ class Card(DeprecatedNamesMixin):
def answer(self) -> str: def answer(self) -> str:
return self.render_output().answer_and_style() 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 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 return self.render_output().answer_av_tags
def render_output( def render_output(

View file

@ -5,7 +5,7 @@
from __future__ import annotations 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 ( from anki import (
card_rendering_pb2, card_rendering_pb2,
@ -92,17 +92,17 @@ LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo]
class Collection(DeprecatedNamesMixin): class Collection(DeprecatedNamesMixin):
sched: Union[V1Scheduler, V2Scheduler, V3Scheduler] sched: V1Scheduler | V2Scheduler | V3Scheduler
def __init__( def __init__(
self, self,
path: str, path: str,
backend: Optional[RustBackend] = None, backend: RustBackend | None = None,
server: bool = False, server: bool = False,
log: bool = False, log: bool = False,
) -> None: ) -> None:
self._backend = backend or RustBackend(server=server) self._backend = backend or RustBackend(server=server)
self.db: Optional[DBProxy] = None self.db: DBProxy | None = None
self._should_log = log self._should_log = log
self.server = server self.server = server
self.path = os.path.abspath(path) self.path = os.path.abspath(path)
@ -211,7 +211,7 @@ class Collection(DeprecatedNamesMixin):
# to check if the backend updated the modification time. # to check if the backend updated the modification time.
return self.db.last_begin_at != self.mod 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." "Flush, commit DB, and take out another write lock if trx=True."
# commit needed? # commit needed?
if self.db.modified_in_python or self.modified_by_backend(): 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) hooks.notes_will_be_deleted(self, note_ids)
return self._backend.remove_notes(note_ids=note_ids, card_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(): if hooks.notes_will_be_deleted.count():
nids = self.db.list( nids = self.db.list(
f"select nid from cards where id in {ids2str(card_ids)}" 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)] return [CardId(id) for id in self._backend.cards_of_note(note_id)]
def defaults_for_adding( def defaults_for_adding(
self, *, current_review_card: Optional[Card] self, *, current_review_card: Card | None
) -> anki.notes.DefaultsForAdding: ) -> anki.notes.DefaultsForAdding:
"""Get starting deck and notetype for add screen. """Get starting deck and notetype for add screen.
An option in the preferences controls whether this will be based on the current deck 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, 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, """If 'change deck depending on notetype' is enabled in the preferences,
return the last deck used with the provided notetype, if any..""" return the last deck used with the provided notetype, if any.."""
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
@ -447,7 +447,7 @@ class Collection(DeprecatedNamesMixin):
########################################################################## ##########################################################################
def after_note_updates( 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: ) -> None:
"If notes modified directly in database, call this afterwards." "If notes modified directly in database, call this afterwards."
self._backend.after_note_updates( self._backend.after_note_updates(
@ -460,7 +460,7 @@ class Collection(DeprecatedNamesMixin):
def find_cards( def find_cards(
self, self,
query: str, query: str,
order: Union[bool, str, BrowserColumns.Column] = False, order: bool | str | BrowserColumns.Column = False,
reverse: bool = False, reverse: bool = False,
) -> Sequence[CardId]: ) -> Sequence[CardId]:
"""Return card ids matching the provided search. """Return card ids matching the provided search.
@ -491,7 +491,7 @@ class Collection(DeprecatedNamesMixin):
def find_notes( def find_notes(
self, self,
query: str, query: str,
order: Union[bool, str, BrowserColumns.Column] = False, order: bool | str | BrowserColumns.Column = False,
reverse: bool = False, reverse: bool = False,
) -> Sequence[NoteId]: ) -> Sequence[NoteId]:
"""Return note ids matching the provided search. """Return note ids matching the provided search.
@ -506,7 +506,7 @@ class Collection(DeprecatedNamesMixin):
def _build_sort_mode( def _build_sort_mode(
self, self,
order: Union[bool, str, BrowserColumns.Column], order: bool | str | BrowserColumns.Column,
reverse: bool, reverse: bool,
finding_notes: bool, finding_notes: bool,
) -> search_pb2.SortOrder: ) -> search_pb2.SortOrder:
@ -539,7 +539,7 @@ class Collection(DeprecatedNamesMixin):
search: str, search: str,
replacement: str, replacement: str,
regex: bool = False, regex: bool = False,
field_name: Optional[str] = None, field_name: str | None = None,
match_case: bool = False, match_case: bool = False,
) -> OpChangesWithCount: ) -> OpChangesWithCount:
"Find and replace fields in a note. Returns changed note count." "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) return self._backend.field_names_for_notes(nids)
# returns array of ("dupestr", [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( nids = self.find_notes(
self.build_search_string(search, SearchNode(field_name=field_name)) self.build_search_string(search, SearchNode(field_name=field_name))
) )
# go through notes # go through notes
vals: Dict[str, List[int]] = {} vals: dict[str, list[int]] = {}
dupes = [] dupes = []
fields: Dict[int, int] = {} fields: dict[int, int] = {}
def ord_for_mid(mid: NotetypeId) -> int: def ord_for_mid(mid: NotetypeId) -> int:
if mid not in fields: if mid not in fields:
@ -596,7 +596,7 @@ class Collection(DeprecatedNamesMixin):
def build_search_string( def build_search_string(
self, self,
*nodes: Union[str, SearchNode], *nodes: str | SearchNode,
joiner: SearchJoiner = "AND", joiner: SearchJoiner = "AND",
) -> str: ) -> str:
"""Join one or more searches, and return a normalized search string. """Join one or more searches, and return a normalized search string.
@ -612,7 +612,7 @@ class Collection(DeprecatedNamesMixin):
def group_searches( def group_searches(
self, self,
*nodes: Union[str, SearchNode], *nodes: str | SearchNode,
joiner: SearchJoiner = "AND", joiner: SearchJoiner = "AND",
) -> SearchNode: ) -> SearchNode:
"""Join provided search nodes and strings into a single 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]: def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
return self._backend.all_browser_columns() 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(): for column in self._backend.all_browser_columns():
if column.key == key: if column.key == key:
return column return column
@ -688,7 +688,7 @@ class Collection(DeprecatedNamesMixin):
def browser_row_for_id( def browser_row_for_id(
self, id_: int 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_) row = self._backend.browser_row_for_id(id_)
return ( return (
((cell.text, cell.is_rtl) for cell in row.cells), ((cell.text, cell.is_rtl) for cell in row.cells),
@ -697,7 +697,7 @@ class Collection(DeprecatedNamesMixin):
row.font_size, 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.""" """Return the stored card column names and ensure the backend columns are set and in sync."""
columns = self.get_config( columns = self.get_config(
BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS
@ -705,11 +705,11 @@ class Collection(DeprecatedNamesMixin):
self._backend.set_active_browser_columns(columns) self._backend.set_active_browser_columns(columns)
return 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.set_config(BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, columns)
self._backend.set_active_browser_columns(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.""" """Return the stored note column names and ensure the backend columns are set and in sync."""
columns = self.get_config( columns = self.get_config(
BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS
@ -717,7 +717,7 @@ class Collection(DeprecatedNamesMixin):
self._backend.set_active_browser_columns(columns) self._backend.set_active_browser_columns(columns)
return 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.set_config(BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, columns)
self._backend.set_active_browser_columns(columns) self._backend.set_active_browser_columns(columns)
@ -745,7 +745,7 @@ class Collection(DeprecatedNamesMixin):
def remove_config(self, key: str) -> OpChanges: def remove_config(self, key: str) -> OpChanges:
return self.conf.remove(key) 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." "This is a debugging aid. Prefer .get_config() when you know the key you need."
return from_json_bytes(self._backend.get_all_config()) return from_json_bytes(self._backend.get_all_config())
@ -802,7 +802,7 @@ class Collection(DeprecatedNamesMixin):
# Stats # Stats
########################################################################## ##########################################################################
def stats(self) -> "anki.stats.CollectionStats": def stats(self) -> anki.stats.CollectionStats:
from anki.stats import CollectionStats from anki.stats import CollectionStats
return CollectionStats(self) return CollectionStats(self)
@ -926,7 +926,7 @@ table.review-log {{ {revlog_style} }}
return True return True
return False 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. """Return undo status if undo available on backend.
If backend has undo available, clear the Python undo state.""" If backend has undo available, clear the Python undo state."""
status = self._backend.get_undo_status() status = self._backend.get_undo_status()
@ -956,7 +956,7 @@ table.review-log {{ {revlog_style} }}
self.clear_python_undo() self.clear_python_undo()
return 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." "Call via .save(). If name not provided, clear any existing checkpoint."
self._last_checkpoint_at = time.time() self._last_checkpoint_at = time.time()
if name: if name:
@ -1017,7 +1017,7 @@ table.review-log {{ {revlog_style} }}
# DB maintenance # DB maintenance
########################################################################## ##########################################################################
def fix_integrity(self) -> Tuple[str, bool]: def fix_integrity(self) -> tuple[str, bool]:
"""Fix possible problems and rebuild caches. """Fix possible problems and rebuild caches.
Returns tuple of (error: str, ok: bool). 'ok' will be true if no 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._startTime = time.time()
self._startReps = self.sched.reps 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." "Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf["timeLim"]: if not self.conf["timeLim"]:
# timeboxing disabled # timeboxing disabled
@ -1126,7 +1126,7 @@ table.review-log {{ {revlog_style} }}
print(args, kwargs) print(args, kwargs)
@deprecated(replaced_by=undo_status) @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." "Undo menu item name, or None if undo unavailable."
status = self.undo_status() status = self.undo_status()
return status.undo or None return status.undo or None
@ -1146,7 +1146,7 @@ table.review-log {{ {revlog_style} }}
self.remove_notes(ids) self.remove_notes(ids)
@deprecated(replaced_by=remove_notes) @deprecated(replaced_by=remove_notes)
def _remNotes(self, ids: List[NoteId]) -> None: def _remNotes(self, ids: list[NoteId]) -> None:
pass pass
@deprecated(replaced_by=card_stats) @deprecated(replaced_by=card_stats)
@ -1154,21 +1154,21 @@ table.review-log {{ {revlog_style} }}
return self.card_stats(card.id, include_revlog=False) return self.card_stats(card.id, include_revlog=False)
@deprecated(replaced_by=after_note_updates) @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) self.after_note_updates(nids, mark_modified=False, generate_cards=False)
@deprecated(replaced_by=after_note_updates) @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) self.after_note_updates(nids, mark_modified=False, generate_cards=True)
# previously returned empty cards, no longer does # previously returned empty cards, no longer does
return [] return []
@deprecated(info="no longer used") @deprecated(info="no longer used")
def emptyCids(self) -> List[CardId]: def emptyCids(self) -> list[CardId]:
return [] return []
@deprecated(info="handled by backend") @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( self.db.executemany(
"insert into graves values (%d, ?, %d)" % (self.usn(), type), "insert into graves values (%d, ?, %d)" % (self.usn(), type),
([x] for x in ids), ([x] for x in ids),
@ -1197,7 +1197,7 @@ _Collection = Collection
@dataclass @dataclass
class _ReviewsUndo: class _ReviewsUndo:
entries: List[LegacyReviewUndo] = field(default_factory=list) entries: list[LegacyReviewUndo] = field(default_factory=list)
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None] _UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
import sys 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 # whether new cards should be mixed with reviews, or shown first or last
NEW_CARDS_DISTRIBUTE = 0 NEW_CARDS_DISTRIBUTE = 0
@ -94,7 +94,7 @@ REVLOG_RESCHED = 4
import anki.collection import anki.collection
def _tr(col: Optional[anki.collection.Collection]) -> Any: def _tr(col: anki.collection.Collection | None) -> Any:
if col: if col:
return col.tr return col.tr
else: else:
@ -107,7 +107,7 @@ def _tr(col: Optional[anki.collection.Collection]) -> Any:
return tr_legacyglobal 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) tr = _tr(col)
return { return {
0: tr.scheduling_show_new_cards_in_random_order(), 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( def newCardSchedulingLabels(
col: Optional[anki.collection.Collection], col: anki.collection.Collection | None,
) -> Dict[int, Any]: ) -> dict[int, Any]:
tr = _tr(col) tr = _tr(col)
return { return {
0: tr.scheduling_mix_new_cards_and_reviews(), 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. of add-ons rely on it.
""" """
from __future__ import annotations
import os import os
import pprint import pprint
import time import time
from sqlite3 import Cursor from sqlite3 import Cursor
from sqlite3 import dbapi2 as sqlite from sqlite3 import dbapi2 as sqlite
from typing import Any, List, Type from typing import Any
DBError = sqlite.Error DBError = sqlite.Error
@ -82,7 +84,7 @@ class DB:
return res[0] return res[0]
return None return None
def all(self, *a: Any, **kw: Any) -> List: def all(self, *a: Any, **kw: Any) -> list:
return self.execute(*a, **kw).fetchall() return self.execute(*a, **kw).fetchall()
def first(self, *a: Any, **kw: Any) -> Any: def first(self, *a: Any, **kw: Any) -> Any:
@ -91,7 +93,7 @@ class DB:
c.close() c.close()
return res 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)] return [x[0] for x in self.execute(*a, **kw)]
def close(self) -> None: def close(self) -> None:
@ -124,5 +126,5 @@ class DB:
def _textFactory(self, data: bytes) -> str: def _textFactory(self, data: bytes) -> str:
return str(data, errors="ignore") 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) return self._db.cursor(factory)

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import re import re
from re import Match 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 import anki._backend
@ -49,7 +49,7 @@ class DBProxy:
*args: ValueForDB, *args: ValueForDB,
first_row_only: bool = False, first_row_only: bool = False,
**kwargs: ValueForDB, **kwargs: ValueForDB,
) -> List[Row]: ) -> list[Row]:
# mark modified? # mark modified?
s = sql.strip().lower() s = sql.strip().lower()
for stmt in "insert", "update", "delete": for stmt in "insert", "update", "delete":
@ -62,15 +62,15 @@ class DBProxy:
# Query shortcuts # 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) return self._query(sql, *args, first_row_only=False, **kwargs)
def list( def list(
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB 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)] 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) rows = self._query(sql, *args, first_row_only=True, **kwargs)
if rows: if rows:
return rows[0] return rows[0]
@ -102,8 +102,8 @@ class DBProxy:
# convert kwargs to list format # convert kwargs to list format
def emulate_named_args( def emulate_named_args(
sql: str, args: Tuple, kwargs: Dict[str, Any] sql: str, args: tuple, kwargs: dict[str, Any]
) -> Tuple[str, Sequence[ValueForDB]]: ) -> tuple[str, Sequence[ValueForDB]]:
# nothing to do? # nothing to do?
if not kwargs: if not kwargs:
return sql, args return sql, args

View file

@ -6,19 +6,7 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import ( from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence, no_type_check
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
NewType,
Optional,
Sequence,
Tuple,
Union,
no_type_check,
)
if TYPE_CHECKING: if TYPE_CHECKING:
import anki import anki
@ -40,8 +28,8 @@ DeckConfigsForUpdate = deckconfig_pb2.DeckConfigsForUpdate
UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest
# type aliases until we can move away from dicts # type aliases until we can move away from dicts
DeckDict = Dict[str, Any] DeckDict = dict[str, Any]
DeckConfigDict = Dict[str, Any] DeckConfigDict = dict[str, Any]
DeckId = NewType("DeckId", int) DeckId = NewType("DeckId", int)
DeckConfigId = NewType("DeckConfigId", int) DeckConfigId = NewType("DeckConfigId", int)
@ -96,7 +84,7 @@ class DeckManager(DeprecatedNamesMixin):
self.col = col.weakref() self.col = col.weakref()
self.decks = DecksDictProxy(col) 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." "Can be called with either a deck or a deck configuration."
if not deck_or_config: if not deck_or_config:
print("col.decks.save() should be passed the changed deck") print("col.decks.save() should be passed the changed deck")
@ -131,7 +119,7 @@ class DeckManager(DeprecatedNamesMixin):
name: str, name: str,
create: bool = True, create: bool = True,
type: DeckConfigId = DeckConfigId(0), type: DeckConfigId = DeckConfigId(0),
) -> Optional[DeckId]: ) -> DeckId | None:
"Add a deck with NAME. Reuse deck if already exists. Return id as int." "Add a deck with NAME. Reuse deck if already exists. Return id as int."
id = self.id_for_name(name) id = self.id_for_name(name)
if id: if id:
@ -155,13 +143,13 @@ class DeckManager(DeprecatedNamesMixin):
skip_empty_default=skip_empty_default, include_filtered=include_filtered 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: try:
return DeckId(self.col._backend.get_deck_id_by_name(name)) return DeckId(self.col._backend.get_deck_id_by_name(name))
except NotFoundError: except NotFoundError:
return None return None
def get_legacy(self, did: DeckId) -> Optional[DeckDict]: def get_legacy(self, did: DeckId) -> DeckDict | None:
try: try:
return from_json_bytes(self.col._backend.get_deck_legacy(did)) return from_json_bytes(self.col._backend.get_deck_legacy(did))
except NotFoundError: except NotFoundError:
@ -170,7 +158,7 @@ class DeckManager(DeprecatedNamesMixin):
def have(self, id: DeckId) -> bool: def have(self, id: DeckId) -> bool:
return bool(self.get_legacy(id)) 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()) return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values())
def new_deck_legacy(self, filtered: bool) -> DeckDict: def new_deck_legacy(self, filtered: bool) -> DeckDict:
@ -191,7 +179,7 @@ class DeckManager(DeprecatedNamesMixin):
@classmethod @classmethod
def find_deck_in_tree( def find_deck_in_tree(
cls, node: DeckTreeNode, deck_id: DeckId cls, node: DeckTreeNode, deck_id: DeckId
) -> Optional[DeckTreeNode]: ) -> DeckTreeNode | None:
if node.deck_id == deck_id: if node.deck_id == deck_id:
return node return node
for child in node.children: for child in node.children:
@ -200,7 +188,7 @@ class DeckManager(DeprecatedNamesMixin):
return match return match
return None return None
def all(self) -> List[DeckDict]: def all(self) -> list[DeckDict]:
"All decks. Expensive; prefer all_names_and_ids()" "All decks. Expensive; prefer all_names_and_ids()"
return self.get_all_legacy() return self.get_all_legacy()
@ -226,7 +214,7 @@ class DeckManager(DeprecatedNamesMixin):
return len(self.all_names_and_ids()) return len(self.all_names_and_ids())
def card_count( def card_count(
self, dids: Union[DeckId, Iterable[DeckId]], include_subdecks: bool self, dids: DeckId | Iterable[DeckId], include_subdecks: bool
) -> Any: ) -> Any:
if isinstance(dids, int): if isinstance(dids, int):
dids = {dids} dids = {dids}
@ -240,7 +228,7 @@ class DeckManager(DeprecatedNamesMixin):
) )
return count 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 not did:
if default: if default:
return self.get_legacy(DEFAULT_DECK_ID) return self.get_legacy(DEFAULT_DECK_ID)
@ -255,7 +243,7 @@ class DeckManager(DeprecatedNamesMixin):
else: else:
return None return None
def by_name(self, name: str) -> Optional[DeckDict]: def by_name(self, name: str) -> DeckDict | None:
"""Get deck with NAME, ignoring case.""" """Get deck with NAME, ignoring case."""
id = self.id_for_name(name) id = self.id_for_name(name)
if id: if id:
@ -271,7 +259,7 @@ class DeckManager(DeprecatedNamesMixin):
def update_dict(self, deck: DeckDict) -> OpChanges: def update_dict(self, deck: DeckDict) -> OpChanges:
return self.col._backend.update_deck_legacy(json=to_json_bytes(deck)) 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." "Rename deck prefix to NAME if not exists. Updates children."
if isinstance(deck, int): if isinstance(deck, int):
deck_id = deck deck_id = deck
@ -300,7 +288,7 @@ class DeckManager(DeprecatedNamesMixin):
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges: def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
return self.col._backend.update_deck_configs(input=input) 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." "A list of all deck config."
return list(from_json_bytes(self.col._backend.all_deck_config_legacy())) return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
@ -318,7 +306,7 @@ class DeckManager(DeprecatedNamesMixin):
# dynamic decks have embedded conf # dynamic decks have embedded conf
return deck return deck
def get_config(self, conf_id: DeckConfigId) -> Optional[DeckConfigDict]: def get_config(self, conf_id: DeckConfigId) -> DeckConfigDict | None:
try: try:
return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id)) return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id))
except NotFoundError: except NotFoundError:
@ -331,7 +319,7 @@ class DeckManager(DeprecatedNamesMixin):
) )
def add_config( def add_config(
self, name: str, clone_from: Optional[DeckConfigDict] = None self, name: str, clone_from: DeckConfigDict | None = None
) -> DeckConfigDict: ) -> DeckConfigDict:
if clone_from is not None: if clone_from is not None:
conf = copy.deepcopy(clone_from) conf = copy.deepcopy(clone_from)
@ -343,7 +331,7 @@ class DeckManager(DeprecatedNamesMixin):
return conf return conf
def add_config_returning_id( def add_config_returning_id(
self, name: str, clone_from: Optional[DeckConfigDict] = None self, name: str, clone_from: DeckConfigDict | None = None
) -> DeckConfigId: ) -> DeckConfigId:
return self.add_config(name, clone_from)["id"] return self.add_config(name, clone_from)["id"]
@ -363,7 +351,7 @@ class DeckManager(DeprecatedNamesMixin):
deck["conf"] = id deck["conf"] = id
self.save(deck) self.save(deck)
def decks_using_config(self, conf: DeckConfigDict) -> List[DeckId]: def decks_using_config(self, conf: DeckConfigDict) -> list[DeckId]:
dids = [] dids = []
for deck in self.all(): for deck in self.all():
if "conf" in deck and deck["conf"] == conf["id"]: if "conf" in deck and deck["conf"] == conf["id"]:
@ -389,13 +377,13 @@ class DeckManager(DeprecatedNamesMixin):
return deck["name"] return deck["name"]
return self.col.tr.decks_no_deck() 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) deck = self.get(did, default=False)
if deck: if deck:
return deck["name"] return deck["name"]
return None 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: if not children:
return self.col.db.list("select id from cards where did=?", did) return self.col.db.list("select id from cards where did=?", did)
dids = [did] dids = [did]
@ -403,7 +391,7 @@ class DeckManager(DeprecatedNamesMixin):
dids.append(id) dids.append(id)
return self.col.db.list(f"select id from cards where did in {ids2str(dids)}") 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)}") return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
# Deck selection # Deck selection
@ -419,7 +407,7 @@ class DeckManager(DeprecatedNamesMixin):
def current(self) -> DeckDict: def current(self) -> DeckDict:
return self.get(self.selected()) 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 # some add-ons assume this will always be non-empty
return self.col.sched.active_decks or [DeckId(1)] return self.col.sched.active_decks or [DeckId(1)]
@ -435,7 +423,7 @@ class DeckManager(DeprecatedNamesMixin):
############################################################# #############################################################
@staticmethod @staticmethod
def path(name: str) -> List[str]: def path(name: str) -> list[str]:
return name.split("::") return name.split("::")
@classmethod @classmethod
@ -443,21 +431,21 @@ class DeckManager(DeprecatedNamesMixin):
return cls.path(name)[-1] return cls.path(name)[-1]
@classmethod @classmethod
def immediate_parent_path(cls, name: str) -> List[str]: def immediate_parent_path(cls, name: str) -> list[str]:
return cls.path(name)[:-1] return cls.path(name)[:-1]
@classmethod @classmethod
def immediate_parent(cls, name: str) -> Optional[str]: def immediate_parent(cls, name: str) -> str | None:
parent_path = cls.immediate_parent_path(name) parent_path = cls.immediate_parent_path(name)
if parent_path: if parent_path:
return "::".join(parent_path) return "::".join(parent_path)
return None return None
@classmethod @classmethod
def key(cls, deck: DeckDict) -> List[str]: def key(cls, deck: DeckDict) -> list[str]:
return cls.path(deck["name"]) 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)." "All children of did, as (name, id)."
name = self.get(did)["name"] name = self.get(did)["name"]
actv = [] actv = []
@ -472,24 +460,24 @@ class DeckManager(DeprecatedNamesMixin):
DeckId(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix) 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) parent_name = self.name(deck_id)
out = [deck_id] out = [deck_id]
out.extend(self.child_ids(parent_name)) out.extend(self.child_ids(parent_name))
return out return out
def parents( def parents(
self, did: DeckId, name_map: Optional[Dict[str, DeckDict]] = None self, did: DeckId, name_map: dict[str, DeckDict] | None = None
) -> List[DeckDict]: ) -> list[DeckDict]:
"All parents of did." "All parents of did."
# get parent and grandparent names # get parent and grandparent names
parents_names: List[str] = [] parents_names: list[str] = []
for part in self.immediate_parent_path(self.get(did)["name"]): for part in self.immediate_parent_path(self.get(did)["name"]):
if not parents_names: if not parents_names:
parents_names.append(part) parents_names.append(part)
else: else:
parents_names.append(f"{parents_names[-1]}::{part}") parents_names.append(f"{parents_names[-1]}::{part}")
parents: List[DeckDict] = [] parents: list[DeckDict] = []
# convert to objects # convert to objects
for parent_name in parents_names: for parent_name in parents_names:
if name_map: if name_map:
@ -499,13 +487,13 @@ class DeckManager(DeprecatedNamesMixin):
parents.append(deck) parents.append(deck)
return parents 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" "All existing parents of name"
if "::" not in name: if "::" not in name:
return [] return []
names = self.immediate_parent_path(name) names = self.immediate_parent_path(name)
head = [] head = []
parents: List[DeckDict] = [] parents: list[DeckDict] = []
while names: while names:
head.append(names.pop(0)) head.append(names.pop(0))
@ -524,7 +512,7 @@ class DeckManager(DeprecatedNamesMixin):
self.select(did) self.select(did)
return 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"]) return bool(self.get(did)["dyn"])
# Legacy # Legacy
@ -546,11 +534,11 @@ class DeckManager(DeprecatedNamesMixin):
self.remove([did]) self.remove([did])
@deprecated(replaced_by=all_names_and_ids) @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()} return {d["name"]: d for d in self.all()}
@deprecated(info="use col.set_deck() instead") @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.set_deck(card_ids=cids, deck_id=did)
self.col.db.execute( self.col.db.execute(
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", 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) @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()] return [str(x.id) for x in self.all_names_and_ids()]
@deprecated(replaced_by=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 [ return [
x.name x.name
for x in self.all_names_and_ids( for x in self.all_names_and_ids(

View file

@ -3,6 +3,8 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
from __future__ import annotations
import json import json
import os import os
import re import re
@ -10,7 +12,7 @@ import shutil
import unicodedata import unicodedata
import zipfile import zipfile
from io import BufferedWriter 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 zipfile import ZipFile
from anki import hooks from anki import hooks
@ -21,7 +23,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML
class Exporter: class Exporter:
includeHTML: Union[bool, None] = None includeHTML: bool | None = None
ext: Optional[str] = None ext: Optional[str] = None
includeTags: Optional[bool] = None includeTags: Optional[bool] = None
includeSched: Optional[bool] = None includeSched: Optional[bool] = None
@ -31,7 +33,7 @@ class Exporter:
self, self,
col: Collection, col: Collection,
did: Optional[DeckId] = None, did: Optional[DeckId] = None,
cids: Optional[List[CardId]] = None, cids: Optional[list[CardId]] = None,
) -> None: ) -> None:
self.col = col.weakref() self.col = col.weakref()
self.did = did self.did = did
@ -177,7 +179,7 @@ where cards.id in %s)"""
class AnkiExporter(Exporter): class AnkiExporter(Exporter):
ext = ".anki2" ext = ".anki2"
includeSched: Union[bool, None] = False includeSched: bool | None = False
includeMedia = True includeMedia = True
def __init__(self, col: Collection) -> None: def __init__(self, col: Collection) -> None:
@ -187,7 +189,7 @@ class AnkiExporter(Exporter):
def key(col: Collection) -> str: def key(col: Collection) -> str:
return col.tr.exporting_anki_20_deck() return col.tr.exporting_anki_20_deck()
def deckIds(self) -> List[DeckId]: def deckIds(self) -> list[DeckId]:
if self.cids: if self.cids:
return self.col.decks.for_card_ids(self.cids) return self.col.decks.for_card_ids(self.cids)
elif self.did: elif self.did:
@ -210,7 +212,7 @@ class AnkiExporter(Exporter):
cids = self.cardIds() cids = self.cardIds()
# copy cards, noting used nids # copy cards, noting used nids
nids = {} nids = {}
data: List[Sequence] = [] data: list[Sequence] = []
for row in self.src.db.execute( for row in self.src.db.execute(
"select * from cards where id in " + ids2str(cids) "select * from cards where id in " + ids2str(cids)
): ):
@ -344,7 +346,7 @@ class AnkiPackageExporter(AnkiExporter):
z.writestr("media", json.dumps(media)) z.writestr("media", json.dumps(media))
z.close() 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 # export into the anki2 file
colfile = path.replace(".apkg", ".anki2") colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile) AnkiExporter.exportInto(self, colfile)
@ -368,7 +370,7 @@ class AnkiPackageExporter(AnkiExporter):
shutil.rmtree(path.replace(".apkg", ".media")) shutil.rmtree(path.replace(".apkg", ".media"))
return 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 = {} media = {}
for c, file in enumerate(files): for c, file in enumerate(files):
cStr = str(c) 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): def id(obj):
if callable(obj.key): if callable(obj.key):
key_str = obj.key(col) key_str = obj.key(col)
else: else:
key_str = obj.key key_str = obj.key
return ("%s (*%s)" % (key_str, obj.ext), obj) return (f"{key_str} (*{obj.ext})", obj)
exps = [ exps = [
id(AnkiCollectionPackageExporter), id(AnkiCollectionPackageExporter),

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Set from typing import TYPE_CHECKING
from anki.hooks import * from anki.hooks import *
from anki.notes import NoteId from anki.notes import NoteId
@ -13,7 +13,7 @@ if TYPE_CHECKING:
class Finder: class Finder:
def __init__(self, col: Optional[Collection]) -> None: def __init__(self, col: Collection | None) -> None:
self.col = col.weakref() self.col = col.weakref()
print("Finder() is deprecated, please use col.find_cards() or .find_notes()") print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
@ -34,7 +34,7 @@ def findReplace(
src: str, src: str,
dst: str, dst: str,
regex: bool = False, regex: bool = False,
field: Optional[str] = None, field: str | None = None,
fold: bool = True, fold: bool = True,
) -> int: ) -> int:
"Find and replace fields in a note. Returns changed note count." "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: def fieldNames(col: Collection, downcase: bool = True) -> List:
fields: Set[str] = set() fields: set[str] = set()
for m in col.models.all(): for m in col.models.all():
for f in m["flds"]: for f in m["flds"]:
name = f["name"].lower() if downcase else f["name"] name = f["name"].lower() if downcase else f["name"]

View file

@ -12,8 +12,6 @@ modifying it.
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Dict, List
import decorator import decorator
# You can find the definitions in ../tools/genhooks.py # You can find the definitions in ../tools/genhooks.py
@ -22,7 +20,7 @@ from anki.hooks_gen import *
# Legacy hook handling # Legacy hook handling
############################################################################## ##############################################################################
_hooks: Dict[str, List[Callable[..., Any]]] = {} _hooks: dict[str, list[Callable[..., Any]]] = {}
def runHook(hook: str, *args: Any) -> None: def runHook(hook: str, *args: Any) -> None:

View file

@ -9,7 +9,7 @@ from __future__ import annotations
import io import io
import os import os
from typing import Any, Callable, Dict, Optional from typing import Any, Callable
import requests import requests
from requests import Response from requests import Response
@ -24,9 +24,9 @@ class HttpClient:
verify = True verify = True
timeout = 60 timeout = 60
# args are (upload_bytes_in_chunk, download_bytes_in_chunk) # 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.progress_hook = progress_hook
self.session = requests.Session() self.session = requests.Session()
@ -44,9 +44,7 @@ class HttpClient:
def __del__(self) -> None: def __del__(self) -> None:
self.close() self.close()
def post( def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response:
self, url: str, data: bytes, headers: Optional[Dict[str, str]]
) -> Response:
headers["User-Agent"] = self._agentName() headers["User-Agent"] = self._agentName()
return self.session.post( return self.session.post(
url, url,
@ -57,7 +55,7 @@ class HttpClient:
verify=self.verify, verify=self.verify,
) # pytype: disable=wrong-arg-types ) # 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: if headers is None:
headers = {} headers = {}
headers["User-Agent"] = self._agentName() headers["User-Agent"] = self._agentName()

View file

@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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.collection import Collection
from anki.importing.anki2 import Anki2Importer from anki.importing.anki2 import Anki2Importer
@ -14,7 +14,7 @@ from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
from anki.lang import TR from anki.lang import TR
def importers(col: Collection) -> Sequence[Tuple[str, Type[Importer]]]: def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
return ( return (
(col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter), (col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter),
( (

View file

@ -5,7 +5,7 @@
import os import os
import unicodedata import unicodedata
from typing import Any, Dict, List, Optional, Tuple from typing import Optional
from anki.cards import CardId from anki.cards import CardId
from anki.collection import Collection from anki.collection import Collection
@ -37,7 +37,7 @@ class Anki2Importer(Importer):
super().__init__(col, file) super().__init__(col, file)
# set later, defined here for typechecking # set later, defined here for typechecking
self._decks: Dict[DeckId, DeckId] = {} self._decks: dict[DeckId, DeckId] = {}
self.source_needs_upgrade = False self.source_needs_upgrade = False
def run(self, media: None = None, importing_v2: bool = True) -> None: def run(self, media: None = None, importing_v2: bool = True) -> None:
@ -80,14 +80,14 @@ class Anki2Importer(Importer):
# Notes # Notes
###################################################################### ######################################################################
def _logNoteRow(self, action: str, noteRow: List[str]) -> None: def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
self.log.append( self.log.append(
"[%s] %s" % (action, stripHTMLMedia(noteRow[6].replace("\x1f", ", "))) "[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
) )
def _importNotes(self) -> None: def _importNotes(self) -> None:
# build guid -> (id,mod,mid) hash & map of existing note ids # 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 = {} existing = {}
for id, guid, mod, mid in self.dst.db.execute( for id, guid, mod, mid in self.dst.db.execute(
"select id, guid, mod, mid from notes" "select id, guid, mod, mid from notes"
@ -96,7 +96,7 @@ class Anki2Importer(Importer):
existing[id] = True existing[id] = True
# we ignore updates to changed schemas. we need to note the ignored # we ignore updates to changed schemas. we need to note the ignored
# guids, so we avoid importing invalid cards # guids, so we avoid importing invalid cards
self._ignoredGuids: Dict[str, bool] = {} self._ignoredGuids: dict[str, bool] = {}
# iterate over source collection # iterate over source collection
add = [] add = []
update = [] update = []
@ -194,7 +194,7 @@ class Anki2Importer(Importer):
# determine if note is a duplicate, and adjust mid and/or guid as required # determine if note is a duplicate, and adjust mid and/or guid as required
# returns true if note should be added # returns true if note should be added
def _uniquifyNote(self, note: List[Any]) -> bool: def _uniquifyNote(self, note: list[Any]) -> bool:
origGuid = note[GUID] origGuid = note[GUID]
srcMid = note[MID] srcMid = note[MID]
dstMid = self._mid(srcMid) dstMid = self._mid(srcMid)
@ -218,7 +218,7 @@ class Anki2Importer(Importer):
def _prepareModels(self) -> None: def _prepareModels(self) -> None:
"Prepare index of schema hashes." "Prepare index of schema hashes."
self._modelMap: Dict[NotetypeId, NotetypeId] = {} self._modelMap: dict[NotetypeId, NotetypeId] = {}
def _mid(self, srcMid: NotetypeId) -> Any: def _mid(self, srcMid: NotetypeId) -> Any:
"Return local id for remote MID." "Return local id for remote MID."
@ -308,7 +308,7 @@ class Anki2Importer(Importer):
if self.source_needs_upgrade: if self.source_needs_upgrade:
self.src.upgrade_to_v2_scheduler() self.src.upgrade_to_v2_scheduler()
# build map of (guid, ord) -> cid and used id cache # 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 = {} existing = {}
for guid, ord, cid in self.dst.db.execute( 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" "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 os
import unicodedata import unicodedata
import zipfile import zipfile
from typing import Any, Dict, Optional from typing import Any, Optional
from anki.importing.anki2 import Anki2Importer from anki.importing.anki2 import Anki2Importer
from anki.utils import tmpfile from anki.utils import tmpfile
class AnkiPackageImporter(Anki2Importer): class AnkiPackageImporter(Anki2Importer):
nameToNum: Dict[str, str] nameToNum: dict[str, str]
zip: Optional[zipfile.ZipFile] zip: Optional[zipfile.ZipFile]
def run(self) -> None: # type: ignore def run(self) -> None: # type: ignore

View file

@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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.collection import Collection
from anki.utils import maxID from anki.utils import maxID
@ -18,7 +18,7 @@ class Importer:
def __init__(self, col: Collection, file: str) -> None: def __init__(self, col: Collection, file: str) -> None:
self.file = file self.file = file
self.log: List[str] = [] self.log: list[str] = []
self.col = col.weakref() self.col = col.weakref()
self.total = 0 self.total = 0
self.dst = None self.dst = None

View file

@ -1,9 +1,11 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import csv import csv
import re import re
from typing import Any, List, Optional, TextIO, Union from typing import Any, Optional, TextIO
from anki.collection import Collection from anki.collection import Collection
from anki.importing.noteimp import ForeignNote, NoteImporter from anki.importing.noteimp import ForeignNote, NoteImporter
@ -19,12 +21,12 @@ class TextImporter(NoteImporter):
self.lines = None self.lines = None
self.fileobj: Optional[TextIO] = None self.fileobj: Optional[TextIO] = None
self.delimiter: Optional[str] = None self.delimiter: Optional[str] = None
self.tagsToAdd: List[str] = [] self.tagsToAdd: list[str] = []
self.numFields = 0 self.numFields = 0
self.dialect: Optional[Any] 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() self.open()
# process all lines # process all lines
log = [] log = []
@ -144,7 +146,7 @@ class TextImporter(NoteImporter):
# pylint: disable=no-member # pylint: disable=no-member
zuper.__del__(self) # type: ignore zuper.__del__(self) # type: ignore
def noteFromFields(self, fields: List[str]) -> ForeignNote: def noteFromFields(self, fields: list[str]) -> ForeignNote:
note = ForeignNote() note = ForeignNote()
note.fields.extend([x for x in fields]) note.fields.extend([x for x in fields])
note.tags.extend(self.tagsToAdd) note.tags.extend(self.tagsToAdd)

View file

@ -3,9 +3,11 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
from __future__ import annotations
import html import html
import unicodedata import unicodedata
from typing import Dict, List, Optional, Tuple, Union from typing import Optional, Union
from anki.collection import Collection from anki.collection import Collection
from anki.config import Config from anki.config import Config
@ -22,9 +24,9 @@ from anki.utils import (
timestampID, timestampID,
) )
TagMappedUpdate = Tuple[int, int, str, str, NoteId, str, str] TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
TagModifiedUpdate = Tuple[int, int, str, str, NoteId, str] TagModifiedUpdate = tuple[int, int, str, str, NoteId, str]
NoTagUpdate = Tuple[int, int, str, NoteId, str] NoTagUpdate = tuple[int, int, str, NoteId, str]
Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate] Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate]
# Stores a list of fields, tags and deck # Stores a list of fields, tags and deck
@ -35,10 +37,10 @@ class ForeignNote:
"An temporary object storing fields and attributes." "An temporary object storing fields and attributes."
def __init__(self) -> None: def __init__(self) -> None:
self.fields: List[str] = [] self.fields: list[str] = []
self.tags: List[str] = [] self.tags: list[str] = []
self.deck = None self.deck = None
self.cards: Dict[int, ForeignCard] = {} # map of ord -> card self.cards: dict[int, ForeignCard] = {} # map of ord -> card
self.fieldsStr = "" self.fieldsStr = ""
@ -75,7 +77,7 @@ class NoteImporter(Importer):
needDelimiter = False needDelimiter = False
allowHTML = False allowHTML = False
importMode = UPDATE_MODE importMode = UPDATE_MODE
mapping: Optional[List[str]] mapping: Optional[list[str]]
tagModified: Optional[str] tagModified: Optional[str]
def __init__(self, col: Collection, file: str) -> None: def __init__(self, col: Collection, file: str) -> None:
@ -109,11 +111,11 @@ class NoteImporter(Importer):
def mappingOk(self) -> bool: def mappingOk(self) -> bool:
return self.model["flds"][0]["name"] in self.mapping 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 a list of foreign notes for importing."
return [] 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." "Convert each card into a note, apply attributes and add to col."
assert self.mappingOk() assert self.mappingOk()
# note whether tags are mapped # note whether tags are mapped
@ -122,7 +124,7 @@ class NoteImporter(Importer):
if f == "_tags": if f == "_tags":
self._tagsMapped = True self._tagsMapped = True
# gather checks for duplicate comparison # gather checks for duplicate comparison
csums: Dict[str, List[NoteId]] = {} csums: dict[str, list[NoteId]] = {}
for csum, id in self.col.db.execute( for csum, id in self.col.db.execute(
"select csum, id from notes where mid = ?", self.model["id"] "select csum, id from notes where mid = ?", self.model["id"]
): ):
@ -130,18 +132,18 @@ class NoteImporter(Importer):
csums[csum].append(id) csums[csum].append(id)
else: else:
csums[csum] = [id] csums[csum] = [id]
firsts: Dict[str, bool] = {} firsts: dict[str, bool] = {}
fld0idx = self.mapping.index(self.model["flds"][0]["name"]) fld0idx = self.mapping.index(self.model["flds"][0]["name"])
self._fmap = self.col.models.field_map(self.model) self._fmap = self.col.models.field_map(self.model)
self._nextID = NoteId(timestampID(self.col.db, "notes")) self._nextID = NoteId(timestampID(self.col.db, "notes"))
# loop through the notes # loop through the notes
updates: List[Updates] = [] updates: list[Updates] = []
updateLog = [] updateLog = []
new = [] new = []
self._ids: List[NoteId] = [] self._ids: list[NoteId] = []
self._cards: List[Tuple] = [] self._cards: list[tuple] = []
dupeCount = 0 dupeCount = 0
dupes: List[str] = [] dupes: list[str] = []
for n in notes: for n in notes:
for c, field in enumerate(n.fields): for c, field in enumerate(n.fields):
if not self.allowHTML: if not self.allowHTML:
@ -232,7 +234,7 @@ class NoteImporter(Importer):
def newData( def newData(
self, n: ForeignNote 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 id = self._nextID
self._nextID = NoteId(self._nextID + 1) self._nextID = NoteId(self._nextID + 1)
self._ids.append(id) self._ids.append(id)
@ -256,8 +258,8 @@ class NoteImporter(Importer):
def addNew( def addNew(
self, self,
rows: List[ rows: list[
Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str] tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]
], ],
) -> None: ) -> None:
self.col.db.executemany( self.col.db.executemany(
@ -265,7 +267,7 @@ class NoteImporter(Importer):
) )
def updateData( def updateData(
self, n: ForeignNote, id: NoteId, sflds: List[str] self, n: ForeignNote, id: NoteId, sflds: list[str]
) -> Optional[Updates]: ) -> Optional[Updates]:
self._ids.append(id) self._ids.append(id)
self.processFields(n, sflds) self.processFields(n, sflds)
@ -280,7 +282,7 @@ class NoteImporter(Importer):
else: else:
return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr) 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()") changes = self.col.db.scalar("select total_changes()")
if self._tagsMapped: if self._tagsMapped:
self.col.db.executemany( self.col.db.executemany(
@ -307,7 +309,7 @@ where id = ? and flds != ?""",
self.updateCount = changes2 - changes self.updateCount = changes2 - changes
def processFields( def processFields(
self, note: ForeignNote, fields: Optional[List[str]] = None self, note: ForeignNote, fields: Optional[list[str]] = None
) -> None: ) -> None:
if not fields: if not fields:
fields = [""] * len(self.model["flds"]) fields = [""] * len(self.model["flds"])

View file

@ -9,7 +9,7 @@ import sys
import time import time
import unicodedata import unicodedata
from string import capwords from string import capwords
from typing import List, Optional, Union from typing import Optional, Union
from xml.dom import minidom from xml.dom import minidom
from xml.dom.minidom import Element, Text from xml.dom.minidom import Element, Text
@ -185,7 +185,7 @@ class SupermemoXmlImporter(NoteImporter):
## DEFAULT IMPORTER METHODS ## DEFAULT IMPORTER METHODS
def foreignNotes(self) -> List[ForeignNote]: def foreignNotes(self) -> list[ForeignNote]:
# Load file and parse it by minidom # Load file and parse it by minidom
self.loadSource(self.file) self.loadSource(self.file)
@ -415,7 +415,7 @@ class SupermemoXmlImporter(NoteImporter):
self.logger("-" * 45, level=3) self.logger("-" * 45, level=3)
for key in list(smel.keys()): for key in list(smel.keys()):
self.logger( self.logger(
"\t%s %s" % ((key + ":").ljust(15), smel[key]), level=3 "\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
) )
else: else:
self.logger("Element skiped \t- no valid Q and A ...", level=3) 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 locale
import re import re
import weakref import weakref
from typing import Optional, Tuple
import anki import anki
import anki._backend import anki._backend
@ -153,7 +152,7 @@ currentLang = "en"
# not reference this, and should use col.tr instead. The global # not reference this, and should use col.tr instead. The global
# instance exists for legacy reasons, and as a convenience for the # instance exists for legacy reasons, and as a convenience for the
# Qt code. # Qt code.
current_i18n: Optional[anki._backend.RustBackend] = None current_i18n: anki._backend.RustBackend | None = None
tr_legacyglobal = anki._backend.Translations(None) tr_legacyglobal = anki._backend.Translations(None)
@ -174,7 +173,7 @@ def set_lang(lang: str) -> None:
tr_legacyglobal.backend = weakref.ref(current_i18n) 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 """Return lang converted to name used on disk and its index, defaulting to system language
or English if not available.""" or English if not available."""
try: try:

View file

@ -7,7 +7,7 @@ import html
import os import os
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, List, Optional, Tuple from typing import Any
import anki import anki
from anki import card_rendering_pb2, hooks from anki import card_rendering_pb2, hooks
@ -41,7 +41,7 @@ class ExtractedLatex:
@dataclass @dataclass
class ExtractedLatexOutput: class ExtractedLatexOutput:
html: str html: str
latex: List[ExtractedLatex] latex: list[ExtractedLatex]
@staticmethod @staticmethod
def from_proto( def from_proto(
@ -80,7 +80,7 @@ def render_latex_returning_errors(
model: NotetypeDict, model: NotetypeDict,
col: anki.collection.Collection, col: anki.collection.Collection,
expand_clozes: bool = False, expand_clozes: bool = False,
) -> Tuple[str, List[str]]: ) -> tuple[str, list[str]]:
"""Returns (text, errors). """Returns (text, errors).
errors will be non-empty if LaTeX failed to render.""" errors will be non-empty if LaTeX failed to render."""
@ -111,7 +111,7 @@ def _save_latex_image(
header: str, header: str,
footer: str, footer: str,
svg: bool, svg: bool,
) -> Optional[str]: ) -> str | None:
# add header/footer # add header/footer
latex = f"{header}\n{extracted.latex_body}\n{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 # 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 re
import sys import sys
import time import time
from typing import Any, Callable, List, Optional, Tuple from typing import Any, Callable
from anki import media_pb2 from anki import media_pb2
from anki.consts import * from anki.consts import *
@ -19,7 +19,7 @@ from anki.template import av_tags_to_native
from anki.utils import intTime 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_folder = re.sub(r"(?i)\.(anki2)$", ".media", col_path)
media_db = f"{media_folder}.db2" media_db = f"{media_folder}.db2"
return (media_folder, media_db) return (media_folder, media_db)
@ -50,7 +50,7 @@ class MediaManager:
def __init__(self, col: anki.collection.Collection, server: bool) -> None: def __init__(self, col: anki.collection.Collection, server: bool) -> None:
self.col = col.weakref() self.col = col.weakref()
self._dir: Optional[str] = None self._dir: str | None = None
if server: if server:
return return
# media directory # media directory
@ -88,7 +88,7 @@ class MediaManager:
# may have been deleted # may have been deleted
pass pass
def dir(self) -> Optional[str]: def dir(self) -> str | None:
return self._dir return self._dir
def force_resync(self) -> None: def force_resync(self) -> None:
@ -106,7 +106,7 @@ class MediaManager:
def strip_av_tags(self, text: str) -> str: def strip_av_tags(self, text: str) -> str:
return self.col._backend.strip_av_tags(text) 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." "This only exists do support a legacy function; do not use."
out = self.col._backend.extract_av_tags(text=text, question_side=True) out = self.col._backend.extract_av_tags(text=text, question_side=True)
return [ return [
@ -148,7 +148,7 @@ class MediaManager:
def have(self, fname: str) -> bool: def have(self, fname: str) -> bool:
return os.path.exists(os.path.join(self.dir(), fname)) 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." "Move provided files to the trash."
self.col._backend.trash_media_files(fnames) self.col._backend.trash_media_files(fnames)
@ -157,7 +157,7 @@ class MediaManager:
def filesInStr( def filesInStr(
self, mid: NotetypeId, string: str, includeRemote: bool = False self, mid: NotetypeId, string: str, includeRemote: bool = False
) -> List[str]: ) -> list[str]:
l = [] l = []
model = self.col.models.get(mid) model = self.col.models.get(mid)
# handle latex # handle latex
@ -204,8 +204,8 @@ class MediaManager:
return output return output
def render_all_latex( def render_all_latex(
self, progress_cb: Optional[Callable[[int], bool]] = None self, progress_cb: Callable[[int], bool] | None = None
) -> Optional[Tuple[int, str]]: ) -> tuple[int, str] | None:
"""Render any LaTeX that is missing. """Render any LaTeX that is missing.
If a progress callback is provided and it returns false, the operation If a progress callback is provided and it returns false, the operation
@ -260,7 +260,7 @@ class MediaManager:
addFile = add_file 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) fname = os.path.basename(opath)
if typeHint: if typeHint:
fname = self.add_extension_based_on_mime(fname, typeHint) fname = self.add_extension_based_on_mime(fname, typeHint)

View file

@ -9,7 +9,7 @@ import copy
import pprint import pprint
import sys import sys
import time 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 import anki # pylint: disable=unused-import
from anki import notetypes_pb2 from anki import notetypes_pb2
@ -29,10 +29,10 @@ ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
# legacy types # legacy types
NotetypeDict = Dict[str, Any] NotetypeDict = dict[str, Any]
NoteType = NotetypeDict NoteType = NotetypeDict
FieldDict = Dict[str, Any] FieldDict = dict[str, Any]
TemplateDict = Dict[str, Union[str, int, None]] TemplateDict = dict[str, Union[str, int, None]]
NotetypeId = NewType("NotetypeId", int) NotetypeId = NewType("NotetypeId", int)
sys.modules["anki.models"].NoteType = NotetypeDict # type: ignore 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 # need to cache responses from the backend. Please do not
# access the cache directly! # access the cache directly!
_cache: Dict[NotetypeId, NotetypeDict] = {} _cache: dict[NotetypeId, NotetypeDict] = {}
def _update_cache(self, notetype: NotetypeDict) -> None: def _update_cache(self, notetype: NotetypeDict) -> None:
self._cache[notetype["id"]] = notetype self._cache[notetype["id"]] = notetype
@ -106,7 +106,7 @@ class ModelManager(DeprecatedNamesMixin):
if ntid in self._cache: if ntid in self._cache:
del self._cache[ntid] 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) return self._cache.get(ntid)
def _clear_cache(self) -> None: def _clear_cache(self) -> None:
@ -142,13 +142,13 @@ class ModelManager(DeprecatedNamesMixin):
# Retrieving and creating models # Retrieving and creating models
############################################################# #############################################################
def id_for_name(self, name: str) -> Optional[NotetypeId]: def id_for_name(self, name: str) -> NotetypeId | None:
try: try:
return NotetypeId(self.col._backend.get_notetype_id_by_name(name)) return NotetypeId(self.col._backend.get_notetype_id_by_name(name))
except NotFoundError: except NotFoundError:
return None return None
def get(self, id: NotetypeId) -> Optional[NotetypeDict]: def get(self, id: NotetypeId) -> NotetypeDict | None:
"Get model with ID, or None." "Get model with ID, or None."
# deal with various legacy input types # deal with various legacy input types
if id is None: if id is None:
@ -165,11 +165,11 @@ class ModelManager(DeprecatedNamesMixin):
return None return None
return notetype return notetype
def all(self) -> List[NotetypeDict]: def all(self) -> list[NotetypeDict]:
"Get all models." "Get all models."
return [self.get(NotetypeId(nt.id)) for nt in self.all_names_and_ids()] 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." "Get model with NAME."
id = self.id_for_name(name) id = self.id_for_name(name)
if id: if id:
@ -231,7 +231,7 @@ class ModelManager(DeprecatedNamesMixin):
# Tools # Tools
################################################## ##################################################
def nids(self, ntid: NotetypeId) -> List[anki.notes.NoteId]: def nids(self, ntid: NotetypeId) -> list[anki.notes.NoteId]:
"Note ids for M." "Note ids for M."
if isinstance(ntid, dict): if isinstance(ntid, dict):
# legacy callers passed in note type # legacy callers passed in note type
@ -261,11 +261,11 @@ class ModelManager(DeprecatedNamesMixin):
# Fields # 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)." "Mapping of field name -> (ord, field)."
return {f["name"]: (f["ord"], f) for f in notetype["flds"]} 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"]] return [f["name"] for f in notetype["flds"]]
def sort_idx(self, notetype: NotetypeDict) -> int: def sort_idx(self, notetype: NotetypeDict) -> int:
@ -394,10 +394,10 @@ and notes.mid = ? and cards.ord = ?""",
def change( # pylint: disable=invalid-name def change( # pylint: disable=invalid-name
self, self,
notetype: NotetypeDict, notetype: NotetypeDict,
nids: List[anki.notes.NoteId], nids: list[anki.notes.NoteId],
newModel: NotetypeDict, newModel: NotetypeDict,
fmap: Dict[int, Optional[int]], fmap: dict[int, int | None],
cmap: Optional[Dict[int, Optional[int]]], cmap: dict[int, int | None] | None,
) -> None: ) -> None:
# - maps are ord->ord, and there should not be duplicate targets # - maps are ord->ord, and there should not be duplicate targets
self.col.mod_schema(check=True) self.col.mod_schema(check=True)
@ -424,8 +424,8 @@ and notes.mid = ? and cards.ord = ?""",
) )
def _convert_legacy_map( def _convert_legacy_map(
self, old_to_new: Dict[int, Optional[int]], new_count: int self, old_to_new: dict[int, int | None], new_count: int
) -> List[int]: ) -> list[int]:
"Convert old->new map to list of old indexes" "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} new_to_old = {v: k for k, v in old_to_new.items() if v is not None}
out = [] out = []
@ -458,7 +458,7 @@ and notes.mid = ? and cards.ord = ?""",
@deprecated(info="use note.cloze_numbers_in_fields()") @deprecated(info="use note.cloze_numbers_in_fields()")
def _availClozeOrds( def _availClozeOrds(
self, notetype: NotetypeDict, flds: str, allow_empty: bool = True self, notetype: NotetypeDict, flds: str, allow_empty: bool = True
) -> List[int]: ) -> list[int]:
import anki.notes_pb2 import anki.notes_pb2
note = anki.notes_pb2.Note(fields=[flds]) note = anki.notes_pb2.Note(fields=[flds])
@ -515,11 +515,11 @@ and notes.mid = ? and cards.ord = ?""",
self.col.set_config("curModel", m["id"]) self.col.set_config("curModel", m["id"])
@deprecated(replaced_by=all_names_and_ids) @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()] return [n.name for n in self.all_names_and_ids()]
@deprecated(replaced_by=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()] return [NotetypeId(n.id) for n in self.all_names_and_ids()]
@deprecated(info="no longer required") @deprecated(info="no longer required")

View file

@ -6,7 +6,7 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import Any, List, NewType, Optional, Sequence, Tuple, Union from typing import Any, NewType, Sequence
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki import hooks, notes_pb2 from anki import hooks, notes_pb2
@ -33,8 +33,8 @@ class Note(DeprecatedNamesMixin):
def __init__( def __init__(
self, self,
col: anki.collection.Collection, col: anki.collection.Collection,
model: Optional[Union[NotetypeDict, NotetypeId]] = None, model: NotetypeDict | NotetypeId | None = None,
id: Optional[NoteId] = None, id: NoteId | None = None,
) -> None: ) -> None:
assert not (model and id) assert not (model and id)
notetype_id = model["id"] if isinstance(model, dict) else model notetype_id = model["id"] if isinstance(model, dict) else model
@ -119,13 +119,13 @@ class Note(DeprecatedNamesMixin):
card._note = self card._note = self
return card 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()] return [self.col.getCard(id) for id in self.card_ids()]
def card_ids(self) -> Sequence[anki.cards.CardId]: def card_ids(self) -> Sequence[anki.cards.CardId]:
return self.col.card_ids_of_note(self.id) 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) return self.col.models.get(self.mid)
_note_type = property(note_type) _note_type = property(note_type)
@ -136,13 +136,13 @@ class Note(DeprecatedNamesMixin):
# Dict interface # Dict interface
################################################## ##################################################
def keys(self) -> List[str]: def keys(self) -> list[str]:
return list(self._fmap.keys()) return list(self._fmap.keys())
def values(self) -> List[str]: def values(self) -> list[str]:
return self.fields 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())] return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
def _field_index(self, key: str) -> int: def _field_index(self, key: str) -> int:

View file

@ -15,7 +15,7 @@ BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
from typing import List, Optional, Sequence from typing import Sequence
from anki import config_pb2 from anki import config_pb2
from anki.cards import CardId 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: def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
return self.col._backend.restore_buried_and_suspended_cards(ids) 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) return self.col._backend.restore_buried_and_suspended_cards(ids)
def unbury_deck( def unbury_deck(
@ -162,12 +162,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self, self,
card_ids: Sequence[CardId], card_ids: Sequence[CardId],
days: str, days: str,
config_key: Optional[Config.String.V] = None, config_key: Config.String.V | None = None,
) -> OpChanges: ) -> OpChanges:
"""Set cards to be due in `days`, turning them into review cards if necessary. """Set cards to be due in `days`, turning them into review cards if necessary.
`days` can be of the form '5' or '5..7' `days` can be of the form '5' or '5..7'
If `config_key` is provided, provided days will be remembered in config.""" 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: if config_key is not None:
key = config_pb2.OptionalStringConfigKey(key=config_key) key = config_pb2.OptionalStringConfigKey(key=config_key)
else: 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 config_key=key, # type: ignore
) )
def resetCards(self, ids: List[CardId]) -> None: def resetCards(self, ids: list[CardId]) -> None:
"Completely reset cards for export." "Completely reset cards for export."
sids = ids2str(ids) sids = ids2str(ids)
assert self.col.db 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) self.orderCards(did)
# for post-import # for post-import
def maybeRandomizeDeck(self, did: Optional[DeckId] = None) -> None: def maybeRandomizeDeck(self, did: DeckId | None = None) -> None:
if not did: if not did:
did = self.col.decks.selected() did = self.col.decks.selected()
conf = self.col.decks.config_dict_for_deck_id(did) 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 # legacy
def sortCards( def sortCards(
self, self,
cids: List[CardId], cids: list[CardId],
start: int = 1, start: int = 1,
step: int = 1, step: int = 1,
shuffle: bool = False, shuffle: bool = False,

View file

@ -3,7 +3,7 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
from typing import List, Optional, Tuple from typing import Optional
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN 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." "Legacy aliases and helpers. These will go away in the future."
def reschedCards( 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: ) -> None:
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") 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(), 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") self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
# used by v2 scheduler and some add-ons # 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": elif type == "time":
self.update_stats(did, milliseconds_delta=cnt) self.update_stats(did, milliseconds_delta=cnt)
def deckDueTree(self) -> List: def deckDueTree(self) -> list:
"List of (base name, did, rev, lrn, new, children)" "List of (base name, did, rev, lrn, new, children)"
print( print(
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()" "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: def _cardConf(self, card: Card) -> DeckConfigDict:
return self.col.decks.config_dict_for_deck_id(card.did) 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) return (ivl, ivl)
# simple aliases # simple aliases

View file

@ -8,7 +8,6 @@ from __future__ import annotations
import random import random
import time import time
from heapq import * from heapq import *
from typing import Any, List, Optional, Tuple, Union
import anki import anki
from anki import hooks from anki import hooks
@ -93,7 +92,7 @@ class Scheduler(V2):
card.usn = self.col.usn() card.usn = self.col.usn()
card.flush() 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] counts = [self.newCount, self.lrnCount, self.revCount]
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
@ -127,7 +126,7 @@ class Scheduler(V2):
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
def _getCard(self) -> Optional[Card]: def _getCard(self) -> Card | None:
"Return the next due card id, or None." "Return the next due card id, or None."
# learning card due? # learning card due?
c = self._getLrnCard() c = self._getLrnCard()
@ -179,12 +178,12 @@ and due <= ? limit %d"""
def _resetLrn(self) -> None: def _resetLrn(self) -> None:
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue: List[Any] = [] self._lrnQueue: list[Any] = []
self._lrnDayQueue: List[Any] = [] self._lrnDayQueue: list[Any] = []
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
def _fillLrn(self) -> Union[bool, List[Any]]: def _fillLrn(self) -> bool | list[Any]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnQueue: if self._lrnQueue:
@ -202,7 +201,7 @@ limit %d"""
self._lrnQueue.sort() self._lrnQueue.sort()
return self._lrnQueue return self._lrnQueue
def _getLrnCard(self, collapse: bool = False) -> Optional[Card]: def _getLrnCard(self, collapse: bool = False) -> Card | None:
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
if collapse: if collapse:
@ -374,7 +373,7 @@ limit %d"""
time.sleep(0.01) time.sleep(0.01)
log() 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." "Remove cards from the learning queues."
if ids: if ids:
extra = f" and id in {ids2str(ids)}" extra = f" and id in {ids2str(ids)}"
@ -429,7 +428,7 @@ and due <= ? limit ?)""",
return self._deckNewLimit(did, self._deckRevLimitSingle) return self._deckNewLimit(did, self._deckRevLimitSingle)
def _resetRev(self) -> None: def _resetRev(self) -> None:
self._revQueue: List[Any] = [] self._revQueue: list[Any] = []
self._revDids = self.col.decks.active()[:] self._revDids = self.col.decks.active()[:]
def _fillRev(self, recursing: bool = False) -> bool: def _fillRev(self, recursing: bool = False) -> bool:

View file

@ -8,7 +8,7 @@ from __future__ import annotations
import random import random
import time import time
from heapq import * 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 import anki # pylint: disable=unused-import
from anki import hooks, scheduler_pb2 from anki import hooks, scheduler_pb2
@ -23,7 +23,7 @@ CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
# legacy type alias # legacy type alias
QueueConfig = Dict[str, Any] QueueConfig = dict[str, Any]
# card types: 0=new, 1=lrn, 2=rev, 3=relrn # card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
@ -49,11 +49,11 @@ class Scheduler(SchedulerBaseWithLegacy):
self.reps = 0 self.reps = 0
self._haveQueues = False self._haveQueues = False
self._lrnCutoff = 0 self._lrnCutoff = 0
self._active_decks: List[DeckId] = [] self._active_decks: list[DeckId] = []
self._current_deck_id = DeckId(1) self._current_deck_id = DeckId(1)
@property @property
def active_decks(self) -> List[DeckId]: def active_decks(self) -> list[DeckId]:
"Caller must make sure to make a copy." "Caller must make sure to make a copy."
return self._active_decks return self._active_decks
@ -96,7 +96,7 @@ class Scheduler(SchedulerBaseWithLegacy):
self.revCount = node.review_count self.revCount = node.review_count
self._immediate_learn_count = node.learn_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.""" """Pop the next card from the queue. None if finished."""
self._checkDay() self._checkDay()
if not self._haveQueues: if not self._haveQueues:
@ -109,7 +109,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return card return card
return None return None
def _getCard(self) -> Optional[Card]: def _getCard(self) -> Card | None:
"""Return the next due card, or None.""" """Return the next due card, or None."""
# learning card due? # learning card due?
c = self._getLrnCard() c = self._getLrnCard()
@ -153,7 +153,7 @@ class Scheduler(SchedulerBaseWithLegacy):
def _resetNew(self) -> None: def _resetNew(self) -> None:
self._newDids = self.col.decks.active()[:] self._newDids = self.col.decks.active()[:]
self._newQueue: List[CardId] = [] self._newQueue: list[CardId] = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self, recursing: bool = False) -> bool: def _fillNew(self, recursing: bool = False) -> bool:
@ -188,7 +188,7 @@ class Scheduler(SchedulerBaseWithLegacy):
self._resetNew() self._resetNew()
return self._fillNew(recursing=True) return self._fillNew(recursing=True)
def _getNewCard(self) -> Optional[Card]: def _getNewCard(self) -> Card | None:
if self._fillNew(): if self._fillNew():
self.newCount -= 1 self.newCount -= 1
return self.col.getCard(self._newQueue.pop()) return self.col.getCard(self._newQueue.pop())
@ -204,7 +204,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return return
self.newCardModulus = 0 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." "True if it's time to display a new card when distributing."
if not self.newCount: if not self.newCount:
return False return False
@ -219,7 +219,7 @@ class Scheduler(SchedulerBaseWithLegacy):
return None return None
def _deckNewLimit( def _deckNewLimit(
self, did: DeckId, fn: Optional[Callable[[DeckDict], int]] = None self, did: DeckId, fn: Callable[[DeckDict], int] | None = None
) -> int: ) -> int:
if not fn: if not fn:
fn = self._deckNewLimitSingle fn = self._deckNewLimitSingle
@ -310,12 +310,12 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
def _resetLrn(self) -> None: def _resetLrn(self) -> None:
self._updateLrnCutoff(force=True) self._updateLrnCutoff(force=True)
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue: List[Tuple[int, CardId]] = [] self._lrnQueue: list[tuple[int, CardId]] = []
self._lrnDayQueue: List[CardId] = [] self._lrnDayQueue: list[CardId] = []
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
def _fillLrn(self) -> Union[bool, List[Any]]: def _fillLrn(self) -> bool | list[Any]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnQueue: if self._lrnQueue:
@ -329,12 +329,12 @@ limit %d"""
% (self._deckLimit(), self.reportLimit), % (self._deckLimit(), self.reportLimit),
cutoff, 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 # as it arrives sorted by did first, we need to sort it
self._lrnQueue.sort() self._lrnQueue.sort()
return self._lrnQueue 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) self._maybeResetLrn(force=collapse and self.lrnCount == 0)
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
@ -348,7 +348,7 @@ limit %d"""
return None return None
# daily learning # daily learning
def _fillLrnDay(self) -> Optional[bool]: def _fillLrnDay(self) -> bool | None:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnDayQueue: if self._lrnDayQueue:
@ -378,7 +378,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
# shouldn't reach here # shouldn't reach here
return False return False
def _getLrnDayCard(self) -> Optional[Card]: def _getLrnDayCard(self) -> Card | None:
if self._fillLrnDay(): if self._fillLrnDay():
self.lrnCount -= 1 self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop()) 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) d = self.col.decks.get(self.col.decks.selected(), default=False)
return self._deckRevLimitSingle(d) return self._deckRevLimitSingle(d)
def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int: def _deckRevLimitSingle(self, d: dict[str, Any]) -> int:
# invalid deck selected? # invalid deck selected?
if not d: if not d:
return 0 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) return hooks.scheduler_review_limit_for_single_deck(lim, d)
def _resetRev(self) -> None: def _resetRev(self) -> None:
self._revQueue: List[CardId] = [] self._revQueue: list[CardId] = []
def _fillRev(self, recursing: bool = False) -> bool: def _fillRev(self, recursing: bool = False) -> bool:
"True if a review card can be fetched." "True if a review card can be fetched."
@ -439,7 +439,7 @@ limit ?"""
self._resetRev() self._resetRev()
return self._fillRev(recursing=True) return self._fillRev(recursing=True)
def _getRevCard(self) -> Optional[Card]: def _getRevCard(self) -> Card | None:
if self._fillRev(): if self._fillRev():
self.revCount -= 1 self.revCount -= 1
return self.col.getCard(self._revQueue.pop()) return self.col.getCard(self._revQueue.pop())
@ -601,7 +601,7 @@ limit ?"""
self._rescheduleLrnCard(card, conf, delay=delay) self._rescheduleLrnCard(card, conf, delay=delay)
def _rescheduleLrnCard( def _rescheduleLrnCard(
self, card: Card, conf: QueueConfig, delay: Optional[int] = None self, card: Card, conf: QueueConfig, delay: int | None = None
) -> Any: ) -> Any:
# normal delay for the current step? # normal delay for the current step?
if delay is None: if delay is None:
@ -690,9 +690,9 @@ limit ?"""
def _leftToday( def _leftToday(
self, self,
delays: List[int], delays: list[int],
left: int, left: int,
now: Optional[int] = None, now: int | None = None,
) -> int: ) -> int:
"The number of steps that can be completed by the day cutoff." "The number of steps that can be completed by the day cutoff."
if not now: if not now:
@ -927,7 +927,7 @@ limit ?"""
min, max = self._fuzzIvlRange(ivl) min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max) return random.randint(min, max)
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]: def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
if ivl < 2: if ivl < 2:
return (1, 1) return (1, 1)
elif ivl == 2: elif ivl == 2:
@ -1080,7 +1080,7 @@ limit ?"""
########################################################################## ##########################################################################
def _burySiblings(self, card: Card) -> None: def _burySiblings(self, card: Card) -> None:
toBury: List[CardId] = [] toBury: list[CardId] = []
nconf = self._newConf(card) nconf = self._newConf(card)
buryNew = nconf.get("bury", True) buryNew = nconf.get("bury", True)
rconf = self._revConf(card) rconf = self._revConf(card)
@ -1115,7 +1115,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
# Review-related UI helpers # 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] counts = [self.newCount, self.lrnCount, self.revCount]
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)

View file

@ -12,7 +12,7 @@ as '2' internally.
from __future__ import annotations from __future__ import annotations
from typing import List, Literal, Sequence, Tuple from typing import Literal, Optional, Sequence
from anki import scheduler_pb2 from anki import scheduler_pb2
from anki.cards import Card 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." "Don't use this, it is a stop-gap until this code is refactored."
return not self.get_queued_cards().cards 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() info = self.get_queued_cards()
return (info.new_count, info.learning_count, info.review_count) 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 # called by col.decks.active(), which add-ons are using
@property @property
def active_decks(self) -> List[DeckId]: def active_decks(self) -> list[DeckId]:
try: try:
return self.col.db.list("select id from active_decks") return self.col.db.list("select id from active_decks")
except DBError: except DBError:

View file

@ -11,7 +11,7 @@ from __future__ import annotations
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Union from typing import Union
@dataclass @dataclass
@ -23,10 +23,10 @@ class TTSTag:
field_text: str field_text: str
lang: str lang: str
voices: List[str] voices: list[str]
speed: float speed: float
# each arg should be in the form 'foo=bar' # each arg should be in the form 'foo=bar'
other_args: List[str] other_args: list[str]
@dataclass @dataclass

View file

@ -8,7 +8,7 @@ from __future__ import annotations
import datetime import datetime
import json import json
import time import time
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Sequence
import anki.cards import anki.cards
import anki.collection import anki.collection
@ -37,12 +37,12 @@ class CardStats:
# legacy # 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) 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 = "<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 return txt
def date(self, tm: float) -> str: def date(self, tm: float) -> str:
@ -183,7 +183,7 @@ from revlog where id > ? """
# Due and cumulative due # 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 start = 0
if self.type == PERIOD_MONTH: if self.type == PERIOD_MONTH:
end, chunk = 31, 1 end, chunk = 31, 1
@ -245,7 +245,7 @@ from revlog where id > ? """
return txt return txt
def _dueInfo(self, tot: int, num: int) -> str: def _dueInfo(self, tot: int, num: int) -> str:
i: List[str] = [] i: list[str] = []
self._line( self._line(
i, i,
"Total", "Total",
@ -264,7 +264,7 @@ and due = ?"""
return self._lineTbl(i) return self._lineTbl(i)
def _due( 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: ) -> Any:
lim = "" lim = ""
if start is not None: if start is not None:
@ -293,7 +293,7 @@ group by day order by day"""
data = self._added(days, chunk) data = self._added(days, chunk)
if not data: if not data:
return "" return ""
conf: Dict[str, Any] = dict( conf: dict[str, Any] = dict(
xaxis=dict(tickDecimals=0, max=0.5), xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right", min=0)], 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 = self._title("Added", "The number of new cards you have added.")
txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards") txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards")
# total and per day average # total and per day average
tot = sum([i[1] for i in data]) tot = sum(i[1] for i in data)
period = self._periodDays() period = self._periodDays()
if not period: if not period:
# base off date of earliest added card # base off date of earliest added card
period = self._deckAge("add") period = self._deckAge("add")
i: List[str] = [] i: list[str] = []
self._line(i, "Total", "%d cards" % tot) self._line(i, "Total", "%d cards" % tot)
self._line(i, "Average", self._avgDay(tot, period, "cards")) self._line(i, "Average", self._avgDay(tot, period, "cards"))
txt += self._lineTbl(i) txt += self._lineTbl(i)
@ -328,7 +328,7 @@ group by day order by day"""
data = self._done(days, chunk) data = self._done(days, chunk)
if not data: if not data:
return "" return ""
conf: Dict[str, Any] = dict( conf: dict[str, Any] = dict(
xaxis=dict(tickDecimals=0, max=0.5), xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right", min=0)], yaxes=[dict(min=0), dict(position="right", min=0)],
) )
@ -384,20 +384,20 @@ group by day order by day"""
def _ansInfo( def _ansInfo(
self, self,
totd: List[Tuple[int, float]], totd: list[tuple[int, float]],
studied: int, studied: int,
first: int, first: int,
unit: str, unit: str,
convHours: bool = False, convHours: bool = False,
total: Optional[int] = None, total: int | None = None,
) -> Tuple[str, int]: ) -> tuple[str, int]:
assert totd assert totd
tot = totd[-1][1] tot = totd[-1][1]
period = self._periodDays() period = self._periodDays()
if not period: if not period:
# base off earliest repetition date # base off earliest repetition date
period = self._deckAge("review") period = self._deckAge("review")
i: List[str] = [] i: list[str] = []
self._line( self._line(
i, i,
"Days studied", "Days studied",
@ -432,12 +432,12 @@ group by day order by day"""
def _splitRepData( def _splitRepData(
self, self,
data: List[Tuple[Any, ...]], data: list[tuple[Any, ...]],
spec: Sequence[Tuple[int, str, str]], spec: Sequence[tuple[int, str, str]],
) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]: ) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
sep: Dict[int, Any] = {} sep: dict[int, Any] = {}
totcnt = {} totcnt = {}
totd: Dict[int, Any] = {} totd: dict[int, Any] = {}
alltot = [] alltot = []
allcnt: float = 0 allcnt: float = 0
for (n, col, lab) in spec: for (n, col, lab) in spec:
@ -471,7 +471,7 @@ group by day order by day"""
) )
return (ret, alltot) 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 = [] lims = []
if num is not None: if num is not None:
lims.append( lims.append(
@ -498,7 +498,7 @@ group by day order by day"""
chunk, chunk,
) )
def _done(self, num: Optional[int] = 7, chunk: int = 1) -> Any: def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
lims = [] lims = []
if num is not None: if num is not None:
lims.append( lims.append(
@ -605,12 +605,12 @@ group by day order by day)"""
yaxes=[dict(), dict(position="right", max=105)], 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, "Average interval", self.col.format_timespan(avg * 86400))
self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400)) self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400))
return txt + self._lineTbl(i) 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() start, end, chunk = self.get_start_end_chunk()
lim = "and grp <= %d" % end if end else "" lim = "and grp <= %d" % end if end else ""
data = [ 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 # 3 + 4 + 4 + spaces on sides and middle = 15
# yng starts at 1+3+1 = 5 # yng starts at 1+3+1 = 5
# mtr starts at 5+4+1 = 10 # 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") types = ("lrn", "yng", "mtr")
eases = self._eases() eases = self._eases()
for (type, ease, cnt) in 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) txt += self._easeInfo(eases)
return txt 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]} types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
for (type, ease, cnt) in eases: for (type, ease, cnt) in eases:
if ease == 1: if ease == 1:
@ -752,7 +752,7 @@ order by thetype, ease"""
shifted = [] shifted = []
counts = [] counts = []
mcount = 0 mcount = 0
trend: List[Tuple[int, int]] = [] trend: list[tuple[int, int]] = []
peak = 0 peak = 0
for d in data: for d in data:
hour = (d[0] - 4) % 24 hour = (d[0] - 4) % 24
@ -852,9 +852,9 @@ group by hour having count() > 30 order by hour"""
("Suspended+Buried", colSusp), ("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 # text data
i: List[str] = [] i: list[str] = []
(c, f) = self.col.db.first( (c, f) = self.col.db.first(
""" """
select count(id), count(distinct nid) from cards select count(id), count(distinct nid) from cards
@ -880,9 +880,7 @@ when you answer "good" on a review."""
) )
return txt return txt
def _line( def _line(self, i: list[str], a: str, b: int | str, bold: bool = True) -> None:
self, i: List[str], a: str, b: Union[int, str], bold: bool = True
) -> None:
# T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews". # T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
colon = ":" colon = ":"
if bold: if bold:
@ -896,7 +894,7 @@ when you answer "good" on a review."""
% (a, colon, b) % (a, colon, b)
) )
def _lineTbl(self, i: List[str]) -> str: def _lineTbl(self, i: list[str]) -> str:
return "<table width=400>" + "".join(i) + "</table>" return "<table width=400>" + "".join(i) + "</table>"
def _factors(self) -> Any: def _factors(self) -> Any:
@ -945,7 +943,7 @@ from cards where did in %s"""
self, self,
id: str, id: str,
data: Any, data: Any,
conf: Optional[Any] = None, conf: Any | None = None,
type: str = "bars", type: str = "bars",
xunit: int = 1, xunit: int = 1,
ylabel: str = "Cards", ylabel: str = "Cards",
@ -1069,7 +1067,7 @@ $(function () {
) )
def _title(self, title: str, subtitle: str = "") -> str: 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: def _deckAge(self, by: str) -> int:
lim = self._revlogLimit() lim = self._revlogLimit()
@ -1089,7 +1087,7 @@ $(function () {
period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400))) period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400)))
return period return period
def _periodDays(self) -> Optional[int]: def _periodDays(self) -> int | None:
start, end, chunk = self.get_start_end_chunk() start, end, chunk = self.get_start_end_chunk()
if end is None: if end is None:
return None return None

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, List, Tuple from typing import Any, Callable
import anki.collection import anki.collection
import anki.models import anki.models
@ -15,7 +15,7 @@ StockNotetypeKind = notetypes_pb2.StockNotetype.Kind
# add-on authors can add ("note type name", function) # add-on authors can add ("note type name", function)
# to this list to have it shown in the add/clone note type screen # to this list to have it shown in the add/clone note type screen
models: List[Tuple] = [] models: list[tuple] = []
def _get_stock_notetype( def _get_stock_notetype(
@ -26,9 +26,9 @@ def _get_stock_notetype(
def get_stock_notetypes( def get_stock_notetypes(
col: anki.collection.Collection, col: anki.collection.Collection,
) -> List[Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]: ) -> list[tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]:
out: List[ out: list[
Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
] = [] ] = []
# add standard # add standard
for kind in [ for kind in [

View file

@ -116,7 +116,7 @@ def after_full_sync() -> None:
def get_method( def get_method(
method_str: str, method_str: str,
) -> Optional[SyncServerMethodRequest.Method.V]: # pylint: disable=no-member ) -> SyncServerMethodRequest.Method.V | None: # pylint: disable=no-member
s = method_str s = method_str
if s == "hostKey": if s == "hostKey":
return Method.HOST_KEY return Method.HOST_KEY

View file

@ -13,7 +13,7 @@ from __future__ import annotations
import pprint import pprint
import re import re
from typing import Collection, List, Match, Optional, Sequence from typing import Collection, Match, Sequence
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.collection import anki.collection
@ -33,7 +33,7 @@ class TagManager:
self.col = col.weakref() self.col = col.weakref()
# legacy add-on code expects a List return type # 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()) return list(self.col._backend.all_tags())
def __repr__(self) -> str: def __repr__(self) -> str:
@ -50,7 +50,7 @@ class TagManager:
def clear_unused_tags(self) -> OpChangesWithCount: def clear_unused_tags(self) -> OpChangesWithCount:
return self.col._backend.clear_unused_tags() 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" basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children: if not children:
query = f"{basequery} AND c.did=?" query = f"{basequery} AND c.did=?"
@ -123,11 +123,11 @@ class TagManager:
# String-based utilities # 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." "Parse a string and return a list of tags."
return [t for t in tags.replace("\u3000", " ").split(" ") if t] 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." "Join tags into a single string, with leading and trailing spaces."
if not tags: if not tags:
return "" return ""
@ -164,30 +164,30 @@ class TagManager:
########################################################################## ##########################################################################
# this is now a no-op - the tags are canonified when the note is saved # 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 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." "True if TAG is in TAGS. Ignore case."
return tag.lower() in [t.lower() for t in tags] return tag.lower() in [t.lower() for t in tags]
# legacy # legacy
########################################################################## ##########################################################################
def registerNotes(self, nids: Optional[List[int]] = None) -> None: def registerNotes(self, nids: list[int] | None = None) -> None:
self.clear_unused_tags() self.clear_unused_tags()
def register( 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: ) -> None:
print("tags.register() is deprecated and no longer works") 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." "Add tags in bulk. TAGS is space-separated."
if add: if add:
self.bulk_add(ids, tags) self.bulk_add(ids, tags)
else: else:
self.bulk_remove(ids, tags) 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) 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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Sequence, Union
import anki import anki
from anki import card_rendering_pb2, hooks from anki import card_rendering_pb2, hooks
@ -50,10 +50,10 @@ CARD_BLANK_HELP = (
class TemplateReplacement: class TemplateReplacement:
field_name: str field_name: str
current_text: str current_text: str
filters: List[str] filters: list[str]
TemplateReplacementList = List[Union[str, TemplateReplacement]] TemplateReplacementList = list[Union[str, TemplateReplacement]]
@dataclass @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)) return list(map(av_tag_to_native, tags))
@ -125,7 +125,7 @@ class TemplateRenderContext:
note: Note, note: Note,
card: Card, card: Card,
notetype: NotetypeDict, notetype: NotetypeDict,
template: Dict, template: dict,
fill_empty: bool, fill_empty: bool,
) -> TemplateRenderContext: ) -> TemplateRenderContext:
return TemplateRenderContext( return TemplateRenderContext(
@ -144,7 +144,7 @@ class TemplateRenderContext:
note: Note, note: Note,
browser: bool = False, browser: bool = False,
notetype: NotetypeDict = None, notetype: NotetypeDict = None,
template: Optional[Dict] = None, template: dict | None = None,
fill_empty: bool = False, fill_empty: bool = False,
) -> None: ) -> None:
self._col = col.weakref() self._col = col.weakref()
@ -153,7 +153,7 @@ class TemplateRenderContext:
self._browser = browser self._browser = browser
self._template = template self._template = template
self._fill_empty = fill_empty self._fill_empty = fill_empty
self._fields: Optional[Dict] = None self._fields: dict | None = None
self._latex_svg = False self._latex_svg = False
if not notetype: if not notetype:
self._note_type = note.note_type() self._note_type = note.note_type()
@ -162,12 +162,12 @@ class TemplateRenderContext:
# if you need to store extra state to share amongst rendering # if you need to store extra state to share amongst rendering
# hooks, you can insert it into this dictionary # 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: def col(self) -> anki.collection.Collection:
return self._col return self._col
def fields(self) -> Dict[str, str]: def fields(self) -> dict[str, str]:
print(".fields() is obsolete, use .note() or .card()") print(".fields() is obsolete, use .note() or .card()")
if not self._fields: if not self._fields:
# fields from note # fields from note
@ -269,8 +269,8 @@ class TemplateRenderOutput:
"Stores the rendered templates and extracted AV tags." "Stores the rendered templates and extracted AV tags."
question_text: str question_text: str
answer_text: str answer_text: str
question_av_tags: List[AVTag] question_av_tags: list[AVTag]
answer_av_tags: List[AVTag] answer_av_tags: list[AVTag]
css: str = "" css: str = ""
def question_and_style(self) -> str: def question_and_style(self) -> str:
@ -281,7 +281,7 @@ class TemplateRenderOutput:
# legacy # 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() template = card.template()
if browser: if browser:
q, a = template.get("bqfmt"), template.get("bafmt") 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( def apply_custom_filters(
rendered: TemplateReplacementList, rendered: TemplateReplacementList,
ctx: TemplateRenderContext, ctx: TemplateRenderContext,
front_side: Optional[str], front_side: str | None,
) -> str: ) -> str:
"Complete rendering by applying any pending custom filters." "Complete rendering by applying any pending custom filters."
# template already fully rendered? # template already fully rendered?

View file

@ -17,11 +17,11 @@ import time
import traceback import traceback
from contextlib import contextmanager from contextlib import contextmanager
from hashlib import sha1 from hashlib import sha1
from typing import Any, Iterable, Iterator, List, Optional, Union from typing import Any, Iterable, Iterator
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
_tmpdir: Optional[str] _tmpdir: str | None
try: try:
# pylint: disable=c-extension-no-member # 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,...)'.""" """Given a list of integers, return a string '(int1,int2,...)'."""
return f"({','.join(str(i) for i in ids)})" 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) return "\x1f".join(list)
def splitFields(string: str) -> List[str]: def splitFields(string: str) -> list[str]:
return string.split("\x1f") 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): if isinstance(data, str):
data = data.encode("utf-8") data = data.encode("utf-8")
return sha1(data).hexdigest() return sha1(data).hexdigest()
@ -218,7 +218,7 @@ def noBundledLibs() -> Iterator[None]:
os.environ["LD_LIBRARY_PATH"] = oldlpath 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." "Execute a command. If WAIT, return exit code."
# ensure we don't open a separate window for forking process on windows # ensure we don't open a separate window for forking process on windows
if isWin: if isWin:
@ -262,7 +262,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ':*?"<>|' invalidFilenameChars = ':*?"<>|'
def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]: def invalidFilename(str: str, dirsep: bool = True) -> str | None:
for c in invalidFilenameChars: for c in invalidFilenameChars:
if c in str: if c in str:
return c return c

View file

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

View file

@ -4,7 +4,7 @@
import copy import copy
import os import os
import time import time
from typing import Tuple from typing import Dict
import pytest import pytest
@ -433,7 +433,7 @@ def test_reviews():
assert "leech" in c.note().tags 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() col = getEmptyCol()
parent = col.decks.get(col.decks.id("parent")) parent = col.decks.get(col.decks.id("parent"))

View file

@ -21,7 +21,7 @@ class Hook:
name: str name: str
# string of the typed arguments passed to the callback, eg # string of the typed arguments passed to the callback, eg
# ["kind: str", "val: int"] # ["kind: str", "val: int"]
args: List[str] = None args: list[str] = None
# string of the return type. if set, hook is a filter. # string of the return type. if set, hook is a filter.
return_type: Optional[str] = None return_type: Optional[str] = None
# if add-ons may be relying on the legacy hook name, add it here # if add-ons may be relying on the legacy hook name, add it here
@ -41,7 +41,7 @@ class Hook:
types_str = ", ".join(types) types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]" return f"Callable[[{types_str}], {self.return_type or 'None'}]"
def arg_names(self) -> List[str]: def arg_names(self) -> list[str]:
names = [] names = []
for arg in self.args or []: for arg in self.args or []:
if not arg: if not arg:
@ -64,7 +64,7 @@ class Hook:
def list_code(self) -> str: def list_code(self) -> str:
return f"""\ return f"""\
_hooks: List[{self.callable()}] = [] _hooks: list[{self.callable()}] = []
""" """
def code(self) -> str: def code(self) -> str:
@ -153,7 +153,7 @@ class {self.classname()}:
return f"{out}\n\n" 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")) hooks.sort(key=attrgetter("name"))
code = f"{prefix}\n" code = f"{prefix}\n"
for hook in hooks: for hook in hooks:

View file

@ -1,6 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import argparse import argparse
import builtins import builtins
import cProfile import cProfile
@ -10,7 +12,7 @@ import os
import sys import sys
import tempfile import tempfile
import traceback import traceback
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast from typing import Any, Callable, Optional, cast
import anki.lang import anki.lang
from anki._backend import RustBackend from anki._backend import RustBackend
@ -90,7 +92,7 @@ from aqt import stats, about, preferences, mediasync # isort:skip
class DialogManager: class DialogManager:
_dialogs: Dict[str, list] = { _dialogs: dict[str, list] = {
"AddCards": [addcards.AddCards, None], "AddCards": [addcards.AddCards, None],
"AddonsDialog": [addons.AddonsDialog, None], "AddonsDialog": [addons.AddonsDialog, None],
"Browser": [browser.Browser, None], "Browser": [browser.Browser, None],
@ -267,7 +269,7 @@ class AnkiApp(QApplication):
KEY = f"anki{checksum(getpass.getuser())}" KEY = f"anki{checksum(getpass.getuser())}"
TMOUT = 30000 TMOUT = 30000
def __init__(self, argv: List[str]) -> None: def __init__(self, argv: list[str]) -> None:
QApplication.__init__(self, argv) QApplication.__init__(self, argv)
self._argv = argv self._argv = argv
@ -328,7 +330,7 @@ class AnkiApp(QApplication):
return QApplication.event(self, evt) 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)." "Returns (opts, args)."
# py2app fails to strip this in some instances, then anki dies # py2app fails to strip this in some instances, then anki dies
# as there's no such profile # 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. """Start AnkiQt application or reuse an existing instance if one exists.
If the function is invoked with exec=False, the AnkiQt will not enter 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 # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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.editor
import aqt.forms import aqt.forms
@ -50,7 +50,7 @@ class AddCards(QDialog):
self.setupEditor() self.setupEditor()
self.setupButtons() self.setupButtons()
self._load_new_note() self._load_new_note()
self.history: List[NoteId] = [] self.history: list[NoteId] = []
self._last_added_note: Optional[Note] = None self._last_added_note: Optional[Note] = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute) gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add") restoreGeom(self, "add")

View file

@ -11,7 +11,7 @@ from collections import defaultdict
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime 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 urllib.parse import parse_qs, urlparse
from zipfile import ZipFile from zipfile import ZipFile
@ -53,7 +53,7 @@ class AbortAddonImport(Exception):
@dataclass @dataclass
class InstallOk: class InstallOk:
name: str name: str
conflicts: List[str] conflicts: list[str]
compatible: bool compatible: bool
@ -75,13 +75,13 @@ class DownloadOk:
@dataclass @dataclass
class DownloadError: class DownloadError:
# set if result was not 200 # set if result was not 200
status_code: Optional[int] = None status_code: int | None = None
# set if an exception occurred # set if an exception occurred
exception: Optional[Exception] = None exception: Exception | None = None
# first arg is add-on id # first arg is add-on id
DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]] DownloadLogEntry = tuple[int, Union[DownloadError, InstallError, InstallOk]]
@dataclass @dataclass
@ -101,21 +101,21 @@ current_point_version = anki.utils.pointVersion()
@dataclass @dataclass
class AddonMeta: class AddonMeta:
dir_name: str dir_name: str
provided_name: Optional[str] provided_name: str | None
enabled: bool enabled: bool
installed_at: int installed_at: int
conflicts: List[str] conflicts: list[str]
min_point_version: int min_point_version: int
max_point_version: int max_point_version: int
branch_index: int branch_index: int
human_version: Optional[str] human_version: str | None
update_enabled: bool update_enabled: bool
homepage: Optional[str] homepage: str | None
def human_name(self) -> str: def human_name(self) -> str:
return self.provided_name or self.dir_name 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) m = ANKIWEB_ID_RE.match(self.dir_name)
if m: if m:
return int(m.group(0)) return int(m.group(0))
@ -134,13 +134,13 @@ class AddonMeta:
def is_latest(self, server_update_time: int) -> bool: def is_latest(self, server_update_time: int) -> bool:
return self.installed_at >= server_update_time return self.installed_at >= server_update_time
def page(self) -> Optional[str]: def page(self) -> str | None:
if self.ankiweb_id(): if self.ankiweb_id():
return f"{aqt.appShared}info/{self.dir_name}" return f"{aqt.appShared}info/{self.dir_name}"
return self.homepage return self.homepage
@staticmethod @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( return AddonMeta(
dir_name=dir_name, dir_name=dir_name,
provided_name=json_meta.get("name"), provided_name=json_meta.get("name"),
@ -207,7 +207,7 @@ class AddonManager:
sys.path.insert(0, self.addonsFolder()) sys.path.insert(0, self.addonsFolder())
# in new code, you may want all_addon_meta() instead # in new code, you may want all_addon_meta() instead
def allAddons(self) -> List[str]: def allAddons(self) -> list[str]:
l = [] l = []
for d in os.listdir(self.addonsFolder()): for d in os.listdir(self.addonsFolder()):
path = self.addonsFolder(d) path = self.addonsFolder(d)
@ -222,7 +222,7 @@ class AddonManager:
def all_addon_meta(self) -> Iterable[AddonMeta]: def all_addon_meta(self) -> Iterable[AddonMeta]:
return map(self.addon_meta, self.allAddons()) 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() root = self.mw.pm.addonFolder()
if dir is None: if dir is None:
return root return root
@ -280,7 +280,7 @@ class AddonManager:
return os.path.join(self.addonsFolder(dir), "meta.json") return os.path.join(self.addonsFolder(dir), "meta.json")
# in new code, use self.addon_meta() instead # 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) path = self._addonMetaPath(dir)
try: try:
with open(path, encoding="utf8") as f: with open(path, encoding="utf8") as f:
@ -293,12 +293,12 @@ class AddonManager:
return dict() return dict()
# in new code, use write_addon_meta() instead # 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) path = self._addonMetaPath(dir)
with open(path, "w", encoding="utf8") as f: with open(path, "w", encoding="utf8") as f:
json.dump(meta, 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) addon = self.addon_meta(dir)
should_enable = enable if enable is not None else not addon.enabled should_enable = enable if enable is not None else not addon.enabled
if should_enable is True: if should_enable is True:
@ -316,7 +316,7 @@ class AddonManager:
addon.enabled = should_enable addon.enabled = should_enable
self.write_addon_meta(addon) self.write_addon_meta(addon)
def ankiweb_addons(self) -> List[int]: def ankiweb_addons(self) -> list[int]:
ids = [] ids = []
for meta in self.all_addon_meta(): for meta in self.all_addon_meta():
if meta.ankiweb_id() is not None: if meta.ankiweb_id() is not None:
@ -332,7 +332,7 @@ class AddonManager:
def addonName(self, dir: str) -> str: def addonName(self, dir: str) -> str:
return self.addon_meta(dir).human_name() 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 return self.addon_meta(dir).conflicts
def annotatedName(self, dir: str) -> str: def annotatedName(self, dir: str) -> str:
@ -345,8 +345,8 @@ class AddonManager:
# Conflict resolution # Conflict resolution
###################################################################### ######################################################################
def allAddonConflicts(self) -> Dict[str, List[str]]: def allAddonConflicts(self) -> dict[str, list[str]]:
all_conflicts: Dict[str, List[str]] = defaultdict(list) all_conflicts: dict[str, list[str]] = defaultdict(list)
for addon in self.all_addon_meta(): for addon in self.all_addon_meta():
if not addon.enabled: if not addon.enabled:
continue continue
@ -354,7 +354,7 @@ class AddonManager:
all_conflicts[other_dir].append(addon.dir_name) all_conflicts[other_dir].append(addon.dir_name)
return all_conflicts 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) conflicts = conflicts or self.addonConflicts(dir)
installed = self.allAddons() installed = self.allAddons()
@ -371,7 +371,7 @@ class AddonManager:
# Installing and deleting add-ons # Installing and deleting add-ons
###################################################################### ######################################################################
def readManifestFile(self, zfile: ZipFile) -> Dict[Any, Any]: def readManifestFile(self, zfile: ZipFile) -> dict[Any, Any]:
try: try:
with zfile.open("manifest.json") as f: with zfile.open("manifest.json") as f:
data = json.loads(f.read()) data = json.loads(f.read())
@ -385,8 +385,8 @@ class AddonManager:
return manifest return manifest
def install( def install(
self, file: Union[IO, str], manifest: Dict[str, Any] = None self, file: IO | str, manifest: dict[str, Any] = None
) -> Union[InstallOk, InstallError]: ) -> InstallOk | InstallError:
"""Install add-on from path or file-like object. Metadata is read """Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest' from the manifest file, with keys overriden by supplying a 'manifest'
dictionary""" dictionary"""
@ -463,8 +463,8 @@ class AddonManager:
###################################################################### ######################################################################
def processPackages( def processPackages(
self, paths: List[str], parent: QWidget = None self, paths: list[str], parent: QWidget = None
) -> Tuple[List[str], List[str]]: ) -> tuple[list[str], list[str]]:
log = [] log = []
errs = [] errs = []
@ -493,7 +493,7 @@ class AddonManager:
def _installationErrorReport( def _installationErrorReport(
self, result: InstallError, base: str, mode: str = "download" self, result: InstallError, base: str, mode: str = "download"
) -> List[str]: ) -> list[str]:
messages = { messages = {
"zip": tr.addons_corrupt_addon_file(), "zip": tr.addons_corrupt_addon_file(),
@ -511,7 +511,7 @@ class AddonManager:
def _installationSuccessReport( def _installationSuccessReport(
self, result: InstallOk, base: str, mode: str = "download" self, result: InstallOk, base: str, mode: str = "download"
) -> List[str]: ) -> list[str]:
name = result.name or base name = result.name or base
if mode == "download": if mode == "download":
@ -536,8 +536,8 @@ class AddonManager:
# Updating # Updating
###################################################################### ######################################################################
def extract_update_info(self, items: List[Dict]) -> List[UpdateInfo]: def extract_update_info(self, items: list[dict]) -> list[UpdateInfo]:
def extract_one(item: Dict) -> UpdateInfo: def extract_one(item: dict) -> UpdateInfo:
id = item["id"] id = item["id"]
meta = self.addon_meta(str(id)) meta = self.addon_meta(str(id))
branch_idx = meta.branch_index branch_idx = meta.branch_index
@ -545,7 +545,7 @@ class AddonManager:
return list(map(extract_one, items)) 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: for item in items:
self.update_supported_version(item) self.update_supported_version(item)
@ -581,7 +581,7 @@ class AddonManager:
if updated: if updated:
self.write_addon_meta(addon) 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.""" """Return ids of add-ons requiring an update."""
need_update = [] need_update = []
for item in items: for item in items:
@ -600,10 +600,10 @@ class AddonManager:
# Add-on Config # Add-on Config
###################################################################### ######################################################################
_configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {} _configButtonActions: dict[str, Callable[[], bool | None]] = {}
_configUpdatedActions: Dict[str, Callable[[Any], 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") path = os.path.join(self.addonsFolder(dir), "config.json")
try: try:
with open(path, encoding="utf8") as f: with open(path, encoding="utf8") as f:
@ -622,7 +622,7 @@ class AddonManager:
def addonFromModule(self, module: str) -> str: def addonFromModule(self, module: str) -> str:
return module.split(".")[0] 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) return self._configButtonActions.get(addon)
def configUpdatedAction(self, addon: str) -> Callable[[Any], None]: def configUpdatedAction(self, addon: str) -> Callable[[Any], None]:
@ -649,7 +649,7 @@ class AddonManager:
# Add-on Config API # 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) addon = self.addonFromModule(module)
# get default config # get default config
config = self.addonConfigDefaults(addon) config = self.addonConfigDefaults(addon)
@ -661,7 +661,7 @@ class AddonManager:
config.update(userConf) config.update(userConf)
return config 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) addon = self.addonFromModule(module)
self._configButtonActions[addon] = fn self._configButtonActions[addon] = fn
@ -700,7 +700,7 @@ class AddonManager:
# Web Exports # Web Exports
###################################################################### ######################################################################
_webExports: Dict[str, str] = {} _webExports: dict[str, str] = {}
def setWebExports(self, module: str, pattern: str) -> None: def setWebExports(self, module: str, pattern: str) -> None:
addon = self.addonFromModule(module) addon = self.addonFromModule(module)
@ -825,18 +825,18 @@ class AddonsDialog(QDialog):
gui_hooks.addons_dialog_did_change_selected_addon(self, addon) gui_hooks.addons_dialog_did_change_selected_addon(self, addon)
return return
def selectedAddons(self) -> List[str]: def selectedAddons(self) -> list[str]:
idxs = [x.row() for x in self.form.addonList.selectedIndexes()] idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
return [self.addons[idx].dir_name for idx in idxs] return [self.addons[idx].dir_name for idx in idxs]
def onlyOneSelected(self) -> Optional[str]: def onlyOneSelected(self) -> str | None:
dirs = self.selectedAddons() dirs = self.selectedAddons()
if len(dirs) != 1: if len(dirs) != 1:
showInfo(tr.addons_please_select_a_single_addon_first()) showInfo(tr.addons_please_select_a_single_addon_first())
return None return None
return dirs[0] 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()] idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
if len(idxs) != 1: if len(idxs) != 1:
showInfo(tr.addons_please_select_a_single_addon_first()) showInfo(tr.addons_please_select_a_single_addon_first())
@ -887,14 +887,14 @@ class AddonsDialog(QDialog):
if obj.ids: if obj.ids:
download_addons(self, self.mgr, obj.ids, self.after_downloading) 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() self.redrawAddons()
if log: if log:
show_log_to_user(self, log) show_log_to_user(self, log)
else: else:
tooltip(tr.addons_no_updates_available()) 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: if not paths:
key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})" key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})"
paths_ = getFile( paths_ = getFile(
@ -943,7 +943,7 @@ class GetAddons(QDialog):
self.addonsDlg = dlg self.addonsDlg = dlg
self.mgr = dlg.mgr self.mgr = dlg.mgr
self.mw = self.mgr.mw self.mw = self.mgr.mw
self.ids: List[int] = [] self.ids: list[int] = []
self.form = aqt.forms.getaddons.Ui_Dialog() self.form = aqt.forms.getaddons.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
b = self.form.buttonBox.addButton( 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." "Fetch a single add-on from AnkiWeb."
try: try:
resp = client.get( resp = client.get(
@ -1025,7 +1025,7 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
return meta 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)) return "<br>".join(map(describe_log_entry, log))
@ -1053,7 +1053,7 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
return buf 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) 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 self.client.progress_hook = bg_thread_progress
def download( def download(
self, ids: List[int], on_done: Callable[[List[DownloadLogEntry]], None] self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None]
) -> None: ) -> None:
self.ids = ids self.ids = ids
self.log: List[DownloadLogEntry] = [] self.log: list[DownloadLogEntry] = []
self.dl_bytes = 0 self.dl_bytes = 0
self.last_tooltip = 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) 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) have_problem = download_encountered_problem(log)
if have_problem: if have_problem:
@ -1153,9 +1153,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
def download_addons( def download_addons(
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
ids: List[int], ids: list[int],
on_done: Callable[[List[DownloadLogEntry]], None], on_done: Callable[[list[DownloadLogEntry]], None],
client: Optional[HttpClient] = None, client: HttpClient | None = None,
) -> None: ) -> None:
if client is None: if client is None:
client = HttpClient() client = HttpClient()
@ -1174,7 +1174,7 @@ class ChooseAddonsToUpdateList(QListWidget):
self, self,
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
updated_addons: List[UpdateInfo], updated_addons: list[UpdateInfo],
) -> None: ) -> None:
QListWidget.__init__(self, parent) QListWidget.__init__(self, parent)
self.mgr = mgr self.mgr = mgr
@ -1266,7 +1266,7 @@ class ChooseAddonsToUpdateList(QListWidget):
return return
self.check_item(self.header_item, Qt.Checked) 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 = [] addon_ids = []
for i in range(1, self.count()): for i in range(1, self.count()):
item = self.item(i) item = self.item(i)
@ -1286,7 +1286,7 @@ class ChooseAddonsToUpdateList(QListWidget):
class ChooseAddonsToUpdateDialog(QDialog): class ChooseAddonsToUpdateDialog(QDialog):
def __init__( def __init__(
self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo] self, parent: QWidget, mgr: AddonManager, updated_addons: list[UpdateInfo]
) -> None: ) -> None:
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setWindowTitle(tr.addons_choose_update_window_title()) self.setWindowTitle(tr.addons_choose_update_window_title())
@ -1312,7 +1312,7 @@ class ChooseAddonsToUpdateDialog(QDialog):
layout.addWidget(button_box) layout.addWidget(button_box)
self.setLayout(layout) self.setLayout(layout)
def ask(self) -> List[int]: def ask(self) -> list[int]:
"Returns a list of selected addons' ids" "Returns a list of selected addons' ids"
ret = self.exec_() ret = self.exec_()
saveGeom(self, "addonsChooseUpdate") saveGeom(self, "addonsChooseUpdate")
@ -1323,9 +1323,9 @@ class ChooseAddonsToUpdateDialog(QDialog):
return [] 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.""" """Fetch update info from AnkiWeb in one or more batches."""
all_info: List[Dict] = [] all_info: list[dict] = []
while ids: while ids:
# get another chunk # get another chunk
@ -1340,7 +1340,7 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
def _fetch_update_info_batch( def _fetch_update_info_batch(
client: HttpClient, chunk: Iterable[str] client: HttpClient, chunk: Iterable[str]
) -> Iterable[Dict]: ) -> Iterable[dict]:
"""Get update info from AnkiWeb. """Get update info from AnkiWeb.
Chunk must not contain more than 25 ids.""" Chunk must not contain more than 25 ids."""
@ -1354,21 +1354,21 @@ def _fetch_update_info_batch(
def check_and_prompt_for_updates( def check_and_prompt_for_updates(
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
on_done: Callable[[List[DownloadLogEntry]], None], on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True, requested_by_user: bool = True,
) -> None: ) -> 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) handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
check_for_updates(mgr, on_updates_received) check_for_updates(mgr, on_updates_received)
def check_for_updates( def check_for_updates(
mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None] mgr: AddonManager, on_done: Callable[[HttpClient, list[dict]], None]
) -> None: ) -> None:
client = HttpClient() client = HttpClient()
def check() -> List[Dict]: def check() -> list[dict]:
return fetch_update_info(client, mgr.ankiweb_addons()) return fetch_update_info(client, mgr.ankiweb_addons())
def update_info_received(future: Future) -> None: def update_info_received(future: Future) -> None:
@ -1395,7 +1395,7 @@ def check_for_updates(
def extract_update_info( 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: ) -> UpdateInfo:
"Process branches to determine the updated mod time and min/max versions." "Process branches to determine the updated mod time and min/max versions."
branches = info_json["branches"] branches = info_json["branches"]
@ -1425,8 +1425,8 @@ def handle_update_info(
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
client: HttpClient, client: HttpClient,
items: List[Dict], items: list[dict],
on_done: Callable[[List[DownloadLogEntry]], None], on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True, requested_by_user: bool = True,
) -> None: ) -> None:
update_info = mgr.extract_update_info(items) update_info = mgr.extract_update_info(items)
@ -1445,8 +1445,8 @@ def prompt_to_update(
parent: QWidget, parent: QWidget,
mgr: AddonManager, mgr: AddonManager,
client: HttpClient, client: HttpClient,
updated_addons: List[UpdateInfo], updated_addons: list[UpdateInfo],
on_done: Callable[[List[DownloadLogEntry]], None], on_done: Callable[[list[DownloadLogEntry]], None],
requested_by_user: bool = True, requested_by_user: bool = True,
) -> None: ) -> None:
if not requested_by_user: if not requested_by_user:
@ -1468,7 +1468,7 @@ def prompt_to_update(
class ConfigEditor(QDialog): 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) super().__init__(dlg)
self.addon = addon self.addon = addon
self.conf = conf self.conf = conf
@ -1509,7 +1509,7 @@ class ConfigEditor(QDialog):
else: else:
self.form.scrollArea.setVisible(False) self.form.scrollArea.setVisible(False)
def updateText(self, conf: Dict[str, Any]) -> None: def updateText(self, conf: dict[str, Any]) -> None:
text = json.dumps( text = json.dumps(
conf, conf,
ensure_ascii=False, ensure_ascii=False,
@ -1584,8 +1584,8 @@ class ConfigEditor(QDialog):
def installAddonPackages( def installAddonPackages(
addonsManager: AddonManager, addonsManager: AddonManager,
paths: List[str], paths: list[str],
parent: Optional[QWidget] = None, parent: QWidget | None = None,
warn: bool = False, warn: bool = False,
strictly_modal: bool = False, strictly_modal: bool = False,
advise_restart: bool = False, advise_restart: bool = False,

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable, Optional, Sequence, Tuple, Union from typing import Callable, Sequence
import aqt import aqt
import aqt.forms import aqt.forms
@ -89,14 +89,14 @@ class MockModel:
class Browser(QMainWindow): class Browser(QMainWindow):
mw: AnkiQt mw: AnkiQt
col: Collection col: Collection
editor: Optional[Editor] editor: Editor | None
table: Table table: Table
def __init__( def __init__(
self, self,
mw: AnkiQt, mw: AnkiQt,
card: Optional[Card] = None, card: Card | None = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None, search: tuple[str | SearchNode] | None = None,
) -> None: ) -> None:
""" """
card -- try to select the provided card after executing "search" or card -- try to select the provided card after executing "search" or
@ -108,8 +108,8 @@ class Browser(QMainWindow):
self.mw = mw self.mw = mw
self.col = self.mw.col self.col = self.mw.col
self.lastFilter = "" self.lastFilter = ""
self.focusTo: Optional[int] = None self.focusTo: int | None = None
self._previewer: Optional[Previewer] = None self._previewer: Previewer | None = None
self._closeEventHasCleanedUp = False self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog() self.form = aqt.forms.browser.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
@ -119,8 +119,8 @@ class Browser(QMainWindow):
restoreSplitter(self.form.splitter, "editor3") restoreSplitter(self.form.splitter, "editor3")
self.form.splitter.setChildrenCollapsible(False) self.form.splitter.setChildrenCollapsible(False)
# set if exactly 1 row is selected; used by the previewer # set if exactly 1 row is selected; used by the previewer
self.card: Optional[Card] = None self.card: Card | None = None
self.current_card: Optional[Card] = None self.current_card: Card | None = None
self.setup_table() self.setup_table()
self.setupMenus() self.setupMenus()
self.setupHooks() self.setupHooks()
@ -134,7 +134,7 @@ class Browser(QMainWindow):
self.setupSearch(card, search) self.setupSearch(card, search)
def on_operation_did_execute( def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object] self, changes: OpChanges, handler: object | None
) -> None: ) -> None:
focused = current_window() == self focused = current_window() == self
self.table.op_executed(changes, handler, focused) self.table.op_executed(changes, handler, focused)
@ -161,7 +161,7 @@ class Browser(QMainWindow):
if changes.note_text or changes.card: if changes.note_text or changes.card:
self._renderPreview() 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: if current_window() == self:
self.setUpdatesEnabled(True) self.setUpdatesEnabled(True)
self.table.redraw_cells() self.table.redraw_cells()
@ -263,8 +263,8 @@ class Browser(QMainWindow):
def reopen( def reopen(
self, self,
_mw: AnkiQt, _mw: AnkiQt,
card: Optional[Card] = None, card: Card | None = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None, search: tuple[str | SearchNode] | None = None,
) -> None: ) -> None:
if search is not None: if search is not None:
self.search_for_terms(*search) self.search_for_terms(*search)
@ -281,8 +281,8 @@ class Browser(QMainWindow):
def setupSearch( def setupSearch(
self, self,
card: Optional[Card] = None, card: Card | None = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None, search: tuple[str | SearchNode] | None = None,
) -> None: ) -> None:
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None) self.form.searchEdit.setCompleter(None)
@ -310,7 +310,7 @@ class Browser(QMainWindow):
self.search_for(normed) self.search_for(normed)
self.update_history() 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 """Keep track of search string so that we reuse identical search when
refreshing, rather than whatever is currently in the search field. refreshing, rather than whatever is currently in the search field.
Optionally set the search bar to a different text than the actual search. 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)) 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) search = self.col.build_search_string(*search_terms)
self.form.searchEdit.setEditText(search) self.form.searchEdit.setEditText(search)
self.onSearchActivated() 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) default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT)
if default.strip(): if default.strip():
search = default search = default
@ -674,7 +674,7 @@ class Browser(QMainWindow):
@ensure_editor_saved @ensure_editor_saved
def add_tags_to_selected_notes( def add_tags_to_selected_notes(
self, self,
tags: Optional[str] = None, tags: str | None = None,
) -> None: ) -> None:
"Shows prompt if tags not provided." "Shows prompt if tags not provided."
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())): 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 @no_arg_trigger
@skip_if_selection_is_empty @skip_if_selection_is_empty
@ensure_editor_saved @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." "Shows prompt if tags not provided."
if not ( if not (
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete()) 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 parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
).run_in_background(initiator=self) ).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) (tags, ok) = getTag(self, self.col, prompt)
if not ok: if not ok:
return None return None

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional, Sequence from typing import Sequence
import aqt import aqt
from anki.notes import NoteId from anki.notes import NoteId
@ -39,7 +39,7 @@ class FindAndReplaceDialog(QDialog):
*, *,
mw: AnkiQt, mw: AnkiQt,
note_ids: Sequence[NoteId], note_ids: Sequence[NoteId],
field: Optional[str] = None, field: str | None = None,
) -> None: ) -> None:
""" """
If 'field' is passed, only this is added to the field selector. If 'field' is passed, only this is added to the field selector.
@ -48,7 +48,7 @@ class FindAndReplaceDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.mw = mw self.mw = mw
self.note_ids = note_ids self.note_ids = note_ids
self.field_names: List[str] = [] self.field_names: list[str] = []
self._field = field self._field = field
if field: if field:

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
import html import html
from typing import Any, List, Optional, Tuple from typing import Any
import anki import anki
import anki.find import anki.find
@ -45,7 +45,7 @@ class FindDuplicatesDialog(QDialog):
) )
form.fields.addItems(fields) form.fields.addItems(fields)
restore_combo_index_for_session(form.fields, fields, "findDupesFields") restore_combo_index_for_session(form.fields, fields, "findDupesFields")
self._dupesButton: Optional[QPushButton] = None self._dupesButton: QPushButton | None = None
# links # links
form.webView.set_title("find duplicates") form.webView.set_title("find duplicates")
@ -75,7 +75,7 @@ class FindDuplicatesDialog(QDialog):
qconnect(search.clicked, on_click) qconnect(search.clicked, on_click)
self.show() 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: if not self._dupesButton:
self._dupesButton = b = self.form.buttonBox.addButton( self._dupesButton = b = self.form.buttonBox.addButton(
tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole
@ -104,7 +104,7 @@ class FindDuplicatesDialog(QDialog):
text += "</ol>" text += "</ol>"
self.form.webView.stdHtml(text, context=self) 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: if not dupes:
return return

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import json import json
import re import re
import time import time
from typing import Any, Callable, Optional, Tuple, Union from typing import Any, Callable
import aqt.browser import aqt.browser
from anki.cards import Card 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.utils import disable_help_button, restoreGeom, saveGeom, tr
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
LastStateAndMod = Tuple[str, int, int] LastStateAndMod = tuple[str, int, int]
class Previewer(QDialog): class Previewer(QDialog):
_last_state: Optional[LastStateAndMod] = None _last_state: LastStateAndMod | None = None
_card_changed = False _card_changed = False
_last_render: Union[int, float] = 0 _last_render: int | float = 0
_timer: Optional[QTimer] = None _timer: QTimer | None = None
_show_both_sides = False _show_both_sides = False
def __init__( def __init__(
@ -57,7 +57,7 @@ class Previewer(QDialog):
disable_help_button(self) disable_help_button(self)
self.setWindowIcon(icon) self.setWindowIcon(icon)
def card(self) -> Optional[Card]: def card(self) -> Card | None:
raise NotImplementedError raise NotImplementedError
def card_changed(self) -> bool: def card_changed(self) -> bool:
@ -143,7 +143,7 @@ class Previewer(QDialog):
if cmd.startswith("play:"): if cmd.startswith("play:"):
play_clicked_audio(cmd, self.card()) 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: if card:
flag = card.user_flag() flag = card.user_flag()
marked = card.note(reload=True).has_tag(MARKED_TAG) marked = card.note(reload=True).has_tag(MARKED_TAG)
@ -247,7 +247,7 @@ class Previewer(QDialog):
self._state = "question" self._state = "question"
self.render_card() self.render_card()
def _state_and_mod(self) -> Tuple[str, int, int]: def _state_and_mod(self) -> tuple[str, int, int]:
c = self.card() c = self.card()
n = c.note() n = c.note()
n.load() n.load()
@ -258,7 +258,7 @@ class Previewer(QDialog):
class MultiCardPreviewer(Previewer): class MultiCardPreviewer(Previewer):
def card(self) -> Optional[Card]: def card(self) -> Card | None:
# need to state explicitly it's not implement to avoid W0223 # need to state explicitly it's not implement to avoid W0223
raise NotImplementedError raise NotImplementedError
@ -321,14 +321,14 @@ class MultiCardPreviewer(Previewer):
class BrowserPreviewer(MultiCardPreviewer): class BrowserPreviewer(MultiCardPreviewer):
_last_card_id = 0 _last_card_id = 0
_parent: Optional[aqt.browser.Browser] _parent: aqt.browser.Browser | None
def __init__( def __init__(
self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None] self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None]
) -> None: ) -> None:
super().__init__(parent=parent, mw=mw, on_close=on_close) super().__init__(parent=parent, mw=mw, on_close=on_close)
def card(self) -> Optional[Card]: def card(self) -> Card | None:
if self._parent.singleCard: if self._parent.singleCard:
return self._parent.card return self._parent.card
else: else:

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Callable, Iterable, List, Optional, Union from typing import Callable, Iterable
from anki.collection import SearchNode from anki.collection import SearchNode
from aqt.theme import ColoredIcon from aqt.theme import ColoredIcon
@ -59,8 +59,8 @@ class SidebarItem:
def __init__( def __init__(
self, self,
name: str, name: str,
icon: Union[str, ColoredIcon], icon: str | ColoredIcon,
search_node: Optional[SearchNode] = None, search_node: SearchNode | None = None,
on_expanded: Callable[[bool], None] = None, on_expanded: Callable[[bool], None] = None,
expanded: bool = False, expanded: bool = False,
item_type: SidebarItemType = SidebarItemType.CUSTOM, item_type: SidebarItemType = SidebarItemType.CUSTOM,
@ -75,24 +75,24 @@ class SidebarItem:
self.id = id self.id = id
self.search_node = search_node self.search_node = search_node
self.on_expanded = on_expanded self.on_expanded = on_expanded
self.children: List["SidebarItem"] = [] self.children: list[SidebarItem] = []
self.tooltip: Optional[str] = None self.tooltip: str | None = None
self._parent_item: Optional["SidebarItem"] = None self._parent_item: SidebarItem | None = None
self._expanded = expanded 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_self = False
self._search_matches_child = False self._search_matches_child = False
def add_child(self, cb: "SidebarItem") -> None: def add_child(self, cb: SidebarItem) -> None:
self.children.append(cb) self.children.append(cb)
cb._parent_item = self cb._parent_item = self
def add_simple( def add_simple(
self, self,
name: str, name: str,
icon: Union[str, ColoredIcon], icon: str | ColoredIcon,
type: SidebarItemType, type: SidebarItemType,
search_node: Optional[SearchNode], search_node: SearchNode | None,
) -> SidebarItem: ) -> SidebarItem:
"Add child sidebar item, and return it." "Add child sidebar item, and return it."
item = SidebarItem( item = SidebarItem(

View file

@ -4,7 +4,6 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Callable, Tuple
import aqt import aqt
from aqt.qt import * from aqt.qt import *
@ -18,7 +17,7 @@ class SidebarTool(Enum):
class SidebarToolbar(QToolBar): 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.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select), (SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
) )

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import Dict, Iterable, List, Optional, Tuple, cast from typing import Iterable, cast
import aqt import aqt
from anki.collection import ( from anki.collection import (
@ -75,8 +75,8 @@ class SidebarTreeView(QTreeView):
self.browser = browser self.browser = browser
self.mw = browser.mw self.mw = browser.mw
self.col = self.mw.col self.col = self.mw.col
self.current_search: Optional[str] = None self.current_search: str | None = None
self.valid_drop_types: Tuple[SidebarItemType, ...] = () self.valid_drop_types: tuple[SidebarItemType, ...] = ()
self._refresh_needed = False self._refresh_needed = False
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
@ -140,7 +140,7 @@ class SidebarTreeView(QTreeView):
########################### ###########################
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> None: ) -> None:
if changes.browser_sidebar and not handler is self: if changes.browser_sidebar and not handler is self:
self._refresh_needed = True self._refresh_needed = True
@ -198,9 +198,9 @@ class SidebarTreeView(QTreeView):
def find_item( def find_item(
self, self,
is_target: Callable[[SidebarItem], bool], is_target: Callable[[SidebarItem], bool],
parent: Optional[SidebarItem] = None, parent: SidebarItem | None = None,
) -> Optional[SidebarItem]: ) -> SidebarItem | None:
def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]: def find_item_rec(parent: SidebarItem) -> SidebarItem | None:
if is_target(parent): if is_target(parent):
return parent return parent
for child in parent.children: for child in parent.children:
@ -226,7 +226,7 @@ class SidebarTreeView(QTreeView):
def _expand_where_necessary( def _expand_where_necessary(
self, self,
model: SidebarModel, model: SidebarModel,
parent: Optional[QModelIndex] = None, parent: QModelIndex | None = None,
searching: bool = False, searching: bool = False,
) -> None: ) -> None:
scroll_to_first_match = searching scroll_to_first_match = searching
@ -348,7 +348,7 @@ class SidebarTreeView(QTreeView):
self.valid_drop_types = tuple(valid_drop_types) 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): if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
return self._handle_drag_drop_decks(sources, target) return self._handle_drag_drop_decks(sources, target)
if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT): if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT):
@ -361,7 +361,7 @@ class SidebarTreeView(QTreeView):
return False return False
def _handle_drag_drop_decks( def _handle_drag_drop_decks(
self, sources: List[SidebarItem], target: SidebarItem self, sources: list[SidebarItem], target: SidebarItem
) -> bool: ) -> bool:
deck_ids = [ deck_ids = [
DeckId(source.id) DeckId(source.id)
@ -380,7 +380,7 @@ class SidebarTreeView(QTreeView):
return True return True
def _handle_drag_drop_tags( def _handle_drag_drop_tags(
self, sources: List[SidebarItem], target: SidebarItem self, sources: list[SidebarItem], target: SidebarItem
) -> bool: ) -> bool:
tags = [ tags = [
source.full_name source.full_name
@ -402,7 +402,7 @@ class SidebarTreeView(QTreeView):
return True return True
def _handle_drag_drop_saved_search( def _handle_drag_drop_saved_search(
self, sources: List[SidebarItem], _target: SidebarItem self, sources: list[SidebarItem], _target: SidebarItem
) -> bool: ) -> bool:
if len(sources) != 1 or sources[0].search_node is None: if len(sources) != 1 or sources[0].search_node is None:
return False return False
@ -464,7 +464,7 @@ class SidebarTreeView(QTreeView):
########################### ###########################
def _root_tree(self) -> SidebarItem: def _root_tree(self) -> SidebarItem:
root: Optional[SidebarItem] = None root: SidebarItem | None = None
for stage in SidebarStage: for stage in SidebarStage:
if stage == SidebarStage.ROOT: if stage == SidebarStage.ROOT:
@ -504,7 +504,7 @@ class SidebarTreeView(QTreeView):
name: str, name: str,
icon: Union[str, ColoredIcon], icon: Union[str, ColoredIcon],
collapse_key: Config.Bool.V, collapse_key: Config.Bool.V,
type: Optional[SidebarItemType] = None, type: SidebarItemType | None = None,
) -> SidebarItem: ) -> SidebarItem:
def update(expanded: bool) -> None: def update(expanded: bool) -> None:
CollectionOp( CollectionOp(
@ -1112,13 +1112,13 @@ class SidebarTreeView(QTreeView):
_saved_searches_key = "savedFilters" _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, {}) 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) self.col.set_config(self._saved_searches_key, searches)
def _get_current_search(self) -> Optional[str]: def _get_current_search(self) -> str | None:
try: try:
return self.col.build_search_string(self.browser.current_search()) return self.col.build_search_string(self.browser.current_search())
except Exception as e: except Exception as e:
@ -1198,24 +1198,24 @@ class SidebarTreeView(QTreeView):
# Helpers # 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()] 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 [ return [
DeckId(item.id) DeckId(item.id)
for item in self._selected_items() for item in self._selected_items()
if item.item_type == SidebarItemType.DECK if item.item_type == SidebarItemType.DECK
] ]
def _selected_saved_searches(self) -> List[str]: def _selected_saved_searches(self) -> list[str]:
return [ return [
item.name item.name
for item in self._selected_items() for item in self._selected_items()
if item.item_type == SidebarItemType.SAVED_SEARCH if item.item_type == SidebarItemType.SAVED_SEARCH
] ]
def _selected_tags(self) -> List[str]: def _selected_tags(self) -> list[str]:
return [ return [
item.full_name item.full_name
for item in self._selected_items() for item in self._selected_items()

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Generator, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING, Generator, Sequence, Union
import aqt import aqt
from anki.cards import CardId from anki.cards import CardId
@ -24,10 +23,10 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]]
class SearchContext: class SearchContext:
search: str search: str
browser: aqt.browser.Browser browser: aqt.browser.Browser
order: Union[bool, str, Column] = True order: bool | str | Column = True
reverse: bool = False reverse: bool = False
# if set, provided ids will be used instead of the regular search # if set, provided ids will be used instead of the regular search
ids: Optional[Sequence[ItemId]] = None ids: Sequence[ItemId] | None = None
@dataclass @dataclass
@ -41,14 +40,14 @@ class CellRow:
def __init__( def __init__(
self, self,
cells: Generator[Tuple[str, bool], None, None], cells: Generator[tuple[str, bool], None, None],
color: BrowserRow.Color.V, color: BrowserRow.Color.V,
font_name: str, font_name: str,
font_size: int, font_size: int,
) -> None: ) -> None:
self.refreshed_at: float = time.time() self.refreshed_at: float = time.time()
self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells) self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color) self.color: tuple[str, str] | None = backend_color_to_aqt_color(color)
self.font_name: str = font_name or "arial" self.font_name: str = font_name or "arial"
self.font_size: int = font_size if font_size > 0 else 12 self.font_size: int = font_size if font_size > 0 else 12
@ -75,7 +74,7 @@ class CellRow:
return row 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: if color == BrowserRow.COLOR_MARKED:
return colors.MARKED_BG return colors.MARKED_BG
if color == BrowserRow.COLOR_SUSPENDED: if color == BrowserRow.COLOR_SUSPENDED:

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast from typing import Any, Callable, Sequence, cast
import aqt import aqt
from anki.cards import Card, CardId from anki.cards import Card, CardId
@ -41,13 +40,13 @@ class DataModel(QAbstractTableModel):
) -> None: ) -> None:
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self.col: Collection = col self.col: Collection = col
self.columns: Dict[str, Column] = dict( self.columns: dict[str, Column] = {
((c.key, c) for c in self.col.all_browser_columns()) c.key: c for c in self.col.all_browser_columns()
) }
gui_hooks.browser_did_fetch_columns(self.columns) gui_hooks.browser_did_fetch_columns(self.columns)
self._state: ItemState = state self._state: ItemState = state
self._items: Sequence[ItemId] = [] self._items: Sequence[ItemId] = []
self._rows: Dict[int, CellRow] = {} self._rows: dict[int, CellRow] = {}
self._block_updates = False self._block_updates = False
self._stale_cutoff = 0.0 self._stale_cutoff = 0.0
self._on_row_state_will_change = row_state_will_change_callback 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) return self._fetch_row_and_update_cache(index, item, None)
def _fetch_row_and_update_cache( 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: ) -> CellRow:
"""Fetch a row from the backend, add it to the cache and return it. """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. Then fire callbacks if the row is being deleted or restored.
@ -119,7 +118,7 @@ class DataModel(QAbstractTableModel):
) )
return row 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.""" """Get row if it is cached, regardless of staleness."""
return self._rows.get(self.get_item(index)) return self._rows.get(self.get_item(index))
@ -175,41 +174,41 @@ class DataModel(QAbstractTableModel):
def get_item(self, index: QModelIndex) -> ItemId: def get_item(self, index: QModelIndex) -> ItemId:
return self._items[index.row()] 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] 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)) 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)) 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)]): if nid_list := self._state.get_note_ids([self.get_item(index)]):
return nid_list[0] return nid_list[0]
return None return None
# Get row numbers from items # 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): for row, i in enumerate(self._items):
if i == item: if i == item:
return row return row
return None return None
def get_item_rows(self, items: Sequence[ItemId]) -> List[int]: def get_item_rows(self, items: Sequence[ItemId]) -> list[int]:
rows = [] rows = []
for row, i in enumerate(self._items): for row, i in enumerate(self._items):
if i in items: if i in items:
rows.append(row) rows.append(row)
return rows 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)) return self.get_item_row(self._state.get_item_from_card_id(card_id))
# Get objects (cards or notes) # 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.""" """Try to return the indicated, possibly deleted card."""
if not index.isValid(): if not index.isValid():
return None return None
@ -218,7 +217,7 @@ class DataModel(QAbstractTableModel):
except NotFoundError: except NotFoundError:
return None 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.""" """Try to return the indicated, possibly deleted note."""
if not index.isValid(): if not index.isValid():
return None return None
@ -280,7 +279,7 @@ class DataModel(QAbstractTableModel):
self.columns[key] = addon_column_fillin(key) self.columns[key] = addon_column_fillin(key)
return self.columns[key] return self.columns[key]
def active_column_index(self, column: str) -> Optional[int]: def active_column_index(self, column: str) -> int | None:
return ( return (
self._state.active_columns.index(column) self._state.active_columns.index(column)
if column in self._state.active_columns if column in self._state.active_columns
@ -317,7 +316,7 @@ class DataModel(QAbstractTableModel):
qfont.setPixelSize(row.font_size) qfont.setPixelSize(row.font_size)
return qfont return qfont
elif role == Qt.TextAlignmentRole: 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: if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
align |= Qt.AlignHCenter align |= Qt.AlignHCenter
return align return align
@ -329,7 +328,7 @@ class DataModel(QAbstractTableModel):
def headerData( def headerData(
self, section: int, orientation: Qt.Orientation, role: int = 0 self, section: int, orientation: Qt.Orientation, role: int = 0
) -> Optional[str]: ) -> str | None:
if orientation == Qt.Horizontal and role == Qt.DisplayRole: if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._state.column_label(self.column_at_section(section)) return self._state.column_label(self.column_at_section(section))
return None return None

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod, abstractproperty 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.browser import BrowserConfig
from anki.cards import Card, CardId from anki.cards import Card, CardId
@ -18,7 +17,7 @@ class ItemState(ABC):
GEOMETRY_KEY_PREFIX: str GEOMETRY_KEY_PREFIX: str
SORT_COLUMN_KEY: str SORT_COLUMN_KEY: str
SORT_BACKWARDS_KEY: str SORT_BACKWARDS_KEY: str
_active_columns: List[str] _active_columns: list[str]
def __init__(self, col: Collection) -> None: def __init__(self, col: Collection) -> None:
self.col = col self.col = col
@ -57,7 +56,7 @@ class ItemState(ABC):
# abstractproperty is deprecated but used due to mypy limitations # abstractproperty is deprecated but used due to mypy limitations
# (https://github.com/python/mypy/issues/1362) # (https://github.com/python/mypy/issues/1362)
@abstractproperty # pylint: disable=deprecated-decorator @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.""" """Return the saved or default columns for the state."""
@abstractmethod @abstractmethod
@ -96,7 +95,7 @@ class ItemState(ABC):
@abstractmethod @abstractmethod
def find_items( def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]: ) -> Sequence[ItemId]:
"""Return the item ids fitting the given search and order.""" """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() self._active_columns = self.col.load_browser_card_columns()
@property @property
def active_columns(self) -> List[str]: def active_columns(self) -> list[str]:
return self._active_columns return self._active_columns
def toggle_active_column(self, column: str) -> None: def toggle_active_column(self, column: str) -> None:
@ -150,7 +149,7 @@ class CardState(ItemState):
return self.get_card(item).note() return self.get_card(item).note()
def find_items( def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]: ) -> Sequence[ItemId]:
return self.col.find_cards(search, order, reverse) return self.col.find_cards(search, order, reverse)
@ -180,7 +179,7 @@ class NoteState(ItemState):
self._active_columns = self.col.load_browser_note_columns() self._active_columns = self.col.load_browser_note_columns()
@property @property
def active_columns(self) -> List[str]: def active_columns(self) -> list[str]:
return self._active_columns return self._active_columns
def toggle_active_column(self, column: str) -> None: def toggle_active_column(self, column: str) -> None:
@ -197,7 +196,7 @@ class NoteState(ItemState):
return self.col.get_note(NoteId(item)) return self.col.get_note(NoteId(item))
def find_items( def find_items(
self, search: str, order: Union[bool, str, Column], reverse: bool self, search: str, order: bool | str | Column, reverse: bool
) -> Sequence[ItemId]: ) -> Sequence[ItemId]:
return self.col.find_notes(search, order, reverse) return self.col.find_notes(search, order, reverse)

View file

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations 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
import aqt.forms import aqt.forms
@ -45,12 +44,12 @@ class Table:
self._on_row_state_will_change, self._on_row_state_will_change,
self._on_row_state_changed, self._on_row_state_changed,
) )
self._view: Optional[QTableView] = None self._view: QTableView | None = None
# cached for performance # cached for performance
self._len_selection = 0 self._len_selection = 0
self._selected_rows: Optional[List[QModelIndex]] = None self._selected_rows: list[QModelIndex] | None = None
# temporarily set for selection preservation # temporarily set for selection preservation
self._current_item: Optional[ItemId] = None self._current_item: ItemId | None = None
self._selected_items: Sequence[ItemId] = [] self._selected_items: Sequence[ItemId] = []
def set_view(self, view: QTableView) -> None: def set_view(self, view: QTableView) -> None:
@ -89,13 +88,13 @@ class Table:
# Get objects # Get objects
def get_current_card(self) -> Optional[Card]: def get_current_card(self) -> Card | None:
return self._model.get_card(self._current()) 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()) 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. """If there is only one row selected return its card, else None.
This may be a different one than the current card.""" This may be a different one than the current card."""
if self.len_selection() != 1: if self.len_selection() != 1:
@ -171,7 +170,7 @@ class Table:
self._model.redraw_cells() self._model.redraw_cells()
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> None: ) -> None:
if changes.browser_table: if changes.browser_table:
self._model.mark_cache_stale() self._model.mark_cache_stale()
@ -260,7 +259,7 @@ class Table:
def _current(self) -> QModelIndex: def _current(self) -> QModelIndex:
return self._view.selectionModel().currentIndex() return self._view.selectionModel().currentIndex()
def _selected(self) -> List[QModelIndex]: def _selected(self) -> list[QModelIndex]:
if self._selected_rows is None: if self._selected_rows is None:
self._selected_rows = self._view.selectionModel().selectedRows() self._selected_rows = self._view.selectionModel().selectedRows()
return self._selected_rows return self._selected_rows
@ -280,7 +279,7 @@ class Table:
self._len_selection = 0 self._len_selection = 0
self._selected_rows = None self._selected_rows = None
def _select_rows(self, rows: List[int]) -> None: def _select_rows(self, rows: list[int]) -> None:
selection = QItemSelection() selection = QItemSelection()
for row in rows: for row in rows:
selection.select( selection.select(
@ -532,9 +531,7 @@ class Table:
self._selected_items = [] self._selected_items = []
self._current_item = None self._current_item = None
def _qualify_selected_rows( def _qualify_selected_rows(self, rows: list[int], current: int | None) -> list[int]:
self, rows: List[int], current: Optional[int]
) -> List[int]:
"""Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current.""" """Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current."""
if rows: if rows:
if len(rows) < self.SELECTION_LIMIT: if len(rows) < self.SELECTION_LIMIT:
@ -544,7 +541,7 @@ class Table:
return rows[0:1] return rows[0:1]
return [current if current else 0] 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 """Return all rows of items that were in the saved selection and the row of the saved
current element if present. current element if present.
""" """
@ -554,7 +551,7 @@ class Table:
) )
return selected_rows, current_row 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 """Convert the items of the saved selection and current element to the new state and
return their rows. return their rows.
""" """

View file

@ -3,7 +3,7 @@
import json import json
import re import re
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Dict, List, Match, Optional from typing import Any, Match, Optional
import aqt import aqt
from anki.collection import OpChanges from anki.collection import OpChanges
@ -135,7 +135,7 @@ class CardLayout(QDialog):
combo.setEnabled(not self._isCloze()) combo.setEnabled(not self._isCloze())
self.ignore_change_signals = False self.ignore_change_signals = False
def _summarizedName(self, idx: int, tmpl: Dict) -> str: def _summarizedName(self, idx: int, tmpl: dict) -> str:
return "{}: {}: {} -> {}".format( return "{}: {}: {} -> {}".format(
idx + 1, idx + 1,
tmpl["name"], tmpl["name"],
@ -146,7 +146,7 @@ class CardLayout(QDialog):
def _fieldsOnTemplate(self, fmt: str) -> str: def _fieldsOnTemplate(self, fmt: str) -> str:
matches = re.findall("{{[^#/}]+?}}", fmt) matches = re.findall("{{[^#/}]+?}}", fmt)
chars_allowed = 30 chars_allowed = 30
field_names: List[str] = [] field_names: list[str] = []
for m in matches: for m in matches:
# strip off mustache # strip off mustache
m = re.sub(r"[{}]", "", m) m = re.sub(r"[{}]", "", m)
@ -440,7 +440,7 @@ class CardLayout(QDialog):
# Reading/writing question/answer/css # Reading/writing question/answer/css
########################################################################## ##########################################################################
def current_template(self) -> Dict: def current_template(self) -> dict:
if self._isCloze(): if self._isCloze():
return self.templates[0] return self.templates[0]
return self.templates[self.ord] return self.templates[self.ord]
@ -592,7 +592,7 @@ class CardLayout(QDialog):
self.mw.taskman.with_progress(get_count, on_done) 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) self.mm.remove_template(self.model, template)
# ensure current ordinal is within bounds # ensure current ordinal is within bounds
@ -668,7 +668,7 @@ class CardLayout(QDialog):
self._flipQA(old, old) self._flipQA(old, old)
self.redraw_everything() 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"]) m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
if not m: if not m:
showInfo(tr.card_templates_anki_couldnt_find_the_line_between()) 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 copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional from typing import Any
import aqt import aqt
from anki.collection import OpChanges from anki.collection import OpChanges
@ -79,7 +79,7 @@ class DeckBrowser:
self.refresh() self.refresh()
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> bool: ) -> bool:
if changes.study_queues and handler is not self: if changes.study_queues and handler is not self:
self._refresh_needed = True self._refresh_needed = True
@ -175,11 +175,11 @@ class DeckBrowser:
def _renderDeckTree(self, top: DeckTreeNode) -> str: def _renderDeckTree(self, top: DeckTreeNode) -> str:
buf = """ buf = """
<tr><th colspan=5 align=start>%s</th> <tr><th colspan=5 align=start>{}</th>
<th class=count>%s</th> <th class=count>{}</th>
<th class=count></th> <th class=count></th>
<th class=count>%s</th> <th class=count>{}</th>
<th class=optscol></th></tr>""" % ( <th class=optscol></th></tr>""".format(
tr.decks_deck(), tr.decks_deck(),
tr.actions_new(), tr.actions_new(),
tr.statistics_due_count(), tr.statistics_due_count(),

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from operator import itemgetter from operator import itemgetter
from typing import Any, Dict, List, Optional from typing import Any
from PyQt5.QtWidgets import QLineEdit from PyQt5.QtWidgets import QLineEdit
@ -30,7 +30,7 @@ from aqt.utils import (
class DeckConf(QDialog): 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) QDialog.__init__(self, mw)
self.mw = mw self.mw = mw
self.deck = deck self.deck = deck
@ -74,7 +74,7 @@ class DeckConf(QDialog):
def setupConfs(self) -> None: def setupConfs(self) -> None:
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange) qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
self.conf: Optional[DeckConfigDict] = None self.conf: DeckConfigDict | None = None
self.loadConfs() self.loadConfs()
def loadConfs(self) -> None: def loadConfs(self) -> None:
@ -175,7 +175,7 @@ class DeckConf(QDialog):
# Loading # 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: def num_to_user(n: Union[int, float]) -> str:
if n == round(n): if n == round(n):
return str(int(n)) return str(int(n))

View file

@ -3,8 +3,6 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Optional
import aqt import aqt
import aqt.deckconf import aqt.deckconf
from anki.cards import Card from anki.cards import Card
@ -67,7 +65,7 @@ class DeckOptionsDialog(QDialog):
QDialog.reject(self) 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()] decks = [aqt.mw.col.decks.current()]
if card := active_card: if card := active_card:
if card.odid and card.odid != decks[0]["id"]: 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) _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 = QDialog(aqt.mw.app.activeWindow())
diag.setWindowTitle("Anki") diag.setWindowTitle("Anki")
box = QVBoxLayout() box = QVBoxLayout()

View file

@ -14,7 +14,7 @@ import urllib.parse
import urllib.request import urllib.request
import warnings import warnings
from random import randrange 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 bs4
import requests import requests
@ -104,14 +104,14 @@ class Editor:
self.mw = mw self.mw = mw
self.widget = widget self.widget = widget
self.parentWindow = parentWindow self.parentWindow = parentWindow
self.note: Optional[Note] = None self.note: Note | None = None
self.addMode = addMode 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 # Similar to currentField, but not set to None on a blur. May be
# outside the bounds of the current notetype. # 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 # current card, for card layout
self.card: Optional[Card] = None self.card: Card | None = None
self.setupOuter() self.setupOuter()
self.setupWeb() self.setupWeb()
self.setupShortcuts() self.setupShortcuts()
@ -147,7 +147,7 @@ class Editor:
default_css=True, default_css=True,
) )
lefttopbtns: List[str] = [] lefttopbtns: list[str] = []
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
lefttopbtns_defs = [ lefttopbtns_defs = [
@ -156,7 +156,7 @@ class Editor:
] ]
lefttopbtns_js = "\n".join(lefttopbtns_defs) lefttopbtns_js = "\n".join(lefttopbtns_defs)
righttopbtns: List[str] = [] righttopbtns: list[str] = []
gui_hooks.editor_did_init_buttons(righttopbtns, self) gui_hooks.editor_did_init_buttons(righttopbtns, self)
# legacy filter # legacy filter
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
@ -191,9 +191,9 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def addButton( def addButton(
self, self,
icon: Optional[str], icon: str | None,
cmd: str, cmd: str,
func: Callable[["Editor"], None], func: Callable[[Editor], None],
tip: str = "", tip: str = "",
label: str = "", label: str = "",
id: str = None, id: str = None,
@ -242,11 +242,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def _addButton( def _addButton(
self, self,
icon: Optional[str], icon: str | None,
cmd: str, cmd: str,
tip: str = "", tip: str = "",
label: str = "", label: str = "",
id: Optional[str] = None, id: str | None = None,
toggleable: bool = False, toggleable: bool = False,
disables: bool = True, disables: bool = True,
rightside: bool = True, rightside: bool = True,
@ -302,7 +302,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def setupShortcuts(self) -> None: def setupShortcuts(self) -> None:
# if a third element is provided, enable shortcut even when no field selected # 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) gui_hooks.editor_did_init_shortcuts(cuts, self)
for row in cuts: for row in cuts:
if len(row) == 2: if len(row) == 2:
@ -449,7 +449,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
###################################################################### ######################################################################
def set_note( 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: ) -> None:
"Make NOTE the current note." "Make NOTE the current note."
self.note = note self.note = note
@ -462,7 +462,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
def loadNoteKeepingFocus(self) -> None: def loadNoteKeepingFocus(self) -> None:
self.loadNote(self.currentField) self.loadNote(self.currentField)
def loadNote(self, focusTo: Optional[int] = None) -> None: def loadNote(self, focusTo: int | None = None) -> None:
if not self.note: if not self.note:
return return
@ -488,7 +488,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
text_color = self.mw.pm.profile.get("lastTextColor", "#00f") text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#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(data),
json.dumps(self.fonts()), json.dumps(self.fonts()),
json.dumps(focusTo), json.dumps(focusTo),
@ -510,7 +510,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
initiator=self initiator=self
) )
def fonts(self) -> List[Tuple[str, int, bool]]: def fonts(self) -> list[tuple[str, int, bool]]:
return [ return [
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) (gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
for f in self.note.note_type()["flds"] 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: if not self.note:
return True return True
m = self.note.note_type() m = self.note.note_type()
@ -700,7 +700,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# Media downloads # Media downloads
###################################################################### ######################################################################
def urlToLink(self, url: str) -> Optional[str]: def urlToLink(self, url: str) -> str | None:
fname = self.urlToFile(url) fname = self.urlToFile(url)
if not fname: if not fname:
return None return None
@ -715,7 +715,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
av_player.play_file(fname) av_player.play_file(fname)
return f"[sound:{html.escape(fname, quote=False)}]" 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() l = url.lower()
for suffix in pics + audio: for suffix in pics + audio:
if l.endswith(f".{suffix}"): if l.endswith(f".{suffix}"):
@ -760,7 +760,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
fname = f"paste-{csum}{ext}" fname = f"paste-{csum}{ext}"
return self._addMediaFromData(fname, data) 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." "Download file into media folder and return local filename or None."
# urllib doesn't understand percent-escaped utf8, but requires things like # urllib doesn't understand percent-escaped utf8, but requires things like
# '#' to be escaped. # '#' to be escaped.
@ -774,7 +774,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# fetch it into a temporary folder # fetch it into a temporary folder
self.mw.progress.start(immediate=not local, parent=self.parentWindow) self.mw.progress.start(immediate=not local, parent=self.parentWindow)
content_type = None content_type = None
error_msg: Optional[str] = None error_msg: str | None = None
try: try:
if local: if local:
req = urllib.request.Request( req = urllib.request.Request(
@ -972,7 +972,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
for name, val in list(self.note.items()): for name, val in list(self.note.items()):
m = re.findall(r"\{\{c(\d+)::", val) m = re.findall(r"\{\{c(\d+)::", val)
if m: 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? # reuse last?
if not KeyboardModifiersPressed().alt: if not KeyboardModifiersPressed().alt:
highest += 1 highest += 1
@ -1069,7 +1069,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
# Links from HTML # Links from HTML
###################################################################### ######################################################################
_links: Dict[str, Callable] = dict( _links: dict[str, Callable] = dict(
fields=onFields, fields=onFields,
cards=onCardLayout, cards=onCardLayout,
bold=toggleBold, bold=toggleBold,
@ -1163,7 +1163,7 @@ class EditorWebView(AnkiWebView):
# returns (html, isInternal) # returns (html, isInternal)
def _processMime( def _processMime(
self, mime: QMimeData, extended: bool = False, drop_event: bool = False 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" % ( # print("html=%s image=%s urls=%s txt=%s" % (
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText())) # mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()))
# print("html", mime.html()) # print("html", mime.html())
@ -1193,7 +1193,7 @@ class EditorWebView(AnkiWebView):
return html, True return html, True
return "", False 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(): if not mime.hasUrls():
return None return None
@ -1206,7 +1206,7 @@ class EditorWebView(AnkiWebView):
return buf 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(): if not mime.hasText():
return None return None
@ -1245,7 +1245,7 @@ class EditorWebView(AnkiWebView):
processed.pop() processed.pop()
return "".join(processed) 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(): if not mime.hasImage():
return None return None
im = QImage(mime.imageData()) im = QImage(mime.imageData())

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import re import re
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, List from typing import Any
import aqt import aqt
from anki.cards import CardId from anki.cards import CardId
@ -89,7 +89,7 @@ class EmptyCardsDialog(QDialog):
self.mw.taskman.run_in_background(delete, on_done) self.mw.taskman.run_in_background(delete, on_done)
def _delete_cards(self, keep_notes: bool) -> int: def _delete_cards(self, keep_notes: bool) -> int:
to_delete: List[CardId] = [] to_delete: list[CardId] = []
note: EmptyCardsReport.NoteWithEmptyCards note: EmptyCardsReport.NoteWithEmptyCards
for note in self.report.notes: for note in self.report.notes:
if keep_notes and note.will_delete_note: if keep_notes and note.will_delete_note:

View file

@ -7,7 +7,6 @@ import os
import re import re
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from typing import List, Optional
import aqt import aqt
from anki import hooks from anki import hooks
@ -29,21 +28,21 @@ class ExportDialog(QDialog):
def __init__( def __init__(
self, self,
mw: aqt.main.AnkiQt, mw: aqt.main.AnkiQt,
did: Optional[DeckId] = None, did: DeckId | None = None,
cids: Optional[List[CardId]] = None, cids: list[CardId] | None = None,
): ):
QDialog.__init__(self, mw, Qt.Window) QDialog.__init__(self, mw, Qt.Window)
self.mw = mw self.mw = mw
self.col = mw.col.weakref() self.col = mw.col.weakref()
self.frm = aqt.forms.exporting.Ui_ExportDialog() self.frm = aqt.forms.exporting.Ui_ExportDialog()
self.frm.setupUi(self) self.frm.setupUi(self)
self.exporter: Optional[Exporter] = None self.exporter: Exporter | None = None
self.cids = cids self.cids = cids
disable_help_button(self) disable_help_button(self)
self.setup(did) self.setup(did)
self.exec_() self.exec_()
def setup(self, did: Optional[DeckId]) -> None: def setup(self, did: DeckId | None) -> None:
self.exporters = exporters(self.col) self.exporters = exporters(self.col)
# if a deck specified, start with .apkg type selected # if a deck specified, start with .apkg type selected
idx = 0 idx = 0

View file

@ -1,6 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Optional
import aqt import aqt
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.consts import * from anki.consts import *

View file

@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 import aqt
from anki.collection import OpChangesWithId, SearchNode from anki.collection import OpChangesWithId, SearchNode
@ -36,8 +36,8 @@ class FilteredDeckConfigDialog(QDialog):
self, self,
mw: AnkiQt, mw: AnkiQt,
deck_id: DeckId = DeckId(0), deck_id: DeckId = DeckId(0),
search: Optional[str] = None, search: str | None = None,
search_2: Optional[str] = None, search_2: str | None = None,
) -> None: ) -> None:
"""If 'deck_id' is non-zero, load and modify its settings. """If 'deck_id' is non-zero, load and modify its settings.
Otherwise, build a new deck and derive settings from the current deck. Otherwise, build a new deck and derive settings from the current deck.
@ -162,15 +162,13 @@ class FilteredDeckConfigDialog(QDialog):
def reopen( def reopen(
self, self,
_mw: AnkiQt, _mw: AnkiQt,
search: Optional[str] = None, search: str | None = None,
search_2: Optional[str] = None, search_2: str | None = None,
_deck: Optional[DeckDict] = None, _deck: DeckDict | None = None,
) -> None: ) -> None:
self.set_custom_searches(search, search_2) self.set_custom_searches(search, search_2)
def set_custom_searches( def set_custom_searches(self, search: str | None, search_2: str | None) -> None:
self, search: Optional[str], search_2: Optional[str]
) -> None:
if search is not None: if search is not None:
self.form.search.setText(search) self.form.search.setText(search)
self.form.search.setFocus() self.form.search.setFocus()
@ -218,12 +216,12 @@ class FilteredDeckConfigDialog(QDialog):
else: else:
aqt.dialogs.open("Browser", self.mw, search=(search,)) 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(): if self.form.secondFilter.isChecked():
return (self.form.search_2.text(),) return (self.form.search_2.text(),)
return () 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. """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. 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 (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
return () 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 """Return a search node that matches cards in filtered decks, if applicable excluding those
in the deck being rebuild.""" in the deck being rebuild."""
if self.deck.id: if self.deck.id:
@ -320,12 +318,12 @@ class FilteredDeckConfigDialog(QDialog):
######################################################## ########################################################
# fixme: remove once we drop support for v1 # 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( return " ".join(
[str(int(val)) if int(val) == val else str(val) for val in values] [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(" ") items = str(line.text()).split(" ")
ret = [] ret = []
for item in items: for item in items:

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Optional, cast from typing import cast
import aqt import aqt
from anki.collection import SearchNode from anki.collection import SearchNode
@ -33,9 +33,9 @@ class Flag:
class FlagManager: class FlagManager:
def __init__(self, mw: aqt.main.AnkiQt) -> None: def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw 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.""" """Return a list of all flags."""
if self._flags is None: if self._flags is None:
self._load_flags() self._load_flags()
@ -55,7 +55,7 @@ class FlagManager:
gui_hooks.flag_label_did_change() gui_hooks.flag_label_did_change()
def _load_flags(self) -> None: 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) icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED)
self._flags = [ self._flags = [

View file

@ -8,7 +8,7 @@ import traceback
import unicodedata import unicodedata
import zipfile import zipfile
from concurrent.futures import Future from concurrent.futures import Future
from typing import Any, Dict, Optional from typing import Any, Optional
import anki.importing as importing import anki.importing as importing
import aqt.deckchooser import aqt.deckchooser
@ -34,7 +34,7 @@ from aqt.utils import (
class ChangeMap(QDialog): 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) QDialog.__init__(self, mw, Qt.Window)
self.mw = mw self.mw = mw
self.model = model self.model = model

View file

@ -5,7 +5,9 @@
Legacy support Legacy support
""" """
from typing import Any, List from __future__ import annotations
from typing import Any
import anki import anki
import aqt import aqt
@ -20,7 +22,7 @@ def bodyClass(col, card) -> str: # type: ignore
return theme_manager.body_classes_for_card_ord(card.ord) 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") print("allSounds() deprecated")
return aqt.mw.col.media._extract_filenames(text) return aqt.mw.col.media._extract_filenames(text)

View file

@ -13,19 +13,7 @@ import zipfile
from argparse import Namespace from argparse import Namespace
from concurrent.futures import Future from concurrent.futures import Future
from threading import Thread from threading import Thread
from typing import ( from typing import Any, Literal, Sequence, TextIO, TypeVar, cast
Any,
Callable,
Dict,
List,
Literal,
Optional,
Sequence,
TextIO,
Tuple,
TypeVar,
cast,
)
import anki import anki
import aqt import aqt
@ -105,13 +93,13 @@ class AnkiQt(QMainWindow):
profileManager: ProfileManagerType, profileManager: ProfileManagerType,
backend: _RustBackend, backend: _RustBackend,
opts: Namespace, opts: Namespace,
args: List[Any], args: list[Any],
) -> None: ) -> None:
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.backend = backend self.backend = backend
self.state: MainWindowState = "startup" self.state: MainWindowState = "startup"
self.opts = opts self.opts = opts
self.col: Optional[Collection] = None self.col: Collection | None = None
self.taskman = TaskManager(self) self.taskman = TaskManager(self)
self.media_syncer = MediaSyncer(self) self.media_syncer = MediaSyncer(self)
aqt.mw = self aqt.mw = self
@ -230,7 +218,7 @@ class AnkiQt(QMainWindow):
self.pm.meta["firstRun"] = False self.pm.meta["firstRun"] = False
self.pm.save() self.pm.save()
self.pendingImport: Optional[str] = None self.pendingImport: str | None = None
self.restoringBackup = False self.restoringBackup = False
# profile not provided on command line? # profile not provided on command line?
if not self.pm.name: if not self.pm.name:
@ -380,7 +368,7 @@ class AnkiQt(QMainWindow):
self.progress.start() self.progress.start()
profiles = self.pm.profiles() profiles = self.pm.profiles()
def downgrade() -> List[str]: def downgrade() -> list[str]:
return self.pm.downgrade(profiles) return self.pm.downgrade(profiles)
def on_done(future: Future) -> None: def on_done(future: Future) -> None:
@ -399,7 +387,7 @@ class AnkiQt(QMainWindow):
self.taskman.run_in_background(downgrade, on_done) 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(): if not self.loadCollection():
return return
@ -678,7 +666,7 @@ class AnkiQt(QMainWindow):
self.maybe_check_for_addon_updates() self.maybe_check_for_addon_updates()
self.deckBrowser.show() self.deckBrowser.show()
def _selectedDeck(self) -> Optional[DeckDict]: def _selectedDeck(self) -> DeckDict | None:
did = self.col.decks.selected() did = self.col.decks.selected()
if not self.col.decks.name_if_exists(did): if not self.col.decks.name_if_exists(did):
showInfo(tr.qt_misc_please_select_a_deck()) showInfo(tr.qt_misc_please_select_a_deck())
@ -721,7 +709,7 @@ class AnkiQt(QMainWindow):
gui_hooks.operation_did_execute(op, None) gui_hooks.operation_did_execute(op, None)
def on_operation_did_execute( def on_operation_did_execute(
self, changes: OpChanges, handler: Optional[object] self, changes: OpChanges, handler: object | None
) -> None: ) -> None:
"Notify current screen of changes." "Notify current screen of changes."
focused = current_window() == self focused = current_window() == self
@ -741,7 +729,7 @@ class AnkiQt(QMainWindow):
self.toolbar.update_sync_status() self.toolbar.update_sync_status()
def on_focus_did_change( def on_focus_did_change(
self, new_focus: Optional[QWidget], _old: Optional[QWidget] self, new_focus: QWidget | None, _old: QWidget | None
) -> None: ) -> None:
"If main window has received focus, ensure current UI state is updated." "If main window has received focus, ensure current UI state is updated."
if new_focus and new_focus.window() == self: if new_focus and new_focus.window() == self:
@ -799,7 +787,7 @@ class AnkiQt(QMainWindow):
self, self,
link: str, link: str,
name: str, name: str,
key: Optional[str] = None, key: str | None = None,
class_: str = "", class_: str = "",
id: str = "", id: str = "",
extra: str = "", extra: str = "",
@ -810,8 +798,8 @@ class AnkiQt(QMainWindow):
else: else:
key = "" key = ""
return """ return """
<button id="%s" class="%s" onclick="pycmd('%s');return false;" <button id="{}" class="{}" onclick="pycmd('{}');return false;"
title="%s" %s>%s</button>""" % ( title="{}" {}>{}</button>""".format(
id, id,
class_, class_,
link, link,
@ -878,7 +866,7 @@ title="%s" %s>%s</button>""" % (
self.errorHandler = aqt.errors.ErrorHandler(self) self.errorHandler = aqt.errors.ErrorHandler(self)
def setupAddons(self, args: Optional[List]) -> None: def setupAddons(self, args: list | None) -> None:
import aqt.addons import aqt.addons
self.addonManager = aqt.addons.AddonManager(self) self.addonManager = aqt.addons.AddonManager(self)
@ -903,7 +891,7 @@ title="%s" %s>%s</button>""" % (
) )
self.pm.set_last_addon_update_check(intTime()) 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: if log:
show_log_to_user(self, log) show_log_to_user(self, log)
@ -1024,11 +1012,11 @@ title="%s" %s>%s</button>""" % (
("y", self.on_sync_button_clicked), ("y", self.on_sync_button_clicked),
] ]
self.applyShortcuts(globalShortcuts) self.applyShortcuts(globalShortcuts)
self.stateShortcuts: List[QShortcut] = [] self.stateShortcuts: list[QShortcut] = []
def applyShortcuts( def applyShortcuts(
self, shortcuts: Sequence[Tuple[str, Callable]] self, shortcuts: Sequence[tuple[str, Callable]]
) -> List[QShortcut]: ) -> list[QShortcut]:
qshortcuts = [] qshortcuts = []
for key, fn in shortcuts: for key, fn in shortcuts:
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
@ -1036,7 +1024,7 @@ title="%s" %s>%s</button>""" % (
qshortcuts.append(scut) qshortcuts.append(scut)
return qshortcuts 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) gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
# legacy hook # legacy hook
runHook(f"{self.state}StateShortcuts", shortcuts) runHook(f"{self.state}StateShortcuts", shortcuts)
@ -1154,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
# legacy # legacy
def onDeckConf(self, deck: Optional[DeckDict] = None) -> None: def onDeckConf(self, deck: DeckDict | None = None) -> None:
pass pass
# Importing & exporting # Importing & exporting
@ -1175,7 +1163,7 @@ title="%s" %s>%s</button>""" % (
aqt.importing.onImport(self) aqt.importing.onImport(self)
def onExport(self, did: Optional[DeckId] = None) -> None: def onExport(self, did: DeckId | None = None) -> None:
import aqt.exporting import aqt.exporting
aqt.exporting.ExportDialog(self, did=did) aqt.exporting.ExportDialog(self, did=did)
@ -1246,7 +1234,7 @@ title="%s" %s>%s</button>""" % (
if self.pm.meta.get("suppressUpdate", None) != ver: if self.pm.meta.get("suppressUpdate", None) != ver:
aqt.update.askAndUpdate(self, ver) aqt.update.askAndUpdate(self, ver)
def newMsg(self, data: Dict) -> None: def newMsg(self, data: dict) -> None:
aqt.update.showMessages(self, data) aqt.update.showMessages(self, data)
def clockIsOff(self, diff: int) -> None: 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.operation_did_execute.append(self.on_operation_did_execute)
gui_hooks.focus_did_change.append(self.on_focus_did_change) 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: def onOdueInvalid(self) -> None:
showWarning(tr.qt_misc_invalid_property_found_on_card_please()) showWarning(tr.qt_misc_invalid_property_found_on_card_please())
@ -1486,12 +1474,12 @@ title="%s" %s>%s</button>""" % (
c._render_output = None c._render_output = None
pprint.pprint(c.__dict__) pprint.pprint(c.__dict__)
def _debugCard(self) -> Optional[anki.cards.Card]: def _debugCard(self) -> anki.cards.Card | None:
card = self.reviewer.card card = self.reviewer.card
self._card_repr(card) self._card_repr(card)
return card return card
def _debugBrowserCard(self) -> Optional[anki.cards.Card]: def _debugBrowserCard(self) -> anki.cards.Card | None:
card = aqt.dialogs._dialogs["Browser"][1].card card = aqt.dialogs._dialogs["Browser"][1].card
self._card_repr(card) self._card_repr(card)
return card return card
@ -1564,7 +1552,7 @@ title="%s" %s>%s</button>""" % (
_dummy1 = windll _dummy1 = windll
_dummy2 = wintypes _dummy2 = wintypes
def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None: def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
if not self.hideMenuAccels: if not self.hideMenuAccels:
return return
tgt = tgt or self tgt = tgt or self

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import itertools import itertools
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar from typing import Iterable, Sequence, TypeVar
import aqt import aqt
from anki.collection import SearchNode from anki.collection import SearchNode
@ -26,7 +26,7 @@ from aqt.utils import (
T = TypeVar("T") 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) l = iter(l)
while True: while True:
res = list(itertools.islice(l, n)) res = list(itertools.islice(l, n))
@ -41,11 +41,11 @@ def check_media_db(mw: aqt.AnkiQt) -> None:
class MediaChecker: class MediaChecker:
progress_dialog: Optional[aqt.progress.ProgressDialog] progress_dialog: aqt.progress.ProgressDialog | None
def __init__(self, mw: aqt.AnkiQt) -> None: def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw self.mw = mw
self._progress_timer: Optional[QTimer] = None self._progress_timer: QTimer | None = None
def check(self) -> None: def check(self) -> None:
self.progress_dialog = self.mw.progress.start() self.progress_dialog = self.mw.progress.start()

View file

@ -11,7 +11,6 @@ import threading
import time import time
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from typing import Tuple
import flask import flask
import flask_cors # type: ignore 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 # catch /_anki references and rewrite them to web export folder
targetPath = "_anki/" targetPath = "_anki/"
if path.startswith(targetPath): if path.startswith(targetPath):

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Callable, List, Optional, Union from typing import Any, Callable, Union
import aqt import aqt
from anki.collection import Progress from anki.collection import Progress
@ -30,8 +30,8 @@ class MediaSyncer:
def __init__(self, mw: aqt.main.AnkiQt) -> None: def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw self.mw = mw
self._syncing: bool = False self._syncing: bool = False
self._log: List[LogEntryWithTime] = [] self._log: list[LogEntryWithTime] = []
self._progress_timer: Optional[QTimer] = None self._progress_timer: QTimer | None = None
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop) gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
def _on_progress(self) -> None: def _on_progress(self) -> None:
@ -98,7 +98,7 @@ class MediaSyncer:
self._log_and_notify(tr.sync_media_failed()) self._log_and_notify(tr.sync_media_failed())
showWarning(str(exc)) showWarning(str(exc))
def entries(self) -> List[LogEntryWithTime]: def entries(self) -> list[LogEntryWithTime]:
return self._log return self._log
def abort(self) -> None: def abort(self) -> None:
@ -125,7 +125,7 @@ class MediaSyncer:
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True) diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
diag.show() diag.show()
timer: Optional[QTimer] = None timer: QTimer | None = None
def check_finished() -> None: def check_finished() -> None:
if not self.is_syncing(): if not self.is_syncing():

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -75,7 +76,7 @@ class ModelChooser(QHBoxLayout):
# edit button # edit button
edit = QPushButton(tr.qt_misc_manage(), clicked=self.onEdit) # type: ignore 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()] return [nt.name for nt in self.deck.models.all_names_and_ids()]
ret = StudyDeck( ret = StudyDeck(

View file

@ -1,9 +1,11 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from concurrent.futures import Future from concurrent.futures import Future
from operator import itemgetter from operator import itemgetter
from typing import Any, List, Optional, Sequence from typing import Any, Optional, Sequence
import aqt.clayout import aqt.clayout
from anki import stdmodels from anki import stdmodels
@ -239,7 +241,7 @@ class AddModel(QDialog):
self.dialog.setupUi(self) self.dialog.setupUi(self)
disable_help_button(self) disable_help_button(self)
# standard models # standard models
self.notetypes: List[ self.notetypes: list[
Union[NotetypeDict, Callable[[Collection], NotetypeDict]] Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
] = [] ] = []
for (name, func) in stdmodels.get_stock_notetypes(self.col): 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 # 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, find_executable,
) )
from queue import Empty, Full, Queue from queue import Empty, Full, Queue
from typing import Dict, Optional from typing import Optional
from anki.utils import isWin from anki.utils import isWin
@ -80,7 +79,7 @@ class MPVBase:
""" """
executable = find_executable("mpv") executable = find_executable("mpv")
popenEnv: Optional[Dict[str, str]] = None popenEnv: Optional[dict[str, str]] = None
default_argv = [ default_argv = [
"--idle", "--idle",

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 anki.models import NotetypeId
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
@ -98,7 +99,7 @@ class NotetypeChooser(QHBoxLayout):
edit = QPushButton(tr.qt_misc_manage()) edit = QPushButton(tr.qt_misc_manage())
qconnect(edit.clicked, self.onEdit) qconnect(edit.clicked, self.onEdit)
def nameFunc() -> List[str]: def nameFunc() -> list[str]:
return sorted(self.mw.col.models.all_names()) return sorted(self.mw.col.models.all_names())
ret = StudyDeck( ret = StudyDeck(

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from concurrent.futures._base import Future 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 import aqt
from anki.collection import ( from anki.collection import (
@ -61,26 +61,26 @@ class CollectionOp(Generic[ResultWithChanges]):
passed to `failure` if it is provided. passed to `failure` if it is provided.
""" """
_success: Optional[Callable[[ResultWithChanges], Any]] = None _success: Callable[[ResultWithChanges], Any] | None = None
_failure: Optional[Callable[[Exception], Any]] = None _failure: Callable[[Exception], Any] | None = None
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
self._parent = parent self._parent = parent
self._op = op self._op = op
def success( def success(
self, success: Optional[Callable[[ResultWithChanges], Any]] self, success: Callable[[ResultWithChanges], Any] | None
) -> CollectionOp[ResultWithChanges]: ) -> CollectionOp[ResultWithChanges]:
self._success = success self._success = success
return self return self
def failure( def failure(
self, failure: Optional[Callable[[Exception], Any]] self, failure: Callable[[Exception], Any] | None
) -> CollectionOp[ResultWithChanges]: ) -> CollectionOp[ResultWithChanges]:
self._failure = failure self._failure = failure
return self 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 from aqt import mw
assert mw assert mw
@ -121,7 +121,7 @@ class CollectionOp(Generic[ResultWithChanges]):
def _fire_change_hooks_after_op_performed( def _fire_change_hooks_after_op_performed(
self, self,
result: ResultWithChanges, result: ResultWithChanges,
handler: Optional[object], handler: object | None,
) -> None: ) -> None:
from aqt import mw from aqt import mw
@ -158,8 +158,8 @@ class QueryOp(Generic[T]):
passed to `failure` if it is provided. passed to `failure` if it is provided.
""" """
_failure: Optional[Callable[[Exception], Any]] = None _failure: Callable[[Exception], Any] | None = None
_progress: Union[bool, str] = False _progress: bool | str = False
def __init__( def __init__(
self, self,
@ -172,11 +172,11 @@ class QueryOp(Generic[T]):
self._op = op self._op = op
self._success = success 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 self._failure = failure
return self 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 self._progress = label or True
return self return self
@ -190,7 +190,7 @@ class QueryOp(Generic[T]):
def wrapped_op() -> T: def wrapped_op() -> T:
assert mw assert mw
if self._progress: if self._progress:
label: Optional[str] label: str | None
if isinstance(self._progress, str): if isinstance(self._progress, str):
label = self._progress label = self._progress
else: else:

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, Sequence from typing import Sequence
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs
@ -50,7 +50,7 @@ def add_deck_dialog(
*, *,
parent: QWidget, parent: QWidget,
default_text: str = "", default_text: str = "",
) -> Optional[CollectionOp[OpChangesWithId]]: ) -> CollectionOp[OpChangesWithId] | None:
if name := getOnlyText( if name := getOnlyText(
tr.decks_new_deck_name(), default=default_text, parent=parent tr.decks_new_deck_name(), default=default_text, parent=parent
).strip(): ).strip():

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, Sequence from typing import Sequence
from anki.collection import OpChanges, OpChangesWithCount from anki.collection import OpChanges, OpChangesWithCount
from anki.decks import DeckId from anki.decks import DeckId
@ -43,7 +43,7 @@ def find_and_replace(
search: str, search: str,
replacement: str, replacement: str,
regex: bool, regex: bool,
field_name: Optional[str], field_name: str | None,
match_case: bool, match_case: bool,
) -> CollectionOp[OpChangesWithCount]: ) -> CollectionOp[OpChangesWithCount]:
return CollectionOp( return CollectionOp(

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, Sequence from typing import Sequence
import aqt import aqt
from anki.cards import CardId from anki.cards import CardId
@ -29,8 +29,8 @@ def set_due_date_dialog(
*, *,
parent: QWidget, parent: QWidget,
card_ids: Sequence[CardId], card_ids: Sequence[CardId],
config_key: Optional[Config.String.V], config_key: Config.String.V | None,
) -> Optional[CollectionOp[OpChanges]]: ) -> CollectionOp[OpChanges] | None:
assert aqt.mw assert aqt.mw
if not card_ids: if not card_ids:
return None return None
@ -77,7 +77,7 @@ def forget_cards(
def reposition_new_cards_dialog( def reposition_new_cards_dialog(
*, parent: QWidget, card_ids: Sequence[CardId] *, parent: QWidget, card_ids: Sequence[CardId]
) -> Optional[CollectionOp[OpChangesWithCount]]: ) -> CollectionOp[OpChangesWithCount] | None:
from aqt import mw from aqt import mw
assert mw assert mw

View file

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable
import aqt import aqt
from anki.collection import OpChanges from anki.collection import OpChanges
@ -72,7 +72,7 @@ class Overview:
self.refresh() self.refresh()
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> bool: ) -> bool:
if changes.study_queues: if changes.study_queues:
self._refresh_needed = True self._refresh_needed = True
@ -115,7 +115,7 @@ class Overview:
openLink(url) openLink(url)
return False return False
def _shortcutKeys(self) -> List[Tuple[str, Callable]]: def _shortcutKeys(self) -> list[tuple[str, Callable]]:
return [ return [
("o", lambda: display_options_for_deck(self.mw.col.decks.current())), ("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
("r", self.rebuild_current_filtered_deck), ("r", self.rebuild_current_filtered_deck),
@ -202,7 +202,7 @@ class Overview:
def _show_finished_screen(self) -> None: def _show_finished_screen(self) -> None:
self.web.load_ts_page("congrats") 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"]: if deck["dyn"]:
desc = tr.studying_this_is_a_special_deck_for() desc = tr.studying_this_is_a_special_deck_for()
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}" desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
@ -219,19 +219,19 @@ class Overview:
dyn = "" dyn = ""
return f'<div class="descfont descmid description {dyn}">{desc}</div>' 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()) counts = list(self.mw.col.sched.counts())
but = self.mw.button but = self.mw.button
return """ return """
<table width=400 cellpadding=5> <table width=400 cellpadding=5>
<tr><td align=center valign=top> <tr><td align=center valign=top>
<table cellspacing=5> <table cellspacing=5>
<tr><td>%s:</td><td><b><span class=new-count>%s</span></b></td></tr> <tr><td>{}:</td><td><b><span class=new-count>{}</span></b></td></tr>
<tr><td>%s:</td><td><b><span class=learn-count>%s</span></b></td></tr> <tr><td>{}:</td><td><b><span class=learn-count>{}</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=review-count>{}</span></b></td></tr>
</table> </table>
</td><td align=center> </td><td align=center>
%s</td></tr></table>""" % ( {}</td></tr></table>""".format(
tr.actions_new(), tr.actions_new(),
counts[0], counts[0],
tr.scheduling_learning(), tr.scheduling_learning(),

View file

@ -9,7 +9,7 @@ import random
import shutil import shutil
import traceback import traceback
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional from typing import Any
from send2trash import send2trash from send2trash import send2trash
@ -62,7 +62,7 @@ class VideoDriver(Enum):
return VideoDriver.Software return VideoDriver.Software
@staticmethod @staticmethod
def all_for_platform() -> List[VideoDriver]: def all_for_platform() -> list[VideoDriver]:
all = [VideoDriver.OpenGL] all = [VideoDriver.OpenGL]
if isWin: if isWin:
all.append(VideoDriver.ANGLE) all.append(VideoDriver.ANGLE)
@ -81,7 +81,7 @@ metaConf = dict(
defaultLang=None, defaultLang=None,
) )
profileConf: Dict[str, Any] = dict( profileConf: dict[str, Any] = dict(
# profile # profile
mainWindowGeom=None, mainWindowGeom=None,
mainWindowState=None, mainWindowState=None,
@ -118,12 +118,12 @@ class AnkiRestart(SystemExit):
class ProfileManager: 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 ## Settings which should be forgotten each Anki restart
self.session: Dict[str, Any] = {} self.session: dict[str, Any] = {}
self.name: Optional[str] = None self.name: str | None = None
self.db: Optional[DB] = None self.db: DB | None = None
self.profile: Optional[Dict] = None self.profile: dict | None = None
# instantiate base folder # instantiate base folder
self.base: str self.base: str
self._setBaseFolder(base) self._setBaseFolder(base)
@ -245,8 +245,8 @@ class ProfileManager:
# Profile load/save # Profile load/save
###################################################################### ######################################################################
def profiles(self) -> List: def profiles(self) -> list:
def names() -> List: def names() -> list:
return self.db.list("select name from profiles where name != '_global'") return self.db.list("select name from profiles where name != '_global'")
n = names() n = names()
@ -393,7 +393,7 @@ class ProfileManager:
# Downgrade # 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." "Downgrade all profiles. Return a list of profiles that couldn't be opened."
problem_profiles = [] problem_profiles = []
for name in profiles: for name in profiles:
@ -420,7 +420,7 @@ class ProfileManager:
os.makedirs(path) os.makedirs(path)
return path return path
def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None: def _setBaseFolder(self, cmdlineBase: str | None) -> None:
if cmdlineBase: if cmdlineBase:
self.base = os.path.abspath(cmdlineBase) self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"): elif os.environ.get("ANKI_BASE"):
@ -612,13 +612,13 @@ create table if not exists profiles
# Profile-specific # Profile-specific
###################################################################### ######################################################################
def set_sync_key(self, val: Optional[str]) -> None: def set_sync_key(self, val: str | None) -> None:
self.profile["syncKey"] = val 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 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 self.profile["hostNum"] = val or 0
def media_syncing_enabled(self) -> bool: def media_syncing_enabled(self) -> bool:
@ -627,7 +627,7 @@ create table if not exists profiles
def auto_syncing_enabled(self) -> bool: def auto_syncing_enabled(self) -> bool:
return self.profile["autoSync"] return self.profile["autoSync"]
def sync_auth(self) -> Optional[SyncAuth]: def sync_auth(self) -> SyncAuth | None:
hkey = self.profile.get("syncKey") hkey = self.profile.get("syncKey")
if not hkey: if not hkey:
return None return None

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Callable, Optional
import aqt.forms import aqt.forms
from aqt.qt import * from aqt.qt import *
@ -19,9 +18,9 @@ class ProgressManager:
self.app = mw.app self.app = mw.app
self.inDB = False self.inDB = False
self.blockUpdates = False self.blockUpdates = False
self._show_timer: Optional[QTimer] = None self._show_timer: QTimer | None = None
self._busy_cursor_timer: Optional[QTimer] = None self._busy_cursor_timer: QTimer | None = None
self._win: Optional[ProgressDialog] = None self._win: ProgressDialog | None = None
self._levels = 0 self._levels = 0
# Safer timers # Safer timers
@ -74,10 +73,10 @@ class ProgressManager:
self, self,
max: int = 0, max: int = 0,
min: int = 0, min: int = 0,
label: Optional[str] = None, label: str | None = None,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
immediate: bool = False, immediate: bool = False,
) -> Optional[ProgressDialog]: ) -> ProgressDialog | None:
self._levels += 1 self._levels += 1
if self._levels > 1: if self._levels > 1:
return None return None
@ -112,11 +111,11 @@ class ProgressManager:
def update( def update(
self, self,
label: Optional[str] = None, label: str | None = None,
value: Optional[int] = None, value: int | None = None,
process: bool = True, process: bool = True,
maybeShow: bool = True, maybeShow: bool = True,
max: Optional[int] = None, max: int | None = None,
) -> None: ) -> None:
# print self._min, self._counter, self._max, label, time.time() - self._lastTime # print self._min, self._counter, self._max, label, time.time() - self._lastTime
if not self.mw.inMainThread(): if not self.mw.inMainThread():

View file

@ -11,7 +11,7 @@ import re
import unicodedata as ucd import unicodedata as ucd
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto 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 from PyQt5.QtCore import Qt
@ -85,7 +85,7 @@ class V3CardInfo:
def top_card(self) -> QueuedCards.QueuedCard: def top_card(self) -> QueuedCards.QueuedCard:
return self.queued_cards.cards[0] return self.queued_cards.cards[0]
def counts(self) -> Tuple[int, List[int]]: def counts(self) -> tuple[int, list[int]]:
"Returns (idx, counts)." "Returns (idx, counts)."
counts = [ counts = [
self.queued_cards.new_count, self.queued_cards.new_count,
@ -117,16 +117,16 @@ class Reviewer:
def __init__(self, mw: AnkiQt) -> None: def __init__(self, mw: AnkiQt) -> None:
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
self.card: Optional[Card] = None self.card: Card | None = None
self.cardQueue: List[Card] = [] self.cardQueue: list[Card] = []
self.previous_card: Optional[Card] = None self.previous_card: Card | None = None
self.hadCardQueue = False self.hadCardQueue = False
self._answeredIds: List[CardId] = [] self._answeredIds: list[CardId] = []
self._recordedAudio: Optional[str] = None self._recordedAudio: str | None = None
self.typeCorrect: str = None # web init happens before this is set self.typeCorrect: str = None # web init happens before this is set
self.state: Optional[str] = None self.state: str | None = None
self._refresh_needed: Optional[RefreshNeeded] = None self._refresh_needed: RefreshNeeded | None = None
self._v3: Optional[V3CardInfo] = None self._v3: V3CardInfo | None = None
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1)) self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech) hooks.card_did_leech.append(self.onLeech)
@ -141,7 +141,7 @@ class Reviewer:
self.refresh_if_needed() self.refresh_if_needed()
# this is only used by add-ons # this is only used by add-ons
def lastCard(self) -> Optional[Card]: def lastCard(self) -> Card | None:
if self._answeredIds: if self._answeredIds:
if not self.card or self._answeredIds[-1] != self.card.id: if not self.card or self._answeredIds[-1] != self.card.id:
try: try:
@ -167,7 +167,7 @@ class Reviewer:
self._refresh_needed = None self._refresh_needed = None
def op_executed( def op_executed(
self, changes: OpChanges, handler: Optional[object], focused: bool self, changes: OpChanges, handler: object | None, focused: bool
) -> bool: ) -> bool:
if handler is not self: if handler is not self:
if changes.study_queues: 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 = Card(self.mw.col, backend_card=self._v3.top_card().card)
self.card.start_timer() self.card.start_timer()
def get_next_states(self) -> Optional[NextStates]: def get_next_states(self) -> NextStates | None:
if v3 := self._v3: if v3 := self._v3:
return v3.next_states return v3.next_states
else: else:
@ -434,7 +434,7 @@ class Reviewer:
def _shortcutKeys( def _shortcutKeys(
self, self,
) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]: ) -> Sequence[Union[tuple[str, Callable], tuple[Qt.Key, Callable]]]:
return [ return [
("e", self.mw.onEditCurrent), ("e", self.mw.onEditCurrent),
(" ", self.onEnterKey), (" ", self.onEnterKey),
@ -587,7 +587,7 @@ class Reviewer:
# can't pass a string in directly, and can't use re.escape as it # can't pass a string in directly, and can't use re.escape as it
# escapes too much # escapes too much
s = """ s = """
<span style="font-family: '%s'; font-size: %spx">%s</span>""" % ( <span style="font-family: '{}'; font-size: {}px">{}</span>""".format(
self.typeFont, self.typeFont,
self.typeSize, self.typeSize,
res, res,
@ -620,23 +620,23 @@ class Reviewer:
def tokenizeComparison( def tokenizeComparison(
self, given: str, correct: str 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 # compare in NFC form so accents appear correct
given = ucd.normalize("NFC", given) given = ucd.normalize("NFC", given)
correct = ucd.normalize("NFC", correct) correct = ucd.normalize("NFC", correct)
s = difflib.SequenceMatcher(None, given, correct, autojunk=False) s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
givenElems: List[Tuple[bool, str]] = [] givenElems: list[tuple[bool, str]] = []
correctElems: List[Tuple[bool, str]] = [] correctElems: list[tuple[bool, str]] = []
givenPoint = 0 givenPoint = 0
correctPoint = 0 correctPoint = 0
offby = 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: if old != new:
array.append((False, s[old:new])) array.append((False, s[old:new]))
def logGood( 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: ) -> None:
if cnt: if cnt:
array.append((True, s[start : start + cnt])) array.append((True, s[start : start + cnt]))
@ -737,8 +737,8 @@ time = %(time)d;
def _showAnswerButton(self) -> None: def _showAnswerButton(self) -> None:
middle = """ middle = """
<span class=stattxt>%s</span><br> <span class=stattxt>{}</span><br>
<button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % ( <button title="{}" id="ansbut" class="focus" onclick='pycmd("ans");'>{}</button>""".format(
self._remaining(), self._remaining(),
tr.actions_shortcut_key(val=tr.studying_space()), tr.actions_shortcut_key(val=tr.studying_space()),
tr.studying_show_answer(), tr.studying_show_answer(),
@ -763,10 +763,10 @@ time = %(time)d;
if not self.mw.col.conf["dueCounts"]: if not self.mw.col.conf["dueCounts"]:
return "" return ""
counts: List[Union[int, str]] counts: list[Union[int, str]]
if v3 := self._v3: if v3 := self._v3:
idx, counts_ = v3.counts() idx, counts_ = v3.counts()
counts = cast(List[Union[int, str]], counts_) counts = cast(list[Union[int, str]], counts_)
else: else:
# v1/v2 scheduler # v1/v2 scheduler
if self.hadCardQueue: if self.hadCardQueue:
@ -790,10 +790,10 @@ time = %(time)d;
else: else:
return 2 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) button_count = self.mw.col.sched.answerButtons(self.card)
if button_count == 2: if button_count == 2:
buttons_tuple: Tuple[Tuple[int, str], ...] = ( buttons_tuple: tuple[tuple[int, str], ...] = (
(1, tr.studying_again()), (1, tr.studying_again()),
(2, tr.studying_good()), (2, tr.studying_good()),
) )
@ -847,7 +847,7 @@ time = %(time)d;
buf += "</tr></table>" buf += "</tr></table>"
return buf return buf
def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str: def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str:
if not self.mw.col.conf["estTimes"]: if not self.mw.col.conf["estTimes"]:
return "<div class=spacer></div>" return "<div class=spacer></div>"
if v3_labels: if v3_labels:
@ -859,7 +859,7 @@ time = %(time)d;
# Leeches # Leeches
########################################################################## ##########################################################################
def onLeech(self, card: Optional[Card] = None) -> None: def onLeech(self, card: Card | None = None) -> None:
# for now # for now
s = tr.studying_card_was_a_leech() s = tr.studying_card_was_a_leech()
# v3 scheduler doesn't report this # v3 scheduler doesn't report this
@ -891,7 +891,7 @@ time = %(time)d;
########################################################################## ##########################################################################
# note the shortcuts listed here also need to be defined above # 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() currentFlag = self.card and self.card.user_flag()
opts = [ opts = [
[ [

View file

@ -15,7 +15,7 @@ import wave
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from concurrent.futures import Future from concurrent.futures import Future
from operator import itemgetter 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 from markdown import markdown
@ -60,7 +60,7 @@ class Player(ABC):
""" """
@abstractmethod @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. """How suited this player is to playing tag.
AVPlayer will choose the player that returns the highest rank 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 class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
default_rank = 0 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): if isinstance(tag, SoundOrVideoTag):
return self.default_rank return self.default_rank
else: else:
@ -115,7 +115,7 @@ class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
class SoundPlayer(Player): # pylint: disable=abstract-method class SoundPlayer(Player): # pylint: disable=abstract-method
default_rank = 0 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): if isinstance(tag, SoundOrVideoTag) and is_audio_file(tag.filename):
return self.default_rank return self.default_rank
else: else:
@ -125,7 +125,7 @@ class SoundPlayer(Player): # pylint: disable=abstract-method
class VideoPlayer(Player): # pylint: disable=abstract-method class VideoPlayer(Player): # pylint: disable=abstract-method
default_rank = 0 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): if isinstance(tag, SoundOrVideoTag) and not is_audio_file(tag.filename):
return self.default_rank return self.default_rank
else: else:
@ -137,16 +137,16 @@ class VideoPlayer(Player): # pylint: disable=abstract-method
class AVPlayer: class AVPlayer:
players: List[Player] = [] players: list[Player] = []
# when a new batch of audio is played, should the currently playing # when a new batch of audio is played, should the currently playing
# audio be stopped? # audio be stopped?
interrupt_current_audio = True interrupt_current_audio = True
def __init__(self) -> None: def __init__(self) -> None:
self._enqueued: List[AVTag] = [] self._enqueued: list[AVTag] = []
self.current_player: Optional[Player] = None 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.""" """Clear the existing queue, then start playing provided tags."""
self.clear_queue_and_maybe_interrupt() self.clear_queue_and_maybe_interrupt()
self._enqueued = tags[:] self._enqueued = tags[:]
@ -185,7 +185,7 @@ class AVPlayer:
if self.current_player: if self.current_player:
self.current_player.stop() self.current_player.stop()
def _pop_next(self) -> Optional[AVTag]: def _pop_next(self) -> AVTag | None:
if not self._enqueued: if not self._enqueued:
return None return None
return self._enqueued.pop(0) return self._enqueued.pop(0)
@ -212,7 +212,7 @@ class AVPlayer:
else: else:
tooltip(f"no players found for {tag}") 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 = [] ranked = []
for p in self.players: for p in self.players:
rank = p.rank_for_tag(tag) rank = p.rank_for_tag(tag)
@ -234,7 +234,7 @@ av_player = AVPlayer()
# return modified command array that points to bundled command, and return # return modified command array that points to bundled command, and return
# required environment # required environment
def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]: def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
cmd = cmd[:] cmd = cmd[:]
env = os.environ.copy() env = os.environ.copy()
if "LD_LIBRARY_PATH" in env: if "LD_LIBRARY_PATH" in env:
@ -275,13 +275,13 @@ def retryWait(proc: subprocess.Popen) -> int:
class SimpleProcessPlayer(Player): # pylint: disable=abstract-method class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
"A player that invokes a new process for each tag to play." "A player that invokes a new process for each tag to play."
args: List[str] = [] args: list[str] = []
env: Optional[Dict[str, str]] = None env: dict[str, str] | None = None
def __init__(self, taskman: TaskManager) -> None: def __init__(self, taskman: TaskManager) -> None:
self._taskman = taskman self._taskman = taskman
self._terminate_flag = False 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: def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
self._terminate_flag = False self._terminate_flag = False
@ -388,7 +388,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
def __init__(self, base_path: str) -> None: def __init__(self, base_path: str) -> None:
mpvPath, self.popenEnv = _packagedCmd(["mpv"]) mpvPath, self.popenEnv = _packagedCmd(["mpv"])
self.executable = mpvPath[0] self.executable = mpvPath[0]
self._on_done: Optional[OnDoneCallback] = None self._on_done: OnDoneCallback | None = None
self.default_argv += [f"--config-dir={base_path}"] self.default_argv += [f"--config-dir={base_path}"]
super().__init__(window_id=None, debug=False) super().__init__(window_id=None, debug=False)
@ -635,7 +635,7 @@ except:
PYAU_CHANNELS = 1 PYAU_CHANNELS = 1
PYAU_INPUT_INDEX: Optional[int] = None PYAU_INPUT_INDEX: int | None = None
class PyAudioThreadedRecorder(threading.Thread): class PyAudioThreadedRecorder(threading.Thread):
@ -839,7 +839,7 @@ def playFromText(text: Any) -> None:
# legacy globals # legacy globals
_player = play _player = play
_queueEraser = clearAudioQueue _queueEraser = clearAudioQueue
mpvManager: Optional["MpvManager"] = None mpvManager: MpvManager | None = None
# add everything from this module into anki.sound for backwards compat # add everything from this module into anki.sound for backwards compat
_exports = [i for i in locals().items() if not i[0].startswith("__")] _exports = [i for i in locals().items() if not i[0].startswith("__")]

View file

@ -1,7 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 import aqt
from anki.collection import OpChangesWithId from anki.collection import OpChangesWithId
@ -34,7 +34,7 @@ class StudyDeck(QDialog):
cancel: bool = True, cancel: bool = True,
parent: Optional[QWidget] = None, parent: Optional[QWidget] = None,
dyn: bool = False, dyn: bool = False,
buttons: Optional[List[Union[str, QPushButton]]] = None, buttons: Optional[list[Union[str, QPushButton]]] = None,
geomKey: str = "default", geomKey: str = "default",
) -> None: ) -> None:
QDialog.__init__(self, parent or mw) QDialog.__init__(self, parent or mw)

View file

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Tuple
from aqt import colors from aqt import colors
from aqt.qt import * from aqt.qt import *
@ -22,8 +20,8 @@ class Switch(QAbstractButton):
radius: int = 10, radius: int = 10,
left_label: str = "", left_label: str = "",
right_label: str = "", right_label: str = "",
left_color: Tuple[str, str] = colors.FLAG4_BG, left_color: tuple[str, str] = colors.FLAG4_BG,
right_color: Tuple[str, str] = colors.FLAG3_BG, right_color: tuple[str, str] = colors.FLAG3_BG,
parent: QWidget = None, parent: QWidget = None,
) -> None: ) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import enum import enum
import os import os
from concurrent.futures import Future from concurrent.futures import Future
from typing import Callable, Tuple from typing import Callable
import aqt import aqt
from anki.errors import Interrupted, SyncError, SyncErrorKind 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( def get_id_and_pass_from_user(
mw: aqt.main.AnkiQt, username: str = "", password: str = "" mw: aqt.main.AnkiQt, username: str = "", password: str = ""
) -> Tuple[str, str]: ) -> tuple[str, str]:
diag = QDialog(mw) diag = QDialog(mw)
diag.setWindowTitle("Anki") diag.setWindowTitle("Anki")
disable_help_button(diag) disable_help_button(diag)

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
from typing import Iterable, List, Optional, Union from typing import Iterable
from anki.collection import Collection from anki.collection import Collection
from aqt import gui_hooks from aqt import gui_hooks
@ -12,14 +12,14 @@ from aqt.qt import *
class TagEdit(QLineEdit): class TagEdit(QLineEdit):
_completer: Union[QCompleter, TagCompleter] _completer: QCompleter | TagCompleter
lostFocus = pyqtSignal() lostFocus = pyqtSignal()
# 0 = tags, 1 = decks # 0 = tags, 1 = decks
def __init__(self, parent: QWidget, type: int = 0) -> None: def __init__(self, parent: QWidget, type: int = 0) -> None:
QLineEdit.__init__(self, parent) QLineEdit.__init__(self, parent)
self.col: Optional[Collection] = None self.col: Collection | None = None
self.model = QStringListModel() self.model = QStringListModel()
self.type = type self.type = type
if type == 0: if type == 0:
@ -112,11 +112,11 @@ class TagCompleter(QCompleter):
edit: TagEdit, edit: TagEdit,
) -> None: ) -> None:
QCompleter.__init__(self, model, parent) QCompleter.__init__(self, model, parent)
self.tags: List[str] = [] self.tags: list[str] = []
self.edit = edit 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 = tags.strip()
stripped_tags = re.sub(" +", " ", stripped_tags) stripped_tags = re.sub(" +", " ", stripped_tags)
self.tags = self.edit.col.tags.split(stripped_tags) self.tags = self.edit.col.tags.split(stripped_tags)

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html # 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 import aqt
from anki.lang import with_collapsed_whitespace from anki.lang import with_collapsed_whitespace
@ -14,7 +15,7 @@ class TagLimit(QDialog):
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None: def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
QDialog.__init__(self, parent, Qt.Window) QDialog.__init__(self, parent, Qt.Window)
self.tags: str = "" self.tags: str = ""
self.tags_list: List[str] = [] self.tags_list: list[str] = []
self.mw = mw self.mw = mw
self.parent_: Optional[CustomStudy] = parent self.parent_: Optional[CustomStudy] = parent
self.deck = self.parent_.deck self.deck = self.parent_.deck

View file

@ -12,7 +12,7 @@ from __future__ import annotations
from concurrent.futures import Future from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
from threading import Lock from threading import Lock
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
@ -29,7 +29,7 @@ class TaskManager(QObject):
QObject.__init__(self) QObject.__init__(self)
self.mw = mw.weakref() self.mw = mw.weakref()
self._executor = ThreadPoolExecutor() self._executor = ThreadPoolExecutor()
self._closures: List[Closure] = [] self._closures: list[Closure] = []
self._closures_lock = Lock() self._closures_lock = Lock()
qconnect(self._closures_pending, self._on_closures_pending) qconnect(self._closures_pending, self._on_closures_pending)
@ -42,8 +42,8 @@ class TaskManager(QObject):
def run_in_background( def run_in_background(
self, self,
task: Callable, task: Callable,
on_done: Optional[Callable[[Future], None]] = None, on_done: Callable[[Future], None] | None = None,
args: Optional[Dict[str, Any]] = None, args: dict[str, Any] | None = None,
) -> Future: ) -> Future:
"""Use QueryOp()/CollectionOp() in new code. """Use QueryOp()/CollectionOp() in new code.
@ -76,9 +76,9 @@ class TaskManager(QObject):
def with_progress( def with_progress(
self, self,
task: Callable, task: Callable,
on_done: Optional[Callable[[Future], None]] = None, on_done: Callable[[Future], None] | None = None,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
label: Optional[str] = None, label: str | None = None,
immediate: bool = False, immediate: bool = False,
) -> None: ) -> None:
"Use QueryOp()/CollectionOp() in new code." "Use QueryOp()/CollectionOp() in new code."

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import platform import platform
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Tuple, Union
from anki.utils import isMac from anki.utils import isMac
from aqt import QApplication, colors, gui_hooks, isWin 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: class ColoredIcon:
path: str path: str
# (day, night) # (day, night)
color: Tuple[str, str] color: tuple[str, str]
def current_color(self, night_mode: bool) -> str: def current_color(self, night_mode: bool) -> str:
if night_mode: if night_mode:
@ -25,17 +24,17 @@ class ColoredIcon:
else: else:
return self.color[0] 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) return ColoredIcon(path=self.path, color=color)
class ThemeManager: class ThemeManager:
_night_mode_preference = False _night_mode_preference = False
_icon_cache_light: Dict[str, QIcon] = {} _icon_cache_light: dict[str, QIcon] = {}
_icon_cache_dark: Dict[str, QIcon] = {} _icon_cache_dark: dict[str, QIcon] = {}
_icon_size = 128 _icon_size = 128
_dark_mode_available: Optional[bool] = None _dark_mode_available: bool | None = None
default_palette: Optional[QPalette] = None default_palette: QPalette | None = None
# Qt applies a gradient to the buttons in dark mode # Qt applies a gradient to the buttons in dark mode
# from about #505050 to #606060. # from about #505050 to #606060.
@ -65,7 +64,7 @@ class ThemeManager:
night_mode = property(get_night_mode, set_night_mode) 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." "Fetch icon from Qt resources, and invert if in night mode."
if self.night_mode: if self.night_mode:
cache = self._icon_cache_light cache = self._icon_cache_light
@ -101,7 +100,7 @@ class ThemeManager:
return cache.setdefault(path, icon) 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." "Returns space-separated class list for platform/theme."
classes = [] classes = []
if isWin: if isWin:
@ -120,17 +119,17 @@ class ThemeManager:
return " ".join(classes) return " ".join(classes)
def body_classes_for_card_ord( 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: ) -> str:
"Returns body classes used when showing a card." "Returns body classes used when showing a card."
return f"card card{card_ord+1} {self.body_class(night_mode)}" 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.""" """Given day/night colors, return the correct one for the current theme."""
idx = 1 if self.night_mode else 0 idx = 1 if self.night_mode else 0
return colors[idx] return colors[idx]
def qcolor(self, colors: Tuple[str, str]) -> QColor: def qcolor(self, colors: tuple[str, str]) -> QColor:
return QColor(self.color(colors)) return QColor(self.color(colors))
def apply_style(self, app: QApplication) -> None: def apply_style(self, app: QApplication) -> None:
@ -166,27 +165,27 @@ QToolTip {
if not self.macos_dark_mode(): if not self.macos_dark_mode():
buf += """ buf += """
QScrollBar { background-color: %s; } QScrollBar {{ background-color: {}; }}
QScrollBar::handle { background-color: %s; border-radius: 5px; } QScrollBar::handle {{ background-color: {}; border-radius: 5px; }}
QScrollBar:horizontal { height: 12px; } QScrollBar:horizontal {{ height: 12px; }}
QScrollBar::handle:horizontal { min-width: 50px; } QScrollBar::handle:horizontal {{ min-width: 50px; }}
QScrollBar:vertical { width: 12px; } QScrollBar:vertical {{ width: 12px; }}
QScrollBar::handle:vertical { min-height: 50px; } QScrollBar::handle:vertical {{ min-height: 50px; }}
QScrollBar::add-line { QScrollBar::add-line {{
border: none; border: none;
background: none; background: none;
} }}
QScrollBar::sub-line { QScrollBar::sub-line {{
border: none; border: none;
background: none; background: none;
} }}
QTabWidget { background-color: %s; } QTabWidget {{ background-color: {}; }}
""" % ( """.format(
self.color(colors.WINDOW_BG), self.color(colors.WINDOW_BG),
# fushion-button-hover-bg # fushion-button-hover-bg
"#656565", "#656565",

View file

@ -2,7 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any
import aqt import aqt
from anki.sync import SyncStatus from anki.sync import SyncStatus
@ -29,7 +29,7 @@ class Toolbar:
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
self.mw = mw self.mw = mw
self.web = web self.web = web
self.link_handlers: Dict[str, Callable] = { self.link_handlers: dict[str, Callable] = {
"study": self._studyLinkHandler, "study": self._studyLinkHandler,
} }
self.web.setFixedHeight(30) self.web.setFixedHeight(30)
@ -38,8 +38,8 @@ class Toolbar:
def draw( def draw(
self, self,
buf: str = "", buf: str = "",
web_context: Optional[Any] = None, web_context: Any | None = None,
link_handler: Optional[Callable[[str], Any]] = None, link_handler: Callable[[str], Any] | None = None,
) -> None: ) -> None:
web_context = web_context or TopToolbar(self) web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler link_handler = link_handler or self._linkHandler
@ -65,8 +65,8 @@ class Toolbar:
cmd: str, cmd: str,
label: str, label: str,
func: Callable, func: Callable,
tip: Optional[str] = None, tip: str | None = None,
id: Optional[str] = None, id: str | None = None,
) -> str: ) -> str:
"""Generates HTML link element and registers link handler """Generates HTML link element and registers link handler
@ -218,8 +218,8 @@ class BottomBar(Toolbar):
def draw( def draw(
self, self,
buf: str = "", buf: str = "",
web_context: Optional[Any] = None, web_context: Any | None = None,
link_handler: Optional[Callable[[str], Any]] = None, link_handler: Callable[[str], Any] | None = None,
) -> None: ) -> None:
# note: some screens may override this # note: some screens may override this
web_context = web_context or BottomToolbar(self) web_context = web_context or BottomToolbar(self)

View file

@ -36,7 +36,7 @@ import threading
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
from operator import attrgetter from operator import attrgetter
from typing import Any, List, Optional, cast from typing import Any, cast
import anki import anki
from anki import hooks from anki import hooks
@ -61,17 +61,17 @@ class TTSVoiceMatch:
class TTSPlayer: class TTSPlayer:
default_rank = 0 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 [] return []
def voices(self) -> List[TTSVoice]: def voices(self) -> list[TTSVoice]:
if self._available_voices is None: if self._available_voices is None:
self._available_voices = self.get_available_voices() self._available_voices = self.get_available_voices()
return self._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() avail_voices = self.voices()
rank = self.default_rank rank = self.default_rank
@ -103,7 +103,7 @@ class TTSPlayer:
class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer): class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
# mypy gets confused if rank_for_tag is defined in 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): if not isinstance(tag, TTSTag):
return None 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 from aqt.sound import av_player
all_voices: List[TTSVoice] = [] all_voices: list[TTSVoice] = []
for p in av_player.players: for p in av_player.players:
getter = getattr(p, "voices", None) getter = getattr(p, "voices", None)
if not getter: if not getter:
@ -185,7 +185,7 @@ class MacTTSPlayer(TTSProcessPlayer):
self._process.stdin.close() self._process.stdin.close()
self._wait_for_termination(tag) self._wait_for_termination(tag)
def get_available_voices(self) -> List[TTSVoice]: def get_available_voices(self) -> list[TTSVoice]:
cmd = subprocess.run( cmd = subprocess.run(
["say", "-v", "?"], capture_output=True, check=True, encoding="utf8" ["say", "-v", "?"], capture_output=True, check=True, encoding="utf8"
) )
@ -197,7 +197,7 @@ class MacTTSPlayer(TTSProcessPlayer):
voices.append(voice) voices.append(voice)
return voices 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) m = self.VOICE_HELP_LINE_RE.match(line)
if not m: if not m:
return None return None
@ -484,7 +484,7 @@ if isWin:
except: except:
speaker = None speaker = None
def get_available_voices(self) -> List[TTSVoice]: def get_available_voices(self) -> list[TTSVoice]:
if self.speaker is None: if self.speaker is None:
return [] return []
return list(map(self._voice_to_object, self.speaker.GetVoices())) return list(map(self._voice_to_object, self.speaker.GetVoices()))
@ -533,7 +533,7 @@ if isWin:
id: Any id: Any
class WindowsRTTTSFilePlayer(TTSProcessPlayer): class WindowsRTTTSFilePlayer(TTSProcessPlayer):
voice_list: List[Any] = [] voice_list: list[Any] = []
tmppath = os.path.join(tmpdir(), "tts.wav") tmppath = os.path.join(tmpdir(), "tts.wav")
def import_voices(self) -> None: def import_voices(self) -> None:
@ -545,7 +545,7 @@ if isWin:
print("winrt tts voices unavailable:", e) print("winrt tts voices unavailable:", e)
self.voice_list = [] 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 = threading.Thread(target=self.import_voices)
t.start() t.start()
t.join() t.join()

View file

@ -2,7 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import time import time
from typing import Any, Dict from typing import Any
import requests import requests
@ -24,7 +24,7 @@ class LatestVersionFinder(QThread):
self.main = main self.main = main
self.config = main.pm.meta self.config = main.pm.meta
def _data(self) -> Dict[str, Any]: def _data(self) -> dict[str, Any]:
return { return {
"ver": versionWithBuild(), "ver": versionWithBuild(),
"os": platDesc(), "os": platDesc(),
@ -73,6 +73,6 @@ def askAndUpdate(mw: aqt.AnkiQt, ver: str) -> None:
openLink(aqt.appWebsite) 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") showText(data["msg"], parent=mw, type="html")
mw.pm.meta["lastMsg"] = data["msgId"] mw.pm.meta["lastMsg"] = data["msgId"]

View file

@ -7,18 +7,7 @@ import re
import subprocess import subprocess
import sys import sys
from functools import wraps from functools import wraps
from typing import ( from typing import TYPE_CHECKING, Any, Literal, Sequence, cast
TYPE_CHECKING,
Any,
Callable,
List,
Literal,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAction, QAction,
@ -79,7 +68,7 @@ def openHelp(section: HelpPageArgument) -> None:
openLink(link) openLink(link)
def openLink(link: Union[str, QUrl]) -> None: def openLink(link: str | QUrl) -> None:
tooltip(tr.qt_misc_loading(), period=1000) tooltip(tr.qt_misc_loading(), period=1000)
with noBundledLibs(): with noBundledLibs():
QDesktopServices.openUrl(QUrl(link)) QDesktopServices.openUrl(QUrl(link))
@ -87,10 +76,10 @@ def openLink(link: Union[str, QUrl]) -> None:
def showWarning( def showWarning(
text: str, text: str,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
help: HelpPageArgument = "", help: HelpPageArgument = "",
title: str = "Anki", title: str = "Anki",
textFormat: Optional[TextFormat] = None, textFormat: TextFormat | None = None,
) -> int: ) -> int:
"Show a small warning with an OK button." "Show a small warning with an OK button."
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat) return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
@ -98,10 +87,10 @@ def showWarning(
def showCritical( def showCritical(
text: str, text: str,
parent: Optional[QDialog] = None, parent: QDialog | None = None,
help: str = "", help: str = "",
title: str = "Anki", title: str = "Anki",
textFormat: Optional[TextFormat] = None, textFormat: TextFormat | None = None,
) -> int: ) -> int:
"Show a small critical error with an OK button." "Show a small critical error with an OK button."
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat) return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
@ -109,12 +98,12 @@ def showCritical(
def showInfo( def showInfo(
text: str, text: str,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
help: HelpPageArgument = "", help: HelpPageArgument = "",
type: str = "info", type: str = "info",
title: str = "Anki", title: str = "Anki",
textFormat: Optional[TextFormat] = None, textFormat: TextFormat | None = None,
customBtns: Optional[List[QMessageBox.StandardButton]] = None, customBtns: list[QMessageBox.StandardButton] | None = None,
) -> int: ) -> int:
"Show a small info window with an OK button." "Show a small info window with an OK button."
parent_widget: QWidget parent_widget: QWidget
@ -157,16 +146,16 @@ def showInfo(
def showText( def showText(
txt: str, txt: str,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
type: str = "text", type: str = "text",
run: bool = True, run: bool = True,
geomKey: Optional[str] = None, geomKey: str | None = None,
minWidth: int = 500, minWidth: int = 500,
minHeight: int = 400, minHeight: int = 400,
title: str = "Anki", title: str = "Anki",
copyBtn: bool = False, copyBtn: bool = False,
plain_text_edit: bool = False, plain_text_edit: bool = False,
) -> Optional[Tuple[QDialog, QDialogButtonBox]]: ) -> tuple[QDialog, QDialogButtonBox] | None:
if not parent: if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw parent = aqt.mw.app.activeWindow() or aqt.mw
diag = QDialog(parent) diag = QDialog(parent)
@ -174,7 +163,7 @@ def showText(
disable_help_button(diag) disable_help_button(diag)
layout = QVBoxLayout(diag) layout = QVBoxLayout(diag)
diag.setLayout(layout) diag.setLayout(layout)
text: Union[QPlainTextEdit, QTextBrowser] text: QPlainTextEdit | QTextBrowser
if plain_text_edit: if plain_text_edit:
# used by the importer # used by the importer
text = QPlainTextEdit() text = QPlainTextEdit()
@ -228,7 +217,7 @@ def askUser(
parent: QWidget = None, parent: QWidget = None,
help: HelpPageArgument = None, help: HelpPageArgument = None,
defaultno: bool = False, defaultno: bool = False,
msgfunc: Optional[Callable] = None, msgfunc: Callable | None = None,
title: str = "Anki", title: str = "Anki",
) -> bool: ) -> bool:
"Show a yes/no question. Return true if yes." "Show a yes/no question. Return true if yes."
@ -256,13 +245,13 @@ class ButtonedDialog(QMessageBox):
def __init__( def __init__(
self, self,
text: str, text: str,
buttons: List[str], buttons: list[str],
parent: Optional[QWidget] = None, parent: QWidget | None = None,
help: HelpPageArgument = None, help: HelpPageArgument = None,
title: str = "Anki", title: str = "Anki",
): ):
QMessageBox.__init__(self, parent) QMessageBox.__init__(self, parent)
self._buttons: List[QPushButton] = [] self._buttons: list[QPushButton] = []
self.setWindowTitle(title) self.setWindowTitle(title)
self.help = help self.help = help
self.setIcon(QMessageBox.Warning) self.setIcon(QMessageBox.Warning)
@ -289,8 +278,8 @@ class ButtonedDialog(QMessageBox):
def askUserDialog( def askUserDialog(
text: str, text: str,
buttons: List[str], buttons: list[str],
parent: Optional[QWidget] = None, parent: QWidget | None = None,
help: HelpPageArgument = None, help: HelpPageArgument = None,
title: str = "Anki", title: str = "Anki",
) -> ButtonedDialog: ) -> ButtonedDialog:
@ -303,10 +292,10 @@ def askUserDialog(
class GetTextDialog(QDialog): class GetTextDialog(QDialog):
def __init__( def __init__(
self, self,
parent: Optional[QWidget], parent: QWidget | None,
question: str, question: str,
help: HelpPageArgument = None, help: HelpPageArgument = None,
edit: Optional[QLineEdit] = None, edit: QLineEdit | None = None,
default: str = "", default: str = "",
title: str = "Anki", title: str = "Anki",
minWidth: int = 400, minWidth: int = 400,
@ -350,14 +339,14 @@ class GetTextDialog(QDialog):
def getText( def getText(
prompt: str, prompt: str,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
help: HelpPageArgument = None, help: HelpPageArgument = None,
edit: Optional[QLineEdit] = None, edit: QLineEdit | None = None,
default: str = "", default: str = "",
title: str = "Anki", title: str = "Anki",
geomKey: Optional[str] = None, geomKey: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> Tuple[str, int]: ) -> tuple[str, int]:
"Returns (string, succeeded)." "Returns (string, succeeded)."
if not parent: if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw 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 # fixme: these utilities could be combined into a single base class
# unused by Anki, but used by add-ons # unused by Anki, but used by add-ons
def chooseList( 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: ) -> int:
if not parent: if not parent:
parent = aqt.mw.app.activeWindow() parent = aqt.mw.app.activeWindow()
@ -408,7 +397,7 @@ def chooseList(
def getTag( def getTag(
parent: QWidget, deck: Collection, question: str, **kwargs: Any parent: QWidget, deck: Collection, question: str, **kwargs: Any
) -> Tuple[str, int]: ) -> tuple[str, int]:
from aqt.tagedit import TagEdit from aqt.tagedit import TagEdit
te = TagEdit(parent) te = TagEdit(parent)
@ -433,12 +422,12 @@ def getFile(
parent: QWidget, parent: QWidget,
title: str, title: str,
# single file returned unless multi=True # single file returned unless multi=True
cb: Optional[Callable[[Union[str, Sequence[str]]], None]], cb: Callable[[str | Sequence[str]], None] | None,
filter: str = "*.*", filter: str = "*.*",
dir: Optional[str] = None, dir: str | None = None,
key: Optional[str] = None, key: str | None = None,
multi: bool = False, # controls whether a single or multiple files is returned 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." "Ask the user for a file."
assert not dir or not key assert not dir or not key
if not dir: if not dir:
@ -480,7 +469,7 @@ def getSaveFile(
dir_description: str, dir_description: str,
key: str, key: str,
ext: str, ext: str,
fname: Optional[str] = None, fname: str | None = None,
) -> str: ) -> str:
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config """Ask the user for a file to save. Use DIR_DESCRIPTION as config
variable. The file dialog will default to open with FNAME.""" variable. The file dialog will default to open with FNAME."""
@ -520,7 +509,7 @@ def saveGeom(widget: QWidget, key: str) -> None:
def restoreGeom( 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: ) -> None:
key += "Geom" key += "Geom"
if aqt.mw.pm.profile.get(key): if aqt.mw.pm.profile.get(key):
@ -562,12 +551,12 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
widget.move(x, y) widget.move(x, y)
def saveState(widget: Union[QFileDialog, QMainWindow], key: str) -> None: def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
key += "State" key += "State"
aqt.mw.pm.profile[key] = widget.saveState() 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" key += "State"
if aqt.mw.pm.profile.get(key): if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[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( def restore_combo_index_for_session(
widget: QComboBox, history: List[str], key: str widget: QComboBox, history: list[str], key: str
) -> None: ) -> None:
textKey = f"{key}ComboActiveText" textKey = f"{key}ComboActiveText"
indexKey = f"{key}ComboActiveIndex" indexKey = f"{key}ComboActiveIndex"
@ -625,7 +614,7 @@ def restore_combo_index_for_session(
widget.setCurrentIndex(index) 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" name += "BoxHistory"
text_input = comboBox.lineEdit().text() text_input = comboBox.lineEdit().text()
if text_input in history: if text_input in history:
@ -639,7 +628,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> st
return text_input 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" name += "BoxHistory"
history = aqt.mw.pm.profile.get(name, []) history = aqt.mw.pm.profile.get(name, [])
comboBox.addItems([""] + history) comboBox.addItems([""] + history)
@ -693,7 +682,7 @@ def downArrow() -> str:
return "" return ""
def current_window() -> Optional[QWidget]: def current_window() -> QWidget | None:
if widget := QApplication.focusWidget(): if widget := QApplication.focusWidget():
return widget.window() return widget.window()
else: else:
@ -703,14 +692,14 @@ def current_window() -> Optional[QWidget]:
# Tooltips # Tooltips
###################################################################### ######################################################################
_tooltipTimer: Optional[QTimer] = None _tooltipTimer: QTimer | None = None
_tooltipLabel: Optional[QLabel] = None _tooltipLabel: QLabel | None = None
def tooltip( def tooltip(
msg: str, msg: str,
period: int = 3000, period: int = 3000,
parent: Optional[QWidget] = None, parent: QWidget | None = None,
x_offset: int = 0, x_offset: int = 0,
y_offset: int = 100, y_offset: int = 100,
) -> None: ) -> None:
@ -785,7 +774,7 @@ class MenuList:
print( print(
"MenuList will be removed; please copy it into your add-on's code if you need it." "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: def addItem(self, title: str, func: Callable) -> MenuItem:
item = MenuItem(title, func) item = MenuItem(title, func)
@ -800,7 +789,7 @@ class MenuList:
self.children.append(submenu) self.children.append(submenu)
return submenu return submenu
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None: def addChild(self, child: SubMenu | QAction | MenuList) -> None:
self.children.append(child) self.children.append(child)
def renderTo(self, qmenu: QMenu) -> None: def renderTo(self, qmenu: QMenu) -> None:
@ -894,7 +883,7 @@ Add-ons, last update check: {}
###################################################################### ######################################################################
# adapted from version detection in qutebrowser # adapted from version detection in qutebrowser
def opengl_vendor() -> Optional[str]: def opengl_vendor() -> str | None:
old_context = QOpenGLContext.currentContext() old_context = QOpenGLContext.currentContext()
old_surface = None if old_context is None else old_context.surface() old_surface = None if old_context is None else old_context.surface()

View file

@ -5,7 +5,7 @@ import dataclasses
import json import json
import re import re
import sys import sys
from typing import Any, Callable, List, Optional, Sequence, Tuple, cast from typing import Any, Callable, Optional, Sequence, cast
import anki import anki
from anki.lang import is_rtl from anki.lang import is_rtl
@ -206,8 +206,8 @@ class WebContent:
body: str = "" body: str = ""
head: str = "" head: str = ""
css: List[str] = dataclasses.field(default_factory=lambda: []) css: list[str] = dataclasses.field(default_factory=lambda: [])
js: List[str] = dataclasses.field(default_factory=lambda: []) js: list[str] = dataclasses.field(default_factory=lambda: [])
# Main web view # Main web view
@ -232,7 +232,7 @@ class AnkiWebView(QWebEngineView):
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True self._domDone = True
self._pendingActions: List[Tuple[str, Sequence[Any]]] = [] self._pendingActions: list[tuple[str, Sequence[Any]]] = []
self.requiresCol = True self.requiresCol = True
self.setPage(self._page) self.setPage(self._page)
@ -415,22 +415,22 @@ border-radius:5px; font-family: Helvetica }"""
font = f'font-size:14px;font-family:"{family}";' font = f'font-size:14px;font-family:"{family}";'
button_style = """ button_style = """
/* Buttons */ /* Buttons */
button{ button{{
background-color: %(color_btn)s; background-color: {color_btn};
font-family:"%(family)s"; } font-family:"{family}"; }}
button:focus{ border-color: %(color_hl)s } button:focus{{ border-color: {color_hl} }}
button:active, button:active:hover { background-color: %(color_hl)s; color: %(color_hl_txt)s;} button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}}
/* Input field focus outline */ /* Input field focus outline */
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus, textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
div[contenteditable="true"]:focus { div[contenteditable="true"]:focus {{
outline: 0 none; outline: 0 none;
border-color: %(color_hl)s; border-color: {color_hl};
}""" % { }}""".format(
"family": family, family=family,
"color_btn": color_btn, color_btn=color_btn,
"color_hl": color_hl, color_hl=color_hl,
"color_hl_txt": color_hl_txt, color_hl_txt=color_hl_txt,
} )
zoom = self.zoomFactor() zoom = self.zoomFactor()
@ -454,8 +454,8 @@ html {{ {font} }}
def stdHtml( def stdHtml(
self, self,
body: str, body: str,
css: Optional[List[str]] = None, css: Optional[list[str]] = None,
js: Optional[List[str]] = None, js: Optional[list[str]] = None,
head: str = "", head: str = "",
context: Optional[Any] = None, context: Optional[Any] = None,
default_css: bool = True, default_css: bool = True,

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" """
System File Locations System File Locations
Retrieves common system path names on Windows XP/Vista 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" __description__ = "Retrieves common Windows system paths as Unicode strings"
class PathConstants(object): class PathConstants:
""" """
Define constants here to avoid dependency on shellcon. Define constants here to avoid dependency on shellcon.
Put it in a class to avoid polluting namespace Put it in a class to avoid polluting namespace