diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index a54dae3c1..f3dd649b2 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations import sys import traceback -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Sequence, Union from weakref import ref from markdown import markdown @@ -59,7 +59,7 @@ class RustBackend(RustBackendGenerated): def __init__( self, - langs: Optional[List[str]] = None, + langs: list[str] | None = None, server: bool = False, ) -> None: # pick up global defaults if not provided @@ -74,12 +74,12 @@ class RustBackend(RustBackendGenerated): def db_query( self, sql: str, args: Sequence[ValueForDB], first_row_only: bool - ) -> List[DBRow]: + ) -> list[DBRow]: return self._db_command( dict(kind="query", sql=sql, args=args, first_row_only=first_row_only) ) - def db_execute_many(self, sql: str, args: List[List[ValueForDB]]) -> List[DBRow]: + def db_execute_many(self, sql: str, args: list[list[ValueForDB]]) -> list[DBRow]: return self._db_command(dict(kind="executemany", sql=sql, args=args)) def db_begin(self) -> None: @@ -91,7 +91,7 @@ class RustBackend(RustBackendGenerated): def db_rollback(self) -> None: return self._db_command(dict(kind="rollback")) - def _db_command(self, input: Dict[str, Any]) -> Any: + def _db_command(self, input: dict[str, Any]) -> Any: bytes_input = to_json_bytes(input) try: return from_json_bytes(self._backend.db_command(bytes_input)) @@ -102,7 +102,7 @@ class RustBackend(RustBackendGenerated): raise backend_exception_to_pylib(err) def translate( - self, module_index: int, message_index: int, **kwargs: Union[str, int, float] + self, module_index: int, message_index: int, **kwargs: str | int | float ) -> str: return self.translate_string( translate_string_in( @@ -133,7 +133,7 @@ class RustBackend(RustBackendGenerated): def translate_string_in( - module_index: int, message_index: int, **kwargs: Union[str, int, float] + module_index: int, message_index: int, **kwargs: str | int | float ) -> i18n_pb2.TranslateStringRequest: args = {} for (k, v) in kwargs.items(): @@ -147,10 +147,10 @@ def translate_string_in( class Translations(GeneratedTranslations): - def __init__(self, backend: Optional[ref[RustBackend]]): + def __init__(self, backend: ref[RustBackend] | None): self.backend = backend - def __call__(self, key: Tuple[int, int], **kwargs: Any) -> str: + def __call__(self, key: tuple[int, int], **kwargs: Any) -> str: "Mimic the old col.tr / TR interface" if "pytest" not in sys.modules: traceback.print_stack(file=sys.stdout) @@ -162,7 +162,7 @@ class Translations(GeneratedTranslations): ) def _translate( - self, module: int, message: int, args: Dict[str, Union[str, int, float]] + self, module: int, message: int, args: dict[str, str | int | float] ) -> str: return self.backend().translate( module_index=module, message_index=message, **args diff --git a/pylib/anki/_backend/genfluent.py b/pylib/anki/_backend/genfluent.py index 09330265c..fae0c37cd 100644 --- a/pylib/anki/_backend/genfluent.py +++ b/pylib/anki/_backend/genfluent.py @@ -50,7 +50,7 @@ def methods() -> str: return "\n".join(out) + "\n" -def get_arg_types(args: List[Variable]) -> str: +def get_arg_types(args: list[Variable]) -> str: return ", ".join( [f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args] @@ -68,7 +68,7 @@ def arg_kind(arg: Variable) -> str: return "str" -def get_args(args: List[Variable]) -> str: +def get_args(args: list[Variable]) -> str: return ", ".join( [f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args] ) diff --git a/pylib/anki/_legacy.py b/pylib/anki/_legacy.py index 3603cb39d..7528de6a1 100644 --- a/pylib/anki/_legacy.py +++ b/pylib/anki/_legacy.py @@ -7,11 +7,11 @@ import functools import os import pathlib import traceback -from typing import Any, Callable, Dict, Optional, Tuple, Union, no_type_check +from typing import Any, Callable, Union, no_type_check import stringcase -VariableTarget = Tuple[Any, str] +VariableTarget = tuple[Any, str] DeprecatedAliasTarget = Union[Callable, VariableTarget] @@ -43,7 +43,7 @@ class DeprecatedNamesMixin: # the @no_type_check lines are required to prevent mypy allowing arbitrary # attributes on the consuming class - _deprecated_aliases: Dict[str, str] = {} + _deprecated_aliases: dict[str, str] = {} @no_type_check def __getattr__(self, name: str) -> Any: @@ -68,7 +68,7 @@ class DeprecatedNamesMixin: cls._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()} -def deprecated(replaced_by: Optional[Callable] = None, info: str = "") -> Callable: +def deprecated(replaced_by: Callable | None = None, info: str = "") -> Callable: """Print a deprecation warning, telling users to use `replaced_by`, or show `doc`.""" def decorator(func: Callable) -> Callable: diff --git a/pylib/anki/browser.py b/pylib/anki/browser.py index 7399fa452..c96fb9c80 100644 --- a/pylib/anki/browser.py +++ b/pylib/anki/browser.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/pylib/anki/buildinfo.py b/pylib/anki/buildinfo.py index afa52b65b..bb388d5be 100644 --- a/pylib/anki/buildinfo.py +++ b/pylib/anki/buildinfo.py @@ -3,7 +3,6 @@ import os import sys -from typing import Dict def _build_info_path() -> str: @@ -19,7 +18,7 @@ def _build_info_path() -> str: raise Exception("missing buildinfo.txt") -def _get_build_info() -> Dict[str, str]: +def _get_build_info() -> dict[str, str]: info = {} with open(_build_info_path(), encoding="utf8") as file: for line in file.readlines(): diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 2f7a50f18..0d7785ec2 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -7,7 +7,6 @@ from __future__ import annotations import pprint import time -from typing import List, NewType, Optional import anki # pylint: disable=unused-import from anki import cards_pb2, hooks @@ -34,7 +33,7 @@ BackendCard = cards_pb2.Card class Card(DeprecatedNamesMixin): - _note: Optional[Note] + _note: Note | None lastIvl: int ord: int nid: anki.notes.NoteId @@ -47,12 +46,12 @@ class Card(DeprecatedNamesMixin): def __init__( self, col: anki.collection.Collection, - id: Optional[CardId] = None, - backend_card: Optional[BackendCard] = None, + id: CardId | None = None, + backend_card: BackendCard | None = None, ) -> None: self.col = col.weakref() - self.timer_started: Optional[float] = None - self._render_output: Optional[anki.template.TemplateRenderOutput] = None + self.timer_started: float | None = None + self._render_output: anki.template.TemplateRenderOutput | None = None if id: # existing card self.id = id @@ -126,10 +125,10 @@ class Card(DeprecatedNamesMixin): def answer(self) -> str: return self.render_output().answer_and_style() - def question_av_tags(self) -> List[AVTag]: + def question_av_tags(self) -> list[AVTag]: return self.render_output().question_av_tags - def answer_av_tags(self) -> List[AVTag]: + def answer_av_tags(self) -> list[AVTag]: return self.render_output().answer_av_tags def render_output( diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 770b9b30f..5cd2f350b 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Any, Generator, List, Literal, Optional, Sequence, Tuple, Union, cast +from typing import Any, Generator, Literal, Sequence, Union, cast from anki import ( card_rendering_pb2, @@ -92,17 +92,17 @@ LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo] class Collection(DeprecatedNamesMixin): - sched: Union[V1Scheduler, V2Scheduler, V3Scheduler] + sched: V1Scheduler | V2Scheduler | V3Scheduler def __init__( self, path: str, - backend: Optional[RustBackend] = None, + backend: RustBackend | None = None, server: bool = False, log: bool = False, ) -> None: self._backend = backend or RustBackend(server=server) - self.db: Optional[DBProxy] = None + self.db: DBProxy | None = None self._should_log = log self.server = server self.path = os.path.abspath(path) @@ -211,7 +211,7 @@ class Collection(DeprecatedNamesMixin): # to check if the backend updated the modification time. return self.db.last_begin_at != self.mod - def save(self, name: Optional[str] = None, trx: bool = True) -> None: + def save(self, name: str | None = None, trx: bool = True) -> None: "Flush, commit DB, and take out another write lock if trx=True." # commit needed? if self.db.modified_in_python or self.modified_by_backend(): @@ -379,7 +379,7 @@ class Collection(DeprecatedNamesMixin): hooks.notes_will_be_deleted(self, note_ids) return self._backend.remove_notes(note_ids=note_ids, card_ids=[]) - def remove_notes_by_card(self, card_ids: List[CardId]) -> None: + def remove_notes_by_card(self, card_ids: list[CardId]) -> None: if hooks.notes_will_be_deleted.count(): nids = self.db.list( f"select nid from cards where id in {ids2str(card_ids)}" @@ -391,7 +391,7 @@ class Collection(DeprecatedNamesMixin): return [CardId(id) for id in self._backend.cards_of_note(note_id)] def defaults_for_adding( - self, *, current_review_card: Optional[Card] + self, *, current_review_card: Card | None ) -> anki.notes.DefaultsForAdding: """Get starting deck and notetype for add screen. An option in the preferences controls whether this will be based on the current deck @@ -406,7 +406,7 @@ class Collection(DeprecatedNamesMixin): home_deck_of_current_review_card=home_deck, ) - def default_deck_for_notetype(self, notetype_id: NotetypeId) -> Optional[DeckId]: + def default_deck_for_notetype(self, notetype_id: NotetypeId) -> DeckId | None: """If 'change deck depending on notetype' is enabled in the preferences, return the last deck used with the provided notetype, if any..""" if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK): @@ -447,7 +447,7 @@ class Collection(DeprecatedNamesMixin): ########################################################################## def after_note_updates( - self, nids: List[NoteId], mark_modified: bool, generate_cards: bool = True + self, nids: list[NoteId], mark_modified: bool, generate_cards: bool = True ) -> None: "If notes modified directly in database, call this afterwards." self._backend.after_note_updates( @@ -460,7 +460,7 @@ class Collection(DeprecatedNamesMixin): def find_cards( self, query: str, - order: Union[bool, str, BrowserColumns.Column] = False, + order: bool | str | BrowserColumns.Column = False, reverse: bool = False, ) -> Sequence[CardId]: """Return card ids matching the provided search. @@ -491,7 +491,7 @@ class Collection(DeprecatedNamesMixin): def find_notes( self, query: str, - order: Union[bool, str, BrowserColumns.Column] = False, + order: bool | str | BrowserColumns.Column = False, reverse: bool = False, ) -> Sequence[NoteId]: """Return note ids matching the provided search. @@ -506,7 +506,7 @@ class Collection(DeprecatedNamesMixin): def _build_sort_mode( self, - order: Union[bool, str, BrowserColumns.Column], + order: bool | str | BrowserColumns.Column, reverse: bool, finding_notes: bool, ) -> search_pb2.SortOrder: @@ -539,7 +539,7 @@ class Collection(DeprecatedNamesMixin): search: str, replacement: str, regex: bool = False, - field_name: Optional[str] = None, + field_name: str | None = None, match_case: bool = False, ) -> OpChangesWithCount: "Find and replace fields in a note. Returns changed note count." @@ -556,14 +556,14 @@ class Collection(DeprecatedNamesMixin): return self._backend.field_names_for_notes(nids) # returns array of ("dupestr", [nids]) - def find_dupes(self, field_name: str, search: str = "") -> List[Tuple[str, list]]: + def find_dupes(self, field_name: str, search: str = "") -> list[tuple[str, list]]: nids = self.find_notes( self.build_search_string(search, SearchNode(field_name=field_name)) ) # go through notes - vals: Dict[str, List[int]] = {} + vals: dict[str, list[int]] = {} dupes = [] - fields: Dict[int, int] = {} + fields: dict[int, int] = {} def ord_for_mid(mid: NotetypeId) -> int: if mid not in fields: @@ -596,7 +596,7 @@ class Collection(DeprecatedNamesMixin): def build_search_string( self, - *nodes: Union[str, SearchNode], + *nodes: str | SearchNode, joiner: SearchJoiner = "AND", ) -> str: """Join one or more searches, and return a normalized search string. @@ -612,7 +612,7 @@ class Collection(DeprecatedNamesMixin): def group_searches( self, - *nodes: Union[str, SearchNode], + *nodes: str | SearchNode, joiner: SearchJoiner = "AND", ) -> SearchNode: """Join provided search nodes and strings into a single SearchNode. @@ -680,7 +680,7 @@ class Collection(DeprecatedNamesMixin): def all_browser_columns(self) -> Sequence[BrowserColumns.Column]: return self._backend.all_browser_columns() - def get_browser_column(self, key: str) -> Optional[BrowserColumns.Column]: + def get_browser_column(self, key: str) -> BrowserColumns.Column | None: for column in self._backend.all_browser_columns(): if column.key == key: return column @@ -688,7 +688,7 @@ class Collection(DeprecatedNamesMixin): def browser_row_for_id( self, id_: int - ) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]: + ) -> tuple[Generator[tuple[str, bool], None, None], BrowserRow.Color.V, str, int]: row = self._backend.browser_row_for_id(id_) return ( ((cell.text, cell.is_rtl) for cell in row.cells), @@ -697,7 +697,7 @@ class Collection(DeprecatedNamesMixin): row.font_size, ) - def load_browser_card_columns(self) -> List[str]: + def load_browser_card_columns(self) -> list[str]: """Return the stored card column names and ensure the backend columns are set and in sync.""" columns = self.get_config( BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS @@ -705,11 +705,11 @@ class Collection(DeprecatedNamesMixin): self._backend.set_active_browser_columns(columns) return columns - def set_browser_card_columns(self, columns: List[str]) -> None: + def set_browser_card_columns(self, columns: list[str]) -> None: self.set_config(BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, columns) self._backend.set_active_browser_columns(columns) - def load_browser_note_columns(self) -> List[str]: + def load_browser_note_columns(self) -> list[str]: """Return the stored note column names and ensure the backend columns are set and in sync.""" columns = self.get_config( BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS @@ -717,7 +717,7 @@ class Collection(DeprecatedNamesMixin): self._backend.set_active_browser_columns(columns) return columns - def set_browser_note_columns(self, columns: List[str]) -> None: + def set_browser_note_columns(self, columns: list[str]) -> None: self.set_config(BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, columns) self._backend.set_active_browser_columns(columns) @@ -745,7 +745,7 @@ class Collection(DeprecatedNamesMixin): def remove_config(self, key: str) -> OpChanges: return self.conf.remove(key) - def all_config(self) -> Dict[str, Any]: + def all_config(self) -> dict[str, Any]: "This is a debugging aid. Prefer .get_config() when you know the key you need." return from_json_bytes(self._backend.get_all_config()) @@ -802,7 +802,7 @@ class Collection(DeprecatedNamesMixin): # Stats ########################################################################## - def stats(self) -> "anki.stats.CollectionStats": + def stats(self) -> anki.stats.CollectionStats: from anki.stats import CollectionStats return CollectionStats(self) @@ -926,7 +926,7 @@ table.review-log {{ {revlog_style} }} return True return False - def _check_backend_undo_status(self) -> Optional[UndoStatus]: + def _check_backend_undo_status(self) -> UndoStatus | None: """Return undo status if undo available on backend. If backend has undo available, clear the Python undo state.""" status = self._backend.get_undo_status() @@ -956,7 +956,7 @@ table.review-log {{ {revlog_style} }} self.clear_python_undo() return undo - def _save_checkpoint(self, name: Optional[str]) -> None: + def _save_checkpoint(self, name: str | None) -> None: "Call via .save(). If name not provided, clear any existing checkpoint." self._last_checkpoint_at = time.time() if name: @@ -1017,7 +1017,7 @@ table.review-log {{ {revlog_style} }} # DB maintenance ########################################################################## - def fix_integrity(self) -> Tuple[str, bool]: + def fix_integrity(self) -> tuple[str, bool]: """Fix possible problems and rebuild caches. Returns tuple of (error: str, ok: bool). 'ok' will be true if no @@ -1108,7 +1108,7 @@ table.review-log {{ {revlog_style} }} self._startTime = time.time() self._startReps = self.sched.reps - def timeboxReached(self) -> Union[Literal[False], Tuple[Any, int]]: + def timeboxReached(self) -> Literal[False] | tuple[Any, int]: "Return (elapsedTime, reps) if timebox reached, or False." if not self.conf["timeLim"]: # timeboxing disabled @@ -1126,7 +1126,7 @@ table.review-log {{ {revlog_style} }} print(args, kwargs) @deprecated(replaced_by=undo_status) - def undo_name(self) -> Optional[str]: + def undo_name(self) -> str | None: "Undo menu item name, or None if undo unavailable." status = self.undo_status() return status.undo or None @@ -1146,7 +1146,7 @@ table.review-log {{ {revlog_style} }} self.remove_notes(ids) @deprecated(replaced_by=remove_notes) - def _remNotes(self, ids: List[NoteId]) -> None: + def _remNotes(self, ids: list[NoteId]) -> None: pass @deprecated(replaced_by=card_stats) @@ -1154,21 +1154,21 @@ table.review-log {{ {revlog_style} }} return self.card_stats(card.id, include_revlog=False) @deprecated(replaced_by=after_note_updates) - def updateFieldCache(self, nids: List[NoteId]) -> None: + def updateFieldCache(self, nids: list[NoteId]) -> None: self.after_note_updates(nids, mark_modified=False, generate_cards=False) @deprecated(replaced_by=after_note_updates) - def genCards(self, nids: List[NoteId]) -> List[int]: + def genCards(self, nids: list[NoteId]) -> list[int]: self.after_note_updates(nids, mark_modified=False, generate_cards=True) # previously returned empty cards, no longer does return [] @deprecated(info="no longer used") - def emptyCids(self) -> List[CardId]: + def emptyCids(self) -> list[CardId]: return [] @deprecated(info="handled by backend") - def _logRem(self, ids: List[Union[int, NoteId]], type: int) -> None: + def _logRem(self, ids: list[int | NoteId], type: int) -> None: self.db.executemany( "insert into graves values (%d, ?, %d)" % (self.usn(), type), ([x] for x in ids), @@ -1197,7 +1197,7 @@ _Collection = Collection @dataclass class _ReviewsUndo: - entries: List[LegacyReviewUndo] = field(default_factory=list) + entries: list[LegacyReviewUndo] = field(default_factory=list) _UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None] diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index f26388245..98ba97b90 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -4,7 +4,7 @@ from __future__ import annotations import sys -from typing import Any, Dict, NewType, Optional +from typing import Any, NewType # whether new cards should be mixed with reviews, or shown first or last NEW_CARDS_DISTRIBUTE = 0 @@ -94,7 +94,7 @@ REVLOG_RESCHED = 4 import anki.collection -def _tr(col: Optional[anki.collection.Collection]) -> Any: +def _tr(col: anki.collection.Collection | None) -> Any: if col: return col.tr else: @@ -107,7 +107,7 @@ def _tr(col: Optional[anki.collection.Collection]) -> Any: return tr_legacyglobal -def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, Any]: +def newCardOrderLabels(col: anki.collection.Collection | None) -> dict[int, Any]: tr = _tr(col) return { 0: tr.scheduling_show_new_cards_in_random_order(), @@ -116,8 +116,8 @@ def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, A def newCardSchedulingLabels( - col: Optional[anki.collection.Collection], -) -> Dict[int, Any]: + col: anki.collection.Collection | None, +) -> dict[int, Any]: tr = _tr(col) return { 0: tr.scheduling_mix_new_cards_and_reviews(), diff --git a/pylib/anki/db.py b/pylib/anki/db.py index ae5d55eac..5d55839fb 100644 --- a/pylib/anki/db.py +++ b/pylib/anki/db.py @@ -9,12 +9,14 @@ but this class is still used by aqt's profile manager, and a number of add-ons rely on it. """ +from __future__ import annotations + import os import pprint import time from sqlite3 import Cursor from sqlite3 import dbapi2 as sqlite -from typing import Any, List, Type +from typing import Any DBError = sqlite.Error @@ -82,7 +84,7 @@ class DB: return res[0] return None - def all(self, *a: Any, **kw: Any) -> List: + def all(self, *a: Any, **kw: Any) -> list: return self.execute(*a, **kw).fetchall() def first(self, *a: Any, **kw: Any) -> Any: @@ -91,7 +93,7 @@ class DB: c.close() return res - def list(self, *a: Any, **kw: Any) -> List: + def list(self, *a: Any, **kw: Any) -> list: return [x[0] for x in self.execute(*a, **kw)] def close(self) -> None: @@ -124,5 +126,5 @@ class DB: def _textFactory(self, data: bytes) -> str: return str(data, errors="ignore") - def cursor(self, factory: Type[Cursor] = Cursor) -> Cursor: + def cursor(self, factory: type[Cursor] = Cursor) -> Cursor: return self._db.cursor(factory) diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 45c391383..9d0b25b89 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -5,7 +5,7 @@ from __future__ import annotations import re from re import Match -from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union +from typing import Any, Iterable, Sequence, Union import anki._backend @@ -49,7 +49,7 @@ class DBProxy: *args: ValueForDB, first_row_only: bool = False, **kwargs: ValueForDB, - ) -> List[Row]: + ) -> list[Row]: # mark modified? s = sql.strip().lower() for stmt in "insert", "update", "delete": @@ -62,15 +62,15 @@ class DBProxy: # Query shortcuts ################### - def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]: + def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> list[Row]: return self._query(sql, *args, first_row_only=False, **kwargs) def list( self, sql: str, *args: ValueForDB, **kwargs: ValueForDB - ) -> List[ValueFromDB]: + ) -> list[ValueFromDB]: return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)] - def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]: + def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Row | None: rows = self._query(sql, *args, first_row_only=True, **kwargs) if rows: return rows[0] @@ -102,8 +102,8 @@ class DBProxy: # convert kwargs to list format def emulate_named_args( - sql: str, args: Tuple, kwargs: Dict[str, Any] -) -> Tuple[str, Sequence[ValueForDB]]: + sql: str, args: tuple, kwargs: dict[str, Any] +) -> tuple[str, Sequence[ValueForDB]]: # nothing to do? if not kwargs: return sql, args diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index b40216b48..5c7909391 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -6,19 +6,7 @@ from __future__ import annotations import copy -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Iterable, - List, - NewType, - Optional, - Sequence, - Tuple, - Union, - no_type_check, -) +from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence, no_type_check if TYPE_CHECKING: import anki @@ -40,8 +28,8 @@ DeckConfigsForUpdate = deckconfig_pb2.DeckConfigsForUpdate UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest # type aliases until we can move away from dicts -DeckDict = Dict[str, Any] -DeckConfigDict = Dict[str, Any] +DeckDict = dict[str, Any] +DeckConfigDict = dict[str, Any] DeckId = NewType("DeckId", int) DeckConfigId = NewType("DeckConfigId", int) @@ -96,7 +84,7 @@ class DeckManager(DeprecatedNamesMixin): self.col = col.weakref() self.decks = DecksDictProxy(col) - def save(self, deck_or_config: Union[DeckDict, DeckConfigDict] = None) -> None: + def save(self, deck_or_config: DeckDict | DeckConfigDict = None) -> None: "Can be called with either a deck or a deck configuration." if not deck_or_config: print("col.decks.save() should be passed the changed deck") @@ -131,7 +119,7 @@ class DeckManager(DeprecatedNamesMixin): name: str, create: bool = True, type: DeckConfigId = DeckConfigId(0), - ) -> Optional[DeckId]: + ) -> DeckId | None: "Add a deck with NAME. Reuse deck if already exists. Return id as int." id = self.id_for_name(name) if id: @@ -155,13 +143,13 @@ class DeckManager(DeprecatedNamesMixin): skip_empty_default=skip_empty_default, include_filtered=include_filtered ) - def id_for_name(self, name: str) -> Optional[DeckId]: + def id_for_name(self, name: str) -> DeckId | None: try: return DeckId(self.col._backend.get_deck_id_by_name(name)) except NotFoundError: return None - def get_legacy(self, did: DeckId) -> Optional[DeckDict]: + def get_legacy(self, did: DeckId) -> DeckDict | None: try: return from_json_bytes(self.col._backend.get_deck_legacy(did)) except NotFoundError: @@ -170,7 +158,7 @@ class DeckManager(DeprecatedNamesMixin): def have(self, id: DeckId) -> bool: return bool(self.get_legacy(id)) - def get_all_legacy(self) -> List[DeckDict]: + def get_all_legacy(self) -> list[DeckDict]: return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values()) def new_deck_legacy(self, filtered: bool) -> DeckDict: @@ -191,7 +179,7 @@ class DeckManager(DeprecatedNamesMixin): @classmethod def find_deck_in_tree( cls, node: DeckTreeNode, deck_id: DeckId - ) -> Optional[DeckTreeNode]: + ) -> DeckTreeNode | None: if node.deck_id == deck_id: return node for child in node.children: @@ -200,7 +188,7 @@ class DeckManager(DeprecatedNamesMixin): return match return None - def all(self) -> List[DeckDict]: + def all(self) -> list[DeckDict]: "All decks. Expensive; prefer all_names_and_ids()" return self.get_all_legacy() @@ -226,7 +214,7 @@ class DeckManager(DeprecatedNamesMixin): return len(self.all_names_and_ids()) def card_count( - self, dids: Union[DeckId, Iterable[DeckId]], include_subdecks: bool + self, dids: DeckId | Iterable[DeckId], include_subdecks: bool ) -> Any: if isinstance(dids, int): dids = {dids} @@ -240,7 +228,7 @@ class DeckManager(DeprecatedNamesMixin): ) return count - def get(self, did: Union[DeckId, str], default: bool = True) -> Optional[DeckDict]: + def get(self, did: DeckId | str, default: bool = True) -> DeckDict | None: if not did: if default: return self.get_legacy(DEFAULT_DECK_ID) @@ -255,7 +243,7 @@ class DeckManager(DeprecatedNamesMixin): else: return None - def by_name(self, name: str) -> Optional[DeckDict]: + def by_name(self, name: str) -> DeckDict | None: """Get deck with NAME, ignoring case.""" id = self.id_for_name(name) if id: @@ -271,7 +259,7 @@ class DeckManager(DeprecatedNamesMixin): def update_dict(self, deck: DeckDict) -> OpChanges: return self.col._backend.update_deck_legacy(json=to_json_bytes(deck)) - def rename(self, deck: Union[DeckDict, DeckId], new_name: str) -> OpChanges: + def rename(self, deck: DeckDict | DeckId, new_name: str) -> OpChanges: "Rename deck prefix to NAME if not exists. Updates children." if isinstance(deck, int): deck_id = deck @@ -300,7 +288,7 @@ class DeckManager(DeprecatedNamesMixin): def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges: return self.col._backend.update_deck_configs(input=input) - def all_config(self) -> List[DeckConfigDict]: + def all_config(self) -> list[DeckConfigDict]: "A list of all deck config." return list(from_json_bytes(self.col._backend.all_deck_config_legacy())) @@ -318,7 +306,7 @@ class DeckManager(DeprecatedNamesMixin): # dynamic decks have embedded conf return deck - def get_config(self, conf_id: DeckConfigId) -> Optional[DeckConfigDict]: + def get_config(self, conf_id: DeckConfigId) -> DeckConfigDict | None: try: return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id)) except NotFoundError: @@ -331,7 +319,7 @@ class DeckManager(DeprecatedNamesMixin): ) def add_config( - self, name: str, clone_from: Optional[DeckConfigDict] = None + self, name: str, clone_from: DeckConfigDict | None = None ) -> DeckConfigDict: if clone_from is not None: conf = copy.deepcopy(clone_from) @@ -343,7 +331,7 @@ class DeckManager(DeprecatedNamesMixin): return conf def add_config_returning_id( - self, name: str, clone_from: Optional[DeckConfigDict] = None + self, name: str, clone_from: DeckConfigDict | None = None ) -> DeckConfigId: return self.add_config(name, clone_from)["id"] @@ -363,7 +351,7 @@ class DeckManager(DeprecatedNamesMixin): deck["conf"] = id self.save(deck) - def decks_using_config(self, conf: DeckConfigDict) -> List[DeckId]: + def decks_using_config(self, conf: DeckConfigDict) -> list[DeckId]: dids = [] for deck in self.all(): if "conf" in deck and deck["conf"] == conf["id"]: @@ -389,13 +377,13 @@ class DeckManager(DeprecatedNamesMixin): return deck["name"] return self.col.tr.decks_no_deck() - def name_if_exists(self, did: DeckId) -> Optional[str]: + def name_if_exists(self, did: DeckId) -> str | None: deck = self.get(did, default=False) if deck: return deck["name"] return None - def cids(self, did: DeckId, children: bool = False) -> List[CardId]: + def cids(self, did: DeckId, children: bool = False) -> list[CardId]: if not children: return self.col.db.list("select id from cards where did=?", did) dids = [did] @@ -403,7 +391,7 @@ class DeckManager(DeprecatedNamesMixin): dids.append(id) return self.col.db.list(f"select id from cards where did in {ids2str(dids)}") - def for_card_ids(self, cids: List[CardId]) -> List[DeckId]: + def for_card_ids(self, cids: list[CardId]) -> list[DeckId]: return self.col.db.list(f"select did from cards where id in {ids2str(cids)}") # Deck selection @@ -419,7 +407,7 @@ class DeckManager(DeprecatedNamesMixin): def current(self) -> DeckDict: return self.get(self.selected()) - def active(self) -> List[DeckId]: + def active(self) -> list[DeckId]: # some add-ons assume this will always be non-empty return self.col.sched.active_decks or [DeckId(1)] @@ -435,7 +423,7 @@ class DeckManager(DeprecatedNamesMixin): ############################################################# @staticmethod - def path(name: str) -> List[str]: + def path(name: str) -> list[str]: return name.split("::") @classmethod @@ -443,21 +431,21 @@ class DeckManager(DeprecatedNamesMixin): return cls.path(name)[-1] @classmethod - def immediate_parent_path(cls, name: str) -> List[str]: + def immediate_parent_path(cls, name: str) -> list[str]: return cls.path(name)[:-1] @classmethod - def immediate_parent(cls, name: str) -> Optional[str]: + def immediate_parent(cls, name: str) -> str | None: parent_path = cls.immediate_parent_path(name) if parent_path: return "::".join(parent_path) return None @classmethod - def key(cls, deck: DeckDict) -> List[str]: + def key(cls, deck: DeckDict) -> list[str]: return cls.path(deck["name"]) - def children(self, did: DeckId) -> List[Tuple[str, DeckId]]: + def children(self, did: DeckId) -> list[tuple[str, DeckId]]: "All children of did, as (name, id)." name = self.get(did)["name"] actv = [] @@ -472,24 +460,24 @@ class DeckManager(DeprecatedNamesMixin): DeckId(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix) ) - def deck_and_child_ids(self, deck_id: DeckId) -> List[DeckId]: + def deck_and_child_ids(self, deck_id: DeckId) -> list[DeckId]: parent_name = self.name(deck_id) out = [deck_id] out.extend(self.child_ids(parent_name)) return out def parents( - self, did: DeckId, name_map: Optional[Dict[str, DeckDict]] = None - ) -> List[DeckDict]: + self, did: DeckId, name_map: dict[str, DeckDict] | None = None + ) -> list[DeckDict]: "All parents of did." # get parent and grandparent names - parents_names: List[str] = [] + parents_names: list[str] = [] for part in self.immediate_parent_path(self.get(did)["name"]): if not parents_names: parents_names.append(part) else: parents_names.append(f"{parents_names[-1]}::{part}") - parents: List[DeckDict] = [] + parents: list[DeckDict] = [] # convert to objects for parent_name in parents_names: if name_map: @@ -499,13 +487,13 @@ class DeckManager(DeprecatedNamesMixin): parents.append(deck) return parents - def parents_by_name(self, name: str) -> List[DeckDict]: + def parents_by_name(self, name: str) -> list[DeckDict]: "All existing parents of name" if "::" not in name: return [] names = self.immediate_parent_path(name) head = [] - parents: List[DeckDict] = [] + parents: list[DeckDict] = [] while names: head.append(names.pop(0)) @@ -524,7 +512,7 @@ class DeckManager(DeprecatedNamesMixin): self.select(did) return did - def is_filtered(self, did: Union[DeckId, str]) -> bool: + def is_filtered(self, did: DeckId | str) -> bool: return bool(self.get(did)["dyn"]) # Legacy @@ -546,11 +534,11 @@ class DeckManager(DeprecatedNamesMixin): self.remove([did]) @deprecated(replaced_by=all_names_and_ids) - def name_map(self) -> Dict[str, DeckDict]: + def name_map(self) -> dict[str, DeckDict]: return {d["name"]: d for d in self.all()} @deprecated(info="use col.set_deck() instead") - def set_deck(self, cids: List[CardId], did: DeckId) -> None: + def set_deck(self, cids: list[CardId], did: DeckId) -> None: self.col.set_deck(card_ids=cids, deck_id=did) self.col.db.execute( f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", @@ -560,11 +548,11 @@ class DeckManager(DeprecatedNamesMixin): ) @deprecated(replaced_by=all_names_and_ids) - def all_ids(self) -> List[str]: + def all_ids(self) -> list[str]: return [str(x.id) for x in self.all_names_and_ids()] @deprecated(replaced_by=all_names_and_ids) - def all_names(self, dyn: bool = True, force_default: bool = True) -> List[str]: + def all_names(self, dyn: bool = True, force_default: bool = True) -> list[str]: return [ x.name for x in self.all_names_and_ids( diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 4b3b9f8f0..0c8da5710 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -3,6 +3,8 @@ # pylint: disable=invalid-name +from __future__ import annotations + import json import os import re @@ -10,7 +12,7 @@ import shutil import unicodedata import zipfile from io import BufferedWriter -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Optional, Sequence from zipfile import ZipFile from anki import hooks @@ -21,7 +23,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML class Exporter: - includeHTML: Union[bool, None] = None + includeHTML: bool | None = None ext: Optional[str] = None includeTags: Optional[bool] = None includeSched: Optional[bool] = None @@ -31,7 +33,7 @@ class Exporter: self, col: Collection, did: Optional[DeckId] = None, - cids: Optional[List[CardId]] = None, + cids: Optional[list[CardId]] = None, ) -> None: self.col = col.weakref() self.did = did @@ -177,7 +179,7 @@ where cards.id in %s)""" class AnkiExporter(Exporter): ext = ".anki2" - includeSched: Union[bool, None] = False + includeSched: bool | None = False includeMedia = True def __init__(self, col: Collection) -> None: @@ -187,7 +189,7 @@ class AnkiExporter(Exporter): def key(col: Collection) -> str: return col.tr.exporting_anki_20_deck() - def deckIds(self) -> List[DeckId]: + def deckIds(self) -> list[DeckId]: if self.cids: return self.col.decks.for_card_ids(self.cids) elif self.did: @@ -210,7 +212,7 @@ class AnkiExporter(Exporter): cids = self.cardIds() # copy cards, noting used nids nids = {} - data: List[Sequence] = [] + data: list[Sequence] = [] for row in self.src.db.execute( "select * from cards where id in " + ids2str(cids) ): @@ -344,7 +346,7 @@ class AnkiPackageExporter(AnkiExporter): z.writestr("media", json.dumps(media)) z.close() - def doExport(self, z: ZipFile, path: str) -> Dict[str, str]: # type: ignore + def doExport(self, z: ZipFile, path: str) -> dict[str, str]: # type: ignore # export into the anki2 file colfile = path.replace(".apkg", ".anki2") AnkiExporter.exportInto(self, colfile) @@ -368,7 +370,7 @@ class AnkiPackageExporter(AnkiExporter): shutil.rmtree(path.replace(".apkg", ".media")) return media - def _exportMedia(self, z: ZipFile, files: List[str], fdir: str) -> Dict[str, str]: + def _exportMedia(self, z: ZipFile, files: list[str], fdir: str) -> dict[str, str]: media = {} for c, file in enumerate(files): cStr = str(c) @@ -445,13 +447,13 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter): ########################################################################## -def exporters(col: Collection) -> List[Tuple[str, Any]]: +def exporters(col: Collection) -> list[tuple[str, Any]]: def id(obj): if callable(obj.key): key_str = obj.key(col) else: key_str = obj.key - return ("%s (*%s)" % (key_str, obj.ext), obj) + return (f"{key_str} (*{obj.ext})", obj) exps = [ id(AnkiCollectionPackageExporter), diff --git a/pylib/anki/find.py b/pylib/anki/find.py index 446fd8d45..3724b64d4 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Set +from typing import TYPE_CHECKING from anki.hooks import * from anki.notes import NoteId @@ -13,7 +13,7 @@ if TYPE_CHECKING: class Finder: - def __init__(self, col: Optional[Collection]) -> None: + def __init__(self, col: Collection | None) -> None: self.col = col.weakref() print("Finder() is deprecated, please use col.find_cards() or .find_notes()") @@ -34,7 +34,7 @@ def findReplace( src: str, dst: str, regex: bool = False, - field: Optional[str] = None, + field: str | None = None, fold: bool = True, ) -> int: "Find and replace fields in a note. Returns changed note count." @@ -58,7 +58,7 @@ def fieldNamesForNotes(col: Collection, nids: List[NoteId]) -> List[str]: def fieldNames(col: Collection, downcase: bool = True) -> List: - fields: Set[str] = set() + fields: set[str] = set() for m in col.models.all(): for f in m["flds"]: name = f["name"].lower() if downcase else f["name"] diff --git a/pylib/anki/hooks.py b/pylib/anki/hooks.py index 87ca01ee8..8efb78bcf 100644 --- a/pylib/anki/hooks.py +++ b/pylib/anki/hooks.py @@ -12,8 +12,6 @@ modifying it. from __future__ import annotations -from typing import Any, Callable, Dict, List - import decorator # You can find the definitions in ../tools/genhooks.py @@ -22,7 +20,7 @@ from anki.hooks_gen import * # Legacy hook handling ############################################################################## -_hooks: Dict[str, List[Callable[..., Any]]] = {} +_hooks: dict[str, list[Callable[..., Any]]] = {} def runHook(hook: str, *args: Any) -> None: diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py index a26536496..6643deb4d 100644 --- a/pylib/anki/httpclient.py +++ b/pylib/anki/httpclient.py @@ -9,7 +9,7 @@ from __future__ import annotations import io import os -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable import requests from requests import Response @@ -24,9 +24,9 @@ class HttpClient: verify = True timeout = 60 # args are (upload_bytes_in_chunk, download_bytes_in_chunk) - progress_hook: Optional[ProgressCallback] = None + progress_hook: ProgressCallback | None = None - def __init__(self, progress_hook: Optional[ProgressCallback] = None) -> None: + def __init__(self, progress_hook: ProgressCallback | None = None) -> None: self.progress_hook = progress_hook self.session = requests.Session() @@ -44,9 +44,7 @@ class HttpClient: def __del__(self) -> None: self.close() - def post( - self, url: str, data: bytes, headers: Optional[Dict[str, str]] - ) -> Response: + def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response: headers["User-Agent"] = self._agentName() return self.session.post( url, @@ -57,7 +55,7 @@ class HttpClient: verify=self.verify, ) # pytype: disable=wrong-arg-types - def get(self, url: str, headers: Dict[str, str] = None) -> Response: + def get(self, url: str, headers: dict[str, str] = None) -> Response: if headers is None: headers = {} headers["User-Agent"] = self._agentName() diff --git a/pylib/anki/importing/__init__.py b/pylib/anki/importing/__init__.py index 7c35ba545..c5636f6b1 100644 --- a/pylib/anki/importing/__init__.py +++ b/pylib/anki/importing/__init__.py @@ -1,7 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Any, Callable, Sequence, Tuple, Type, Union +from typing import Any, Callable, Sequence, Type, Union from anki.collection import Collection from anki.importing.anki2 import Anki2Importer @@ -14,7 +14,7 @@ from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore from anki.lang import TR -def importers(col: Collection) -> Sequence[Tuple[str, Type[Importer]]]: +def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]: return ( (col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter), ( diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index cddacd787..7b5a5c46b 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -5,7 +5,7 @@ import os import unicodedata -from typing import Any, Dict, List, Optional, Tuple +from typing import Optional from anki.cards import CardId from anki.collection import Collection @@ -37,7 +37,7 @@ class Anki2Importer(Importer): super().__init__(col, file) # set later, defined here for typechecking - self._decks: Dict[DeckId, DeckId] = {} + self._decks: dict[DeckId, DeckId] = {} self.source_needs_upgrade = False def run(self, media: None = None, importing_v2: bool = True) -> None: @@ -80,14 +80,14 @@ class Anki2Importer(Importer): # Notes ###################################################################### - def _logNoteRow(self, action: str, noteRow: List[str]) -> None: + def _logNoteRow(self, action: str, noteRow: list[str]) -> None: self.log.append( - "[%s] %s" % (action, stripHTMLMedia(noteRow[6].replace("\x1f", ", "))) + "[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", "))) ) def _importNotes(self) -> None: # build guid -> (id,mod,mid) hash & map of existing note ids - self._notes: Dict[str, Tuple[NoteId, int, NotetypeId]] = {} + self._notes: dict[str, tuple[NoteId, int, NotetypeId]] = {} existing = {} for id, guid, mod, mid in self.dst.db.execute( "select id, guid, mod, mid from notes" @@ -96,7 +96,7 @@ class Anki2Importer(Importer): existing[id] = True # we ignore updates to changed schemas. we need to note the ignored # guids, so we avoid importing invalid cards - self._ignoredGuids: Dict[str, bool] = {} + self._ignoredGuids: dict[str, bool] = {} # iterate over source collection add = [] update = [] @@ -194,7 +194,7 @@ class Anki2Importer(Importer): # determine if note is a duplicate, and adjust mid and/or guid as required # returns true if note should be added - def _uniquifyNote(self, note: List[Any]) -> bool: + def _uniquifyNote(self, note: list[Any]) -> bool: origGuid = note[GUID] srcMid = note[MID] dstMid = self._mid(srcMid) @@ -218,7 +218,7 @@ class Anki2Importer(Importer): def _prepareModels(self) -> None: "Prepare index of schema hashes." - self._modelMap: Dict[NotetypeId, NotetypeId] = {} + self._modelMap: dict[NotetypeId, NotetypeId] = {} def _mid(self, srcMid: NotetypeId) -> Any: "Return local id for remote MID." @@ -308,7 +308,7 @@ class Anki2Importer(Importer): if self.source_needs_upgrade: self.src.upgrade_to_v2_scheduler() # build map of (guid, ord) -> cid and used id cache - self._cards: Dict[Tuple[str, int], CardId] = {} + self._cards: dict[tuple[str, int], CardId] = {} existing = {} for guid, ord, cid in self.dst.db.execute( "select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id" diff --git a/pylib/anki/importing/apkg.py b/pylib/anki/importing/apkg.py index 6fd26bbfb..049f54c37 100644 --- a/pylib/anki/importing/apkg.py +++ b/pylib/anki/importing/apkg.py @@ -7,14 +7,14 @@ import json import os import unicodedata import zipfile -from typing import Any, Dict, Optional +from typing import Any, Optional from anki.importing.anki2 import Anki2Importer from anki.utils import tmpfile class AnkiPackageImporter(Anki2Importer): - nameToNum: Dict[str, str] + nameToNum: dict[str, str] zip: Optional[zipfile.ZipFile] def run(self) -> None: # type: ignore diff --git a/pylib/anki/importing/base.py b/pylib/anki/importing/base.py index 4ee6626a8..7a4ea1aff 100644 --- a/pylib/anki/importing/base.py +++ b/pylib/anki/importing/base.py @@ -1,7 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Any, List, Optional +from typing import Any, Optional from anki.collection import Collection from anki.utils import maxID @@ -18,7 +18,7 @@ class Importer: def __init__(self, col: Collection, file: str) -> None: self.file = file - self.log: List[str] = [] + self.log: list[str] = [] self.col = col.weakref() self.total = 0 self.dst = None diff --git a/pylib/anki/importing/csvfile.py b/pylib/anki/importing/csvfile.py index 961018772..41ed93614 100644 --- a/pylib/anki/importing/csvfile.py +++ b/pylib/anki/importing/csvfile.py @@ -1,9 +1,11 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + import csv import re -from typing import Any, List, Optional, TextIO, Union +from typing import Any, Optional, TextIO from anki.collection import Collection from anki.importing.noteimp import ForeignNote, NoteImporter @@ -19,12 +21,12 @@ class TextImporter(NoteImporter): self.lines = None self.fileobj: Optional[TextIO] = None self.delimiter: Optional[str] = None - self.tagsToAdd: List[str] = [] + self.tagsToAdd: list[str] = [] self.numFields = 0 self.dialect: Optional[Any] - self.data: Optional[Union[str, List[str]]] + self.data: Optional[str | list[str]] - def foreignNotes(self) -> List[ForeignNote]: + def foreignNotes(self) -> list[ForeignNote]: self.open() # process all lines log = [] @@ -144,7 +146,7 @@ class TextImporter(NoteImporter): # pylint: disable=no-member zuper.__del__(self) # type: ignore - def noteFromFields(self, fields: List[str]) -> ForeignNote: + def noteFromFields(self, fields: list[str]) -> ForeignNote: note = ForeignNote() note.fields.extend([x for x in fields]) note.tags.extend(self.tagsToAdd) diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index eca2ed90d..0718dd018 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -3,9 +3,11 @@ # pylint: disable=invalid-name +from __future__ import annotations + import html import unicodedata -from typing import Dict, List, Optional, Tuple, Union +from typing import Optional, Union from anki.collection import Collection from anki.config import Config @@ -22,9 +24,9 @@ from anki.utils import ( timestampID, ) -TagMappedUpdate = Tuple[int, int, str, str, NoteId, str, str] -TagModifiedUpdate = Tuple[int, int, str, str, NoteId, str] -NoTagUpdate = Tuple[int, int, str, NoteId, str] +TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str] +TagModifiedUpdate = tuple[int, int, str, str, NoteId, str] +NoTagUpdate = tuple[int, int, str, NoteId, str] Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate] # Stores a list of fields, tags and deck @@ -35,10 +37,10 @@ class ForeignNote: "An temporary object storing fields and attributes." def __init__(self) -> None: - self.fields: List[str] = [] - self.tags: List[str] = [] + self.fields: list[str] = [] + self.tags: list[str] = [] self.deck = None - self.cards: Dict[int, ForeignCard] = {} # map of ord -> card + self.cards: dict[int, ForeignCard] = {} # map of ord -> card self.fieldsStr = "" @@ -75,7 +77,7 @@ class NoteImporter(Importer): needDelimiter = False allowHTML = False importMode = UPDATE_MODE - mapping: Optional[List[str]] + mapping: Optional[list[str]] tagModified: Optional[str] def __init__(self, col: Collection, file: str) -> None: @@ -109,11 +111,11 @@ class NoteImporter(Importer): def mappingOk(self) -> bool: return self.model["flds"][0]["name"] in self.mapping - def foreignNotes(self) -> List: + def foreignNotes(self) -> list: "Return a list of foreign notes for importing." return [] - def importNotes(self, notes: List[ForeignNote]) -> None: + def importNotes(self, notes: list[ForeignNote]) -> None: "Convert each card into a note, apply attributes and add to col." assert self.mappingOk() # note whether tags are mapped @@ -122,7 +124,7 @@ class NoteImporter(Importer): if f == "_tags": self._tagsMapped = True # gather checks for duplicate comparison - csums: Dict[str, List[NoteId]] = {} + csums: dict[str, list[NoteId]] = {} for csum, id in self.col.db.execute( "select csum, id from notes where mid = ?", self.model["id"] ): @@ -130,18 +132,18 @@ class NoteImporter(Importer): csums[csum].append(id) else: csums[csum] = [id] - firsts: Dict[str, bool] = {} + firsts: dict[str, bool] = {} fld0idx = self.mapping.index(self.model["flds"][0]["name"]) self._fmap = self.col.models.field_map(self.model) self._nextID = NoteId(timestampID(self.col.db, "notes")) # loop through the notes - updates: List[Updates] = [] + updates: list[Updates] = [] updateLog = [] new = [] - self._ids: List[NoteId] = [] - self._cards: List[Tuple] = [] + self._ids: list[NoteId] = [] + self._cards: list[tuple] = [] dupeCount = 0 - dupes: List[str] = [] + dupes: list[str] = [] for n in notes: for c, field in enumerate(n.fields): if not self.allowHTML: @@ -232,7 +234,7 @@ class NoteImporter(Importer): def newData( self, n: ForeignNote - ) -> Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]: + ) -> tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]: id = self._nextID self._nextID = NoteId(self._nextID + 1) self._ids.append(id) @@ -256,8 +258,8 @@ class NoteImporter(Importer): def addNew( self, - rows: List[ - Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str] + rows: list[ + tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str] ], ) -> None: self.col.db.executemany( @@ -265,7 +267,7 @@ class NoteImporter(Importer): ) def updateData( - self, n: ForeignNote, id: NoteId, sflds: List[str] + self, n: ForeignNote, id: NoteId, sflds: list[str] ) -> Optional[Updates]: self._ids.append(id) self.processFields(n, sflds) @@ -280,7 +282,7 @@ class NoteImporter(Importer): else: return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr) - def addUpdates(self, rows: List[Updates]) -> None: + def addUpdates(self, rows: list[Updates]) -> None: changes = self.col.db.scalar("select total_changes()") if self._tagsMapped: self.col.db.executemany( @@ -307,7 +309,7 @@ where id = ? and flds != ?""", self.updateCount = changes2 - changes def processFields( - self, note: ForeignNote, fields: Optional[List[str]] = None + self, note: ForeignNote, fields: Optional[list[str]] = None ) -> None: if not fields: fields = [""] * len(self.model["flds"]) diff --git a/pylib/anki/importing/supermemo_xml.py b/pylib/anki/importing/supermemo_xml.py index 7d91e95c5..b3f1ad539 100644 --- a/pylib/anki/importing/supermemo_xml.py +++ b/pylib/anki/importing/supermemo_xml.py @@ -9,7 +9,7 @@ import sys import time import unicodedata from string import capwords -from typing import List, Optional, Union +from typing import Optional, Union from xml.dom import minidom from xml.dom.minidom import Element, Text @@ -185,7 +185,7 @@ class SupermemoXmlImporter(NoteImporter): ## DEFAULT IMPORTER METHODS - def foreignNotes(self) -> List[ForeignNote]: + def foreignNotes(self) -> list[ForeignNote]: # Load file and parse it by minidom self.loadSource(self.file) @@ -415,7 +415,7 @@ class SupermemoXmlImporter(NoteImporter): self.logger("-" * 45, level=3) for key in list(smel.keys()): self.logger( - "\t%s %s" % ((key + ":").ljust(15), smel[key]), level=3 + "\t{} {}".format((key + ":").ljust(15), smel[key]), level=3 ) else: self.logger("Element skiped \t- no valid Q and A ...", level=3) diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index b0f8a6412..d159eaf44 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -6,7 +6,6 @@ from __future__ import annotations import locale import re import weakref -from typing import Optional, Tuple import anki import anki._backend @@ -153,7 +152,7 @@ currentLang = "en" # not reference this, and should use col.tr instead. The global # instance exists for legacy reasons, and as a convenience for the # Qt code. -current_i18n: Optional[anki._backend.RustBackend] = None +current_i18n: anki._backend.RustBackend | None = None tr_legacyglobal = anki._backend.Translations(None) @@ -174,7 +173,7 @@ def set_lang(lang: str) -> None: tr_legacyglobal.backend = weakref.ref(current_i18n) -def get_def_lang(lang: Optional[str] = None) -> Tuple[int, str]: +def get_def_lang(lang: str | None = None) -> tuple[int, str]: """Return lang converted to name used on disk and its index, defaulting to system language or English if not available.""" try: diff --git a/pylib/anki/latex.py b/pylib/anki/latex.py index 8e9c31966..98dc90bcd 100644 --- a/pylib/anki/latex.py +++ b/pylib/anki/latex.py @@ -7,7 +7,7 @@ import html import os import re from dataclasses import dataclass -from typing import Any, List, Optional, Tuple +from typing import Any import anki from anki import card_rendering_pb2, hooks @@ -41,7 +41,7 @@ class ExtractedLatex: @dataclass class ExtractedLatexOutput: html: str - latex: List[ExtractedLatex] + latex: list[ExtractedLatex] @staticmethod def from_proto( @@ -80,7 +80,7 @@ def render_latex_returning_errors( model: NotetypeDict, col: anki.collection.Collection, expand_clozes: bool = False, -) -> Tuple[str, List[str]]: +) -> tuple[str, list[str]]: """Returns (text, errors). errors will be non-empty if LaTeX failed to render.""" @@ -111,7 +111,7 @@ def _save_latex_image( header: str, footer: str, svg: bool, -) -> Optional[str]: +) -> str | None: # add header/footer latex = f"{header}\n{extracted.latex_body}\n{footer}" # it's only really secure if run in a jail, but these are the most common diff --git a/pylib/anki/media.py b/pylib/anki/media.py index daae446d0..a491f2436 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -8,7 +8,7 @@ import pprint import re import sys import time -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable from anki import media_pb2 from anki.consts import * @@ -19,7 +19,7 @@ from anki.template import av_tags_to_native from anki.utils import intTime -def media_paths_from_col_path(col_path: str) -> Tuple[str, str]: +def media_paths_from_col_path(col_path: str) -> tuple[str, str]: media_folder = re.sub(r"(?i)\.(anki2)$", ".media", col_path) media_db = f"{media_folder}.db2" return (media_folder, media_db) @@ -50,7 +50,7 @@ class MediaManager: def __init__(self, col: anki.collection.Collection, server: bool) -> None: self.col = col.weakref() - self._dir: Optional[str] = None + self._dir: str | None = None if server: return # media directory @@ -88,7 +88,7 @@ class MediaManager: # may have been deleted pass - def dir(self) -> Optional[str]: + def dir(self) -> str | None: return self._dir def force_resync(self) -> None: @@ -106,7 +106,7 @@ class MediaManager: def strip_av_tags(self, text: str) -> str: return self.col._backend.strip_av_tags(text) - def _extract_filenames(self, text: str) -> List[str]: + def _extract_filenames(self, text: str) -> list[str]: "This only exists do support a legacy function; do not use." out = self.col._backend.extract_av_tags(text=text, question_side=True) return [ @@ -148,7 +148,7 @@ class MediaManager: def have(self, fname: str) -> bool: return os.path.exists(os.path.join(self.dir(), fname)) - def trash_files(self, fnames: List[str]) -> None: + def trash_files(self, fnames: list[str]) -> None: "Move provided files to the trash." self.col._backend.trash_media_files(fnames) @@ -157,7 +157,7 @@ class MediaManager: def filesInStr( self, mid: NotetypeId, string: str, includeRemote: bool = False - ) -> List[str]: + ) -> list[str]: l = [] model = self.col.models.get(mid) # handle latex @@ -204,8 +204,8 @@ class MediaManager: return output def render_all_latex( - self, progress_cb: Optional[Callable[[int], bool]] = None - ) -> Optional[Tuple[int, str]]: + self, progress_cb: Callable[[int], bool] | None = None + ) -> tuple[int, str] | None: """Render any LaTeX that is missing. If a progress callback is provided and it returns false, the operation @@ -260,7 +260,7 @@ class MediaManager: addFile = add_file - def writeData(self, opath: str, data: bytes, typeHint: Optional[str] = None) -> str: + def writeData(self, opath: str, data: bytes, typeHint: str | None = None) -> str: fname = os.path.basename(opath) if typeHint: fname = self.add_extension_based_on_mime(fname, typeHint) diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 1f172f9b8..7f34872aa 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -9,7 +9,7 @@ import copy import pprint import sys import time -from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union +from typing import Any, NewType, Sequence, Union import anki # pylint: disable=unused-import from anki import notetypes_pb2 @@ -29,10 +29,10 @@ ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest # legacy types -NotetypeDict = Dict[str, Any] +NotetypeDict = dict[str, Any] NoteType = NotetypeDict -FieldDict = Dict[str, Any] -TemplateDict = Dict[str, Union[str, int, None]] +FieldDict = dict[str, Any] +TemplateDict = dict[str, Union[str, int, None]] NotetypeId = NewType("NotetypeId", int) sys.modules["anki.models"].NoteType = NotetypeDict # type: ignore @@ -97,7 +97,7 @@ class ModelManager(DeprecatedNamesMixin): # need to cache responses from the backend. Please do not # access the cache directly! - _cache: Dict[NotetypeId, NotetypeDict] = {} + _cache: dict[NotetypeId, NotetypeDict] = {} def _update_cache(self, notetype: NotetypeDict) -> None: self._cache[notetype["id"]] = notetype @@ -106,7 +106,7 @@ class ModelManager(DeprecatedNamesMixin): if ntid in self._cache: del self._cache[ntid] - def _get_cached(self, ntid: NotetypeId) -> Optional[NotetypeDict]: + def _get_cached(self, ntid: NotetypeId) -> NotetypeDict | None: return self._cache.get(ntid) def _clear_cache(self) -> None: @@ -142,13 +142,13 @@ class ModelManager(DeprecatedNamesMixin): # Retrieving and creating models ############################################################# - def id_for_name(self, name: str) -> Optional[NotetypeId]: + def id_for_name(self, name: str) -> NotetypeId | None: try: return NotetypeId(self.col._backend.get_notetype_id_by_name(name)) except NotFoundError: return None - def get(self, id: NotetypeId) -> Optional[NotetypeDict]: + def get(self, id: NotetypeId) -> NotetypeDict | None: "Get model with ID, or None." # deal with various legacy input types if id is None: @@ -165,11 +165,11 @@ class ModelManager(DeprecatedNamesMixin): return None return notetype - def all(self) -> List[NotetypeDict]: + def all(self) -> list[NotetypeDict]: "Get all models." return [self.get(NotetypeId(nt.id)) for nt in self.all_names_and_ids()] - def by_name(self, name: str) -> Optional[NotetypeDict]: + def by_name(self, name: str) -> NotetypeDict | None: "Get model with NAME." id = self.id_for_name(name) if id: @@ -231,7 +231,7 @@ class ModelManager(DeprecatedNamesMixin): # Tools ################################################## - def nids(self, ntid: NotetypeId) -> List[anki.notes.NoteId]: + def nids(self, ntid: NotetypeId) -> list[anki.notes.NoteId]: "Note ids for M." if isinstance(ntid, dict): # legacy callers passed in note type @@ -261,11 +261,11 @@ class ModelManager(DeprecatedNamesMixin): # Fields ################################################## - def field_map(self, notetype: NotetypeDict) -> Dict[str, Tuple[int, FieldDict]]: + def field_map(self, notetype: NotetypeDict) -> dict[str, tuple[int, FieldDict]]: "Mapping of field name -> (ord, field)." return {f["name"]: (f["ord"], f) for f in notetype["flds"]} - def field_names(self, notetype: NotetypeDict) -> List[str]: + def field_names(self, notetype: NotetypeDict) -> list[str]: return [f["name"] for f in notetype["flds"]] def sort_idx(self, notetype: NotetypeDict) -> int: @@ -394,10 +394,10 @@ and notes.mid = ? and cards.ord = ?""", def change( # pylint: disable=invalid-name self, notetype: NotetypeDict, - nids: List[anki.notes.NoteId], + nids: list[anki.notes.NoteId], newModel: NotetypeDict, - fmap: Dict[int, Optional[int]], - cmap: Optional[Dict[int, Optional[int]]], + fmap: dict[int, int | None], + cmap: dict[int, int | None] | None, ) -> None: # - maps are ord->ord, and there should not be duplicate targets self.col.mod_schema(check=True) @@ -424,8 +424,8 @@ and notes.mid = ? and cards.ord = ?""", ) def _convert_legacy_map( - self, old_to_new: Dict[int, Optional[int]], new_count: int - ) -> List[int]: + self, old_to_new: dict[int, int | None], new_count: int + ) -> list[int]: "Convert old->new map to list of old indexes" new_to_old = {v: k for k, v in old_to_new.items() if v is not None} out = [] @@ -458,7 +458,7 @@ and notes.mid = ? and cards.ord = ?""", @deprecated(info="use note.cloze_numbers_in_fields()") def _availClozeOrds( self, notetype: NotetypeDict, flds: str, allow_empty: bool = True - ) -> List[int]: + ) -> list[int]: import anki.notes_pb2 note = anki.notes_pb2.Note(fields=[flds]) @@ -515,11 +515,11 @@ and notes.mid = ? and cards.ord = ?""", self.col.set_config("curModel", m["id"]) @deprecated(replaced_by=all_names_and_ids) - def all_names(self) -> List[str]: + def all_names(self) -> list[str]: return [n.name for n in self.all_names_and_ids()] @deprecated(replaced_by=all_names_and_ids) - def ids(self) -> List[NotetypeId]: + def ids(self) -> list[NotetypeId]: return [NotetypeId(n.id) for n in self.all_names_and_ids()] @deprecated(info="no longer required") diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 2c357d6b7..04cb0637f 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -6,7 +6,7 @@ from __future__ import annotations import copy -from typing import Any, List, NewType, Optional, Sequence, Tuple, Union +from typing import Any, NewType, Sequence import anki # pylint: disable=unused-import from anki import hooks, notes_pb2 @@ -33,8 +33,8 @@ class Note(DeprecatedNamesMixin): def __init__( self, col: anki.collection.Collection, - model: Optional[Union[NotetypeDict, NotetypeId]] = None, - id: Optional[NoteId] = None, + model: NotetypeDict | NotetypeId | None = None, + id: NoteId | None = None, ) -> None: assert not (model and id) notetype_id = model["id"] if isinstance(model, dict) else model @@ -119,13 +119,13 @@ class Note(DeprecatedNamesMixin): card._note = self return card - def cards(self) -> List[anki.cards.Card]: + def cards(self) -> list[anki.cards.Card]: return [self.col.getCard(id) for id in self.card_ids()] def card_ids(self) -> Sequence[anki.cards.CardId]: return self.col.card_ids_of_note(self.id) - def note_type(self) -> Optional[NotetypeDict]: + def note_type(self) -> NotetypeDict | None: return self.col.models.get(self.mid) _note_type = property(note_type) @@ -136,13 +136,13 @@ class Note(DeprecatedNamesMixin): # Dict interface ################################################## - def keys(self) -> List[str]: + def keys(self) -> list[str]: return list(self._fmap.keys()) - def values(self) -> List[str]: + def values(self) -> list[str]: return self.fields - def items(self) -> List[Tuple[str, str]]: + def items(self) -> list[tuple[str, str]]: return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())] def _field_index(self, key: str) -> int: diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index defd25599..e209075f9 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -15,7 +15,7 @@ BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate -from typing import List, Optional, Sequence +from typing import Sequence from anki import config_pb2 from anki.cards import CardId @@ -115,7 +115,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges: return self.col._backend.restore_buried_and_suspended_cards(ids) - def unbury_cards(self, ids: List[CardId]) -> OpChanges: + def unbury_cards(self, ids: list[CardId]) -> OpChanges: return self.col._backend.restore_buried_and_suspended_cards(ids) def unbury_deck( @@ -162,12 +162,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self, card_ids: Sequence[CardId], days: str, - config_key: Optional[Config.String.V] = None, + config_key: Config.String.V | None = None, ) -> OpChanges: """Set cards to be due in `days`, turning them into review cards if necessary. `days` can be of the form '5' or '5..7' If `config_key` is provided, provided days will be remembered in config.""" - key: Optional[config_pb2.OptionalStringConfigKey] + key: config_pb2.OptionalStringConfigKey | None if config_key is not None: key = config_pb2.OptionalStringConfigKey(key=config_key) else: @@ -179,7 +179,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l config_key=key, # type: ignore ) - def resetCards(self, ids: List[CardId]) -> None: + def resetCards(self, ids: list[CardId]) -> None: "Completely reset cards for export." sids = ids2str(ids) assert self.col.db @@ -229,7 +229,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self.orderCards(did) # for post-import - def maybeRandomizeDeck(self, did: Optional[DeckId] = None) -> None: + def maybeRandomizeDeck(self, did: DeckId | None = None) -> None: if not did: did = self.col.decks.selected() conf = self.col.decks.config_dict_for_deck_id(did) @@ -240,7 +240,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l # legacy def sortCards( self, - cids: List[CardId], + cids: list[CardId], start: int = 1, step: int = 1, shuffle: bool = False, diff --git a/pylib/anki/scheduler/legacy.py b/pylib/anki/scheduler/legacy.py index 85afb717d..99088ddb6 100644 --- a/pylib/anki/scheduler/legacy.py +++ b/pylib/anki/scheduler/legacy.py @@ -3,7 +3,7 @@ # pylint: disable=invalid-name -from typing import List, Optional, Tuple +from typing import Optional from anki.cards import Card, CardId from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN @@ -17,7 +17,7 @@ class SchedulerBaseWithLegacy(SchedulerBase): "Legacy aliases and helpers. These will go away in the future." def reschedCards( - self, card_ids: List[CardId], min_interval: int, max_interval: int + self, card_ids: list[CardId], min_interval: int, max_interval: int ) -> None: self.set_due_date(card_ids, f"{min_interval}-{max_interval}!") @@ -77,7 +77,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe self.col.usn(), ) - def remFromDyn(self, cids: List[CardId]) -> None: + def remFromDyn(self, cids: list[CardId]) -> None: self.emptyDyn(None, f"id in {ids2str(cids)} and odid") # used by v2 scheduler and some add-ons @@ -104,7 +104,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe elif type == "time": self.update_stats(did, milliseconds_delta=cnt) - def deckDueTree(self) -> List: + def deckDueTree(self) -> list: "List of (base name, did, rev, lrn, new, children)" print( "deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()" @@ -116,7 +116,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe def _cardConf(self, card: Card) -> DeckConfigDict: return self.col.decks.config_dict_for_deck_id(card.did) - def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]: + def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]: return (ivl, ivl) # simple aliases diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 0a4ddccc3..aabdc1ab3 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -8,7 +8,6 @@ from __future__ import annotations import random import time from heapq import * -from typing import Any, List, Optional, Tuple, Union import anki from anki import hooks @@ -93,7 +92,7 @@ class Scheduler(V2): card.usn = self.col.usn() card.flush() - def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: + def counts(self, card: Card | None = None) -> tuple[int, int, int]: counts = [self.newCount, self.lrnCount, self.revCount] if card: idx = self.countIdx(card) @@ -127,7 +126,7 @@ class Scheduler(V2): # Getting the next card ########################################################################## - def _getCard(self) -> Optional[Card]: + def _getCard(self) -> Card | None: "Return the next due card id, or None." # learning card due? c = self._getLrnCard() @@ -179,12 +178,12 @@ and due <= ? limit %d""" def _resetLrn(self) -> None: self._resetLrnCount() - self._lrnQueue: List[Any] = [] - self._lrnDayQueue: List[Any] = [] + self._lrnQueue: list[Any] = [] + self._lrnDayQueue: list[Any] = [] self._lrnDids = self.col.decks.active()[:] # sub-day learning - def _fillLrn(self) -> Union[bool, List[Any]]: + def _fillLrn(self) -> bool | list[Any]: if not self.lrnCount: return False if self._lrnQueue: @@ -202,7 +201,7 @@ limit %d""" self._lrnQueue.sort() return self._lrnQueue - def _getLrnCard(self, collapse: bool = False) -> Optional[Card]: + def _getLrnCard(self, collapse: bool = False) -> Card | None: if self._fillLrn(): cutoff = time.time() if collapse: @@ -374,7 +373,7 @@ limit %d""" time.sleep(0.01) log() - def removeLrn(self, ids: Optional[List[int]] = None) -> None: + def removeLrn(self, ids: list[int] | None = None) -> None: "Remove cards from the learning queues." if ids: extra = f" and id in {ids2str(ids)}" @@ -429,7 +428,7 @@ and due <= ? limit ?)""", return self._deckNewLimit(did, self._deckRevLimitSingle) def _resetRev(self) -> None: - self._revQueue: List[Any] = [] + self._revQueue: list[Any] = [] self._revDids = self.col.decks.active()[:] def _fillRev(self, recursing: bool = False) -> bool: diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index 750bb4635..69cc13176 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -8,7 +8,7 @@ from __future__ import annotations import random import time from heapq import * -from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast +from typing import Any, Callable, cast import anki # pylint: disable=unused-import from anki import hooks, scheduler_pb2 @@ -23,7 +23,7 @@ CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse # legacy type alias -QueueConfig = Dict[str, Any] +QueueConfig = dict[str, Any] # card types: 0=new, 1=lrn, 2=rev, 3=relrn # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, @@ -49,11 +49,11 @@ class Scheduler(SchedulerBaseWithLegacy): self.reps = 0 self._haveQueues = False self._lrnCutoff = 0 - self._active_decks: List[DeckId] = [] + self._active_decks: list[DeckId] = [] self._current_deck_id = DeckId(1) @property - def active_decks(self) -> List[DeckId]: + def active_decks(self) -> list[DeckId]: "Caller must make sure to make a copy." return self._active_decks @@ -96,7 +96,7 @@ class Scheduler(SchedulerBaseWithLegacy): self.revCount = node.review_count self._immediate_learn_count = node.learn_count - def getCard(self) -> Optional[Card]: + def getCard(self) -> Card | None: """Pop the next card from the queue. None if finished.""" self._checkDay() if not self._haveQueues: @@ -109,7 +109,7 @@ class Scheduler(SchedulerBaseWithLegacy): return card return None - def _getCard(self) -> Optional[Card]: + def _getCard(self) -> Card | None: """Return the next due card, or None.""" # learning card due? c = self._getLrnCard() @@ -153,7 +153,7 @@ class Scheduler(SchedulerBaseWithLegacy): def _resetNew(self) -> None: self._newDids = self.col.decks.active()[:] - self._newQueue: List[CardId] = [] + self._newQueue: list[CardId] = [] self._updateNewCardRatio() def _fillNew(self, recursing: bool = False) -> bool: @@ -188,7 +188,7 @@ class Scheduler(SchedulerBaseWithLegacy): self._resetNew() return self._fillNew(recursing=True) - def _getNewCard(self) -> Optional[Card]: + def _getNewCard(self) -> Card | None: if self._fillNew(): self.newCount -= 1 return self.col.getCard(self._newQueue.pop()) @@ -204,7 +204,7 @@ class Scheduler(SchedulerBaseWithLegacy): return self.newCardModulus = 0 - def _timeForNewCard(self) -> Optional[bool]: + def _timeForNewCard(self) -> bool | None: "True if it's time to display a new card when distributing." if not self.newCount: return False @@ -219,7 +219,7 @@ class Scheduler(SchedulerBaseWithLegacy): return None def _deckNewLimit( - self, did: DeckId, fn: Optional[Callable[[DeckDict], int]] = None + self, did: DeckId, fn: Callable[[DeckDict], int] | None = None ) -> int: if not fn: fn = self._deckNewLimitSingle @@ -310,12 +310,12 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW} def _resetLrn(self) -> None: self._updateLrnCutoff(force=True) self._resetLrnCount() - self._lrnQueue: List[Tuple[int, CardId]] = [] - self._lrnDayQueue: List[CardId] = [] + self._lrnQueue: list[tuple[int, CardId]] = [] + self._lrnDayQueue: list[CardId] = [] self._lrnDids = self.col.decks.active()[:] # sub-day learning - def _fillLrn(self) -> Union[bool, List[Any]]: + def _fillLrn(self) -> bool | list[Any]: if not self.lrnCount: return False if self._lrnQueue: @@ -329,12 +329,12 @@ limit %d""" % (self._deckLimit(), self.reportLimit), cutoff, ) - self._lrnQueue = [cast(Tuple[int, CardId], tuple(e)) for e in self._lrnQueue] + self._lrnQueue = [cast(tuple[int, CardId], tuple(e)) for e in self._lrnQueue] # as it arrives sorted by did first, we need to sort it self._lrnQueue.sort() return self._lrnQueue - def _getLrnCard(self, collapse: bool = False) -> Optional[Card]: + def _getLrnCard(self, collapse: bool = False) -> Card | None: self._maybeResetLrn(force=collapse and self.lrnCount == 0) if self._fillLrn(): cutoff = time.time() @@ -348,7 +348,7 @@ limit %d""" return None # daily learning - def _fillLrnDay(self) -> Optional[bool]: + def _fillLrnDay(self) -> bool | None: if not self.lrnCount: return False if self._lrnDayQueue: @@ -378,7 +378,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", # shouldn't reach here return False - def _getLrnDayCard(self) -> Optional[Card]: + def _getLrnDayCard(self) -> Card | None: if self._fillLrnDay(): self.lrnCount -= 1 return self.col.getCard(self._lrnDayQueue.pop()) @@ -391,7 +391,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", d = self.col.decks.get(self.col.decks.selected(), default=False) return self._deckRevLimitSingle(d) - def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int: + def _deckRevLimitSingle(self, d: dict[str, Any]) -> int: # invalid deck selected? if not d: return 0 @@ -405,7 +405,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""", return hooks.scheduler_review_limit_for_single_deck(lim, d) def _resetRev(self) -> None: - self._revQueue: List[CardId] = [] + self._revQueue: list[CardId] = [] def _fillRev(self, recursing: bool = False) -> bool: "True if a review card can be fetched." @@ -439,7 +439,7 @@ limit ?""" self._resetRev() return self._fillRev(recursing=True) - def _getRevCard(self) -> Optional[Card]: + def _getRevCard(self) -> Card | None: if self._fillRev(): self.revCount -= 1 return self.col.getCard(self._revQueue.pop()) @@ -601,7 +601,7 @@ limit ?""" self._rescheduleLrnCard(card, conf, delay=delay) def _rescheduleLrnCard( - self, card: Card, conf: QueueConfig, delay: Optional[int] = None + self, card: Card, conf: QueueConfig, delay: int | None = None ) -> Any: # normal delay for the current step? if delay is None: @@ -690,9 +690,9 @@ limit ?""" def _leftToday( self, - delays: List[int], + delays: list[int], left: int, - now: Optional[int] = None, + now: int | None = None, ) -> int: "The number of steps that can be completed by the day cutoff." if not now: @@ -927,7 +927,7 @@ limit ?""" min, max = self._fuzzIvlRange(ivl) return random.randint(min, max) - def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]: + def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]: if ivl < 2: return (1, 1) elif ivl == 2: @@ -1080,7 +1080,7 @@ limit ?""" ########################################################################## def _burySiblings(self, card: Card) -> None: - toBury: List[CardId] = [] + toBury: list[CardId] = [] nconf = self._newConf(card) buryNew = nconf.get("bury", True) rconf = self._revConf(card) @@ -1115,7 +1115,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", # Review-related UI helpers ########################################################################## - def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: + def counts(self, card: Card | None = None) -> tuple[int, int, int]: counts = [self.newCount, self.lrnCount, self.revCount] if card: idx = self.countIdx(card) diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 60463dbce..2850a2b84 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -12,7 +12,7 @@ as '2' internally. from __future__ import annotations -from typing import List, Literal, Sequence, Tuple +from typing import Literal, Optional, Sequence from anki import scheduler_pb2 from anki.cards import Card @@ -113,7 +113,7 @@ class Scheduler(SchedulerBaseWithLegacy): "Don't use this, it is a stop-gap until this code is refactored." return not self.get_queued_cards().cards - def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: + def counts(self, card: Optional[Card] = None) -> tuple[int, int, int]: info = self.get_queued_cards() return (info.new_count, info.learning_count, info.review_count) @@ -230,7 +230,7 @@ class Scheduler(SchedulerBaseWithLegacy): # called by col.decks.active(), which add-ons are using @property - def active_decks(self) -> List[DeckId]: + def active_decks(self) -> list[DeckId]: try: return self.col.db.list("select id from active_decks") except DBError: diff --git a/pylib/anki/sound.py b/pylib/anki/sound.py index a3bcfe99d..3d375f716 100644 --- a/pylib/anki/sound.py +++ b/pylib/anki/sound.py @@ -11,7 +11,7 @@ from __future__ import annotations import re from dataclasses import dataclass -from typing import List, Union +from typing import Union @dataclass @@ -23,10 +23,10 @@ class TTSTag: field_text: str lang: str - voices: List[str] + voices: list[str] speed: float # each arg should be in the form 'foo=bar' - other_args: List[str] + other_args: list[str] @dataclass diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 9ef02c026..04b3fe33e 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -8,7 +8,7 @@ from __future__ import annotations import datetime import json import time -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Sequence import anki.cards import anki.collection @@ -37,12 +37,12 @@ class CardStats: # legacy - def addLine(self, k: str, v: Union[int, str]) -> None: + def addLine(self, k: str, v: int | str) -> None: self.txt += self.makeLine(k, v) - def makeLine(self, k: str, v: Union[str, int]) -> str: + def makeLine(self, k: str, v: str | int) -> str: txt = "" - txt += "%s%s" % (k, v) + txt += f"{k}{v}" return txt def date(self, tm: float) -> str: @@ -183,7 +183,7 @@ from revlog where id > ? """ # Due and cumulative due ###################################################################### - def get_start_end_chunk(self, by: str = "review") -> Tuple[int, Optional[int], int]: + def get_start_end_chunk(self, by: str = "review") -> tuple[int, int | None, int]: start = 0 if self.type == PERIOD_MONTH: end, chunk = 31, 1 @@ -245,7 +245,7 @@ from revlog where id > ? """ return txt def _dueInfo(self, tot: int, num: int) -> str: - i: List[str] = [] + i: list[str] = [] self._line( i, "Total", @@ -264,7 +264,7 @@ and due = ?""" return self._lineTbl(i) def _due( - self, start: Optional[int] = None, end: Optional[int] = None, chunk: int = 1 + self, start: int | None = None, end: int | None = None, chunk: int = 1 ) -> Any: lim = "" if start is not None: @@ -293,7 +293,7 @@ group by day order by day""" data = self._added(days, chunk) if not data: return "" - conf: Dict[str, Any] = dict( + conf: dict[str, Any] = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)], ) @@ -311,12 +311,12 @@ group by day order by day""" txt = self._title("Added", "The number of new cards you have added.") txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards") # total and per day average - tot = sum([i[1] for i in data]) + tot = sum(i[1] for i in data) period = self._periodDays() if not period: # base off date of earliest added card period = self._deckAge("add") - i: List[str] = [] + i: list[str] = [] self._line(i, "Total", "%d cards" % tot) self._line(i, "Average", self._avgDay(tot, period, "cards")) txt += self._lineTbl(i) @@ -328,7 +328,7 @@ group by day order by day""" data = self._done(days, chunk) if not data: return "" - conf: Dict[str, Any] = dict( + conf: dict[str, Any] = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)], ) @@ -384,20 +384,20 @@ group by day order by day""" def _ansInfo( self, - totd: List[Tuple[int, float]], + totd: list[tuple[int, float]], studied: int, first: int, unit: str, convHours: bool = False, - total: Optional[int] = None, - ) -> Tuple[str, int]: + total: int | None = None, + ) -> tuple[str, int]: assert totd tot = totd[-1][1] period = self._periodDays() if not period: # base off earliest repetition date period = self._deckAge("review") - i: List[str] = [] + i: list[str] = [] self._line( i, "Days studied", @@ -432,12 +432,12 @@ group by day order by day""" def _splitRepData( self, - data: List[Tuple[Any, ...]], - spec: Sequence[Tuple[int, str, str]], - ) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]: - sep: Dict[int, Any] = {} + data: list[tuple[Any, ...]], + spec: Sequence[tuple[int, str, str]], + ) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]: + sep: dict[int, Any] = {} totcnt = {} - totd: Dict[int, Any] = {} + totd: dict[int, Any] = {} alltot = [] allcnt: float = 0 for (n, col, lab) in spec: @@ -471,7 +471,7 @@ group by day order by day""" ) return (ret, alltot) - def _added(self, num: Optional[int] = 7, chunk: int = 1) -> Any: + def _added(self, num: int | None = 7, chunk: int = 1) -> Any: lims = [] if num is not None: lims.append( @@ -498,7 +498,7 @@ group by day order by day""" chunk, ) - def _done(self, num: Optional[int] = 7, chunk: int = 1) -> Any: + def _done(self, num: int | None = 7, chunk: int = 1) -> Any: lims = [] if num is not None: lims.append( @@ -605,12 +605,12 @@ group by day order by day)""" yaxes=[dict(), dict(position="right", max=105)], ), ) - i: List[str] = [] + i: list[str] = [] self._line(i, "Average interval", self.col.format_timespan(avg * 86400)) self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400)) return txt + self._lineTbl(i) - def _ivls(self) -> Tuple[List[Any], int]: + def _ivls(self) -> tuple[list[Any], int]: start, end, chunk = self.get_start_end_chunk() lim = "and grp <= %d" % end if end else "" data = [ @@ -643,7 +643,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE # 3 + 4 + 4 + spaces on sides and middle = 15 # yng starts at 1+3+1 = 5 # mtr starts at 5+4+1 = 10 - d: Dict[str, List] = {"lrn": [], "yng": [], "mtr": []} + d: dict[str, list] = {"lrn": [], "yng": [], "mtr": []} types = ("lrn", "yng", "mtr") eases = self._eases() for (type, ease, cnt) in eases: @@ -685,7 +685,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE txt += self._easeInfo(eases) return txt - def _easeInfo(self, eases: List[Tuple[int, int, int]]) -> str: + def _easeInfo(self, eases: list[tuple[int, int, int]]) -> str: types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]} for (type, ease, cnt) in eases: if ease == 1: @@ -752,7 +752,7 @@ order by thetype, ease""" shifted = [] counts = [] mcount = 0 - trend: List[Tuple[int, int]] = [] + trend: list[tuple[int, int]] = [] peak = 0 for d in data: hour = (d[0] - 4) % 24 @@ -852,9 +852,9 @@ group by hour having count() > 30 order by hour""" ("Suspended+Buried", colSusp), ) ): - d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col)) + d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col)) # text data - i: List[str] = [] + i: list[str] = [] (c, f) = self.col.db.first( """ select count(id), count(distinct nid) from cards @@ -880,9 +880,7 @@ when you answer "good" on a review.""" ) return txt - def _line( - self, i: List[str], a: str, b: Union[int, str], bold: bool = True - ) -> None: + def _line(self, i: list[str], a: str, b: int | str, bold: bool = True) -> None: # T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews". colon = ":" if bold: @@ -896,7 +894,7 @@ when you answer "good" on a review.""" % (a, colon, b) ) - def _lineTbl(self, i: List[str]) -> str: + def _lineTbl(self, i: list[str]) -> str: return "" + "".join(i) + "
" def _factors(self) -> Any: @@ -945,7 +943,7 @@ from cards where did in %s""" self, id: str, data: Any, - conf: Optional[Any] = None, + conf: Any | None = None, type: str = "bars", xunit: int = 1, ylabel: str = "Cards", @@ -1069,7 +1067,7 @@ $(function () { ) def _title(self, title: str, subtitle: str = "") -> str: - return "

%s

%s" % (title, subtitle) + return f"

{title}

{subtitle}" def _deckAge(self, by: str) -> int: lim = self._revlogLimit() @@ -1089,7 +1087,7 @@ $(function () { period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400))) return period - def _periodDays(self) -> Optional[int]: + def _periodDays(self) -> int | None: start, end, chunk = self.get_start_end_chunk() if end is None: return None diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py index eb5dad1a2..ff93f5bb4 100644 --- a/pylib/anki/stdmodels.py +++ b/pylib/anki/stdmodels.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, Callable, List, Tuple +from typing import Any, Callable import anki.collection import anki.models @@ -15,7 +15,7 @@ StockNotetypeKind = notetypes_pb2.StockNotetype.Kind # add-on authors can add ("note type name", function) # to this list to have it shown in the add/clone note type screen -models: List[Tuple] = [] +models: list[tuple] = [] def _get_stock_notetype( @@ -26,9 +26,9 @@ def _get_stock_notetype( def get_stock_notetypes( col: anki.collection.Collection, -) -> List[Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]: - out: List[ - Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] +) -> list[tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]: + out: list[ + tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] ] = [] # add standard for kind in [ diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py index 9e6852eda..2e7c2a916 100644 --- a/pylib/anki/syncserver/__init__.py +++ b/pylib/anki/syncserver/__init__.py @@ -116,7 +116,7 @@ def after_full_sync() -> None: def get_method( method_str: str, -) -> Optional[SyncServerMethodRequest.Method.V]: # pylint: disable=no-member +) -> SyncServerMethodRequest.Method.V | None: # pylint: disable=no-member s = method_str if s == "hostKey": return Method.HOST_KEY diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index c1fd64b2a..425987ebc 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -13,7 +13,7 @@ from __future__ import annotations import pprint import re -from typing import Collection, List, Match, Optional, Sequence +from typing import Collection, Match, Sequence import anki # pylint: disable=unused-import import anki.collection @@ -33,7 +33,7 @@ class TagManager: self.col = col.weakref() # legacy add-on code expects a List return type - def all(self) -> List[str]: + def all(self) -> list[str]: return list(self.col._backend.all_tags()) def __repr__(self) -> str: @@ -50,7 +50,7 @@ class TagManager: def clear_unused_tags(self) -> OpChangesWithCount: return self.col._backend.clear_unused_tags() - def byDeck(self, did: DeckId, children: bool = False) -> List[str]: + def byDeck(self, did: DeckId, children: bool = False) -> list[str]: basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" if not children: query = f"{basequery} AND c.did=?" @@ -123,11 +123,11 @@ class TagManager: # String-based utilities ########################################################################## - def split(self, tags: str) -> List[str]: + def split(self, tags: str) -> list[str]: "Parse a string and return a list of tags." return [t for t in tags.replace("\u3000", " ").split(" ") if t] - def join(self, tags: List[str]) -> str: + def join(self, tags: list[str]) -> str: "Join tags into a single string, with leading and trailing spaces." if not tags: return "" @@ -164,30 +164,30 @@ class TagManager: ########################################################################## # this is now a no-op - the tags are canonified when the note is saved - def canonify(self, tagList: List[str]) -> List[str]: + def canonify(self, tagList: list[str]) -> list[str]: return tagList - def inList(self, tag: str, tags: List[str]) -> bool: + def inList(self, tag: str, tags: list[str]) -> bool: "True if TAG is in TAGS. Ignore case." return tag.lower() in [t.lower() for t in tags] # legacy ########################################################################## - def registerNotes(self, nids: Optional[List[int]] = None) -> None: + def registerNotes(self, nids: list[int] | None = None) -> None: self.clear_unused_tags() def register( - self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False + self, tags: Collection[str], usn: int | None = None, clear: bool = False ) -> None: print("tags.register() is deprecated and no longer works") - def bulkAdd(self, ids: List[NoteId], tags: str, add: bool = True) -> None: + def bulkAdd(self, ids: list[NoteId], tags: str, add: bool = True) -> None: "Add tags in bulk. TAGS is space-separated." if add: self.bulk_add(ids, tags) else: self.bulk_remove(ids, tags) - def bulkRem(self, ids: List[NoteId], tags: str) -> None: + def bulkRem(self, ids: list[NoteId], tags: str) -> None: self.bulkAdd(ids, tags, False) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index ab4a57a4b..b4d4fb206 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -29,7 +29,7 @@ template_legacy.py file, using the legacy addHook() system. from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Sequence, Union import anki from anki import card_rendering_pb2, hooks @@ -50,10 +50,10 @@ CARD_BLANK_HELP = ( class TemplateReplacement: field_name: str current_text: str - filters: List[str] + filters: list[str] -TemplateReplacementList = List[Union[str, TemplateReplacement]] +TemplateReplacementList = list[Union[str, TemplateReplacement]] @dataclass @@ -105,7 +105,7 @@ def av_tag_to_native(tag: card_rendering_pb2.AVTag) -> AVTag: ) -def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> List[AVTag]: +def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> list[AVTag]: return list(map(av_tag_to_native, tags)) @@ -125,7 +125,7 @@ class TemplateRenderContext: note: Note, card: Card, notetype: NotetypeDict, - template: Dict, + template: dict, fill_empty: bool, ) -> TemplateRenderContext: return TemplateRenderContext( @@ -144,7 +144,7 @@ class TemplateRenderContext: note: Note, browser: bool = False, notetype: NotetypeDict = None, - template: Optional[Dict] = None, + template: dict | None = None, fill_empty: bool = False, ) -> None: self._col = col.weakref() @@ -153,7 +153,7 @@ class TemplateRenderContext: self._browser = browser self._template = template self._fill_empty = fill_empty - self._fields: Optional[Dict] = None + self._fields: dict | None = None self._latex_svg = False if not notetype: self._note_type = note.note_type() @@ -162,12 +162,12 @@ class TemplateRenderContext: # if you need to store extra state to share amongst rendering # hooks, you can insert it into this dictionary - self.extra_state: Dict[str, Any] = {} + self.extra_state: dict[str, Any] = {} def col(self) -> anki.collection.Collection: return self._col - def fields(self) -> Dict[str, str]: + def fields(self) -> dict[str, str]: print(".fields() is obsolete, use .note() or .card()") if not self._fields: # fields from note @@ -269,8 +269,8 @@ class TemplateRenderOutput: "Stores the rendered templates and extracted AV tags." question_text: str answer_text: str - question_av_tags: List[AVTag] - answer_av_tags: List[AVTag] + question_av_tags: list[AVTag] + answer_av_tags: list[AVTag] css: str = "" def question_and_style(self) -> str: @@ -281,7 +281,7 @@ class TemplateRenderOutput: # legacy -def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]: +def templates_for_card(card: Card, browser: bool) -> tuple[str, str]: template = card.template() if browser: q, a = template.get("bqfmt"), template.get("bafmt") @@ -295,7 +295,7 @@ def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]: def apply_custom_filters( rendered: TemplateReplacementList, ctx: TemplateRenderContext, - front_side: Optional[str], + front_side: str | None, ) -> str: "Complete rendering by applying any pending custom filters." # template already fully rendered? diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index e59629af0..3f0a7f476 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -17,11 +17,11 @@ import time import traceback from contextlib import contextmanager from hashlib import sha1 -from typing import Any, Iterable, Iterator, List, Optional, Union +from typing import Any, Iterable, Iterator from anki.dbproxy import DBProxy -_tmpdir: Optional[str] +_tmpdir: str | None try: # pylint: disable=c-extension-no-member @@ -89,7 +89,7 @@ def htmlToTextLine(s: str) -> str: ############################################################################## -def ids2str(ids: Iterable[Union[int, str]]) -> str: +def ids2str(ids: Iterable[int | str]) -> str: """Given a list of integers, return a string '(int1,int2,...)'.""" return f"({','.join(str(i) for i in ids)})" @@ -140,11 +140,11 @@ def guid64() -> str: ############################################################################## -def joinFields(list: List[str]) -> str: +def joinFields(list: list[str]) -> str: return "\x1f".join(list) -def splitFields(string: str) -> List[str]: +def splitFields(string: str) -> list[str]: return string.split("\x1f") @@ -152,7 +152,7 @@ def splitFields(string: str) -> List[str]: ############################################################################## -def checksum(data: Union[bytes, str]) -> str: +def checksum(data: bytes | str) -> str: if isinstance(data, str): data = data.encode("utf-8") return sha1(data).hexdigest() @@ -218,7 +218,7 @@ def noBundledLibs() -> Iterator[None]: os.environ["LD_LIBRARY_PATH"] = oldlpath -def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int: +def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int: "Execute a command. If WAIT, return exit code." # ensure we don't open a separate window for forking process on windows if isWin: @@ -262,7 +262,7 @@ devMode = os.getenv("ANKIDEV", "") invalidFilenameChars = ':*?"<>|' -def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]: +def invalidFilename(str: str, dirsep: bool = True) -> str | None: for c in invalidFilenameChars: if c in str: return c diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index f201ae47c..dbad433b5 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -268,7 +268,9 @@ def test_chained_mods(): a1 = "sentence" q2 = 'en chaine' a2 = "chained" - note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % ( + note[ + "Text" + ] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format( q1, a1, q2, diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index fbf5cbb0c..98af76223 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -4,7 +4,7 @@ import copy import os import time -from typing import Tuple +from typing import Dict import pytest @@ -433,7 +433,7 @@ def test_reviews(): assert "leech" in c.note().tags -def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]: +def review_limits_setup() -> tuple[anki.collection.Collection, Dict]: col = getEmptyCol() parent = col.decks.get(col.decks.id("parent")) diff --git a/pylib/tools/hookslib.py b/pylib/tools/hookslib.py index c4331b22b..fb601f231 100644 --- a/pylib/tools/hookslib.py +++ b/pylib/tools/hookslib.py @@ -21,7 +21,7 @@ class Hook: name: str # string of the typed arguments passed to the callback, eg # ["kind: str", "val: int"] - args: List[str] = None + args: list[str] = None # string of the return type. if set, hook is a filter. return_type: Optional[str] = None # if add-ons may be relying on the legacy hook name, add it here @@ -41,7 +41,7 @@ class Hook: types_str = ", ".join(types) return f"Callable[[{types_str}], {self.return_type or 'None'}]" - def arg_names(self) -> List[str]: + def arg_names(self) -> list[str]: names = [] for arg in self.args or []: if not arg: @@ -64,7 +64,7 @@ class Hook: def list_code(self) -> str: return f"""\ - _hooks: List[{self.callable()}] = [] + _hooks: list[{self.callable()}] = [] """ def code(self) -> str: @@ -153,7 +153,7 @@ class {self.classname()}: return f"{out}\n\n" -def write_file(path: str, hooks: List[Hook], prefix: str, suffix: str): +def write_file(path: str, hooks: list[Hook], prefix: str, suffix: str): hooks.sort(key=attrgetter("name")) code = f"{prefix}\n" for hook in hooks: diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 2a84a181c..c74149823 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + import argparse import builtins import cProfile @@ -10,7 +12,7 @@ import os import sys import tempfile import traceback -from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast +from typing import Any, Callable, Optional, cast import anki.lang from anki._backend import RustBackend @@ -90,7 +92,7 @@ from aqt import stats, about, preferences, mediasync # isort:skip class DialogManager: - _dialogs: Dict[str, list] = { + _dialogs: dict[str, list] = { "AddCards": [addcards.AddCards, None], "AddonsDialog": [addons.AddonsDialog, None], "Browser": [browser.Browser, None], @@ -267,7 +269,7 @@ class AnkiApp(QApplication): KEY = f"anki{checksum(getpass.getuser())}" TMOUT = 30000 - def __init__(self, argv: List[str]) -> None: + def __init__(self, argv: list[str]) -> None: QApplication.__init__(self, argv) self._argv = argv @@ -328,7 +330,7 @@ class AnkiApp(QApplication): return QApplication.event(self, evt) -def parseArgs(argv: List[str]) -> Tuple[argparse.Namespace, List[str]]: +def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]: "Returns (opts, args)." # py2app fails to strip this in some instances, then anki dies # as there's no such profile @@ -444,7 +446,7 @@ def run() -> None: ) -def _run(argv: Optional[List[str]] = None, exec: bool = True) -> Optional[AnkiApp]: +def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]: """Start AnkiQt application or reuse an existing instance if one exists. If the function is invoked with exec=False, the AnkiQt will not enter diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 56bcc28a2..1e418bfe7 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -1,7 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Callable, List, Optional +from typing import Callable, Optional import aqt.editor import aqt.forms @@ -50,7 +50,7 @@ class AddCards(QDialog): self.setupEditor() self.setupButtons() self._load_new_note() - self.history: List[NoteId] = [] + self.history: list[NoteId] = [] self._last_added_note: Optional[Note] = None gui_hooks.operation_did_execute.append(self.on_operation_did_execute) restoreGeom(self, "add") diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 3db0dd9db..12ffe8fcc 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -11,7 +11,7 @@ from collections import defaultdict from concurrent.futures import Future from dataclasses import dataclass from datetime import datetime -from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union +from typing import IO, Any, Callable, Iterable, Union from urllib.parse import parse_qs, urlparse from zipfile import ZipFile @@ -53,7 +53,7 @@ class AbortAddonImport(Exception): @dataclass class InstallOk: name: str - conflicts: List[str] + conflicts: list[str] compatible: bool @@ -75,13 +75,13 @@ class DownloadOk: @dataclass class DownloadError: # set if result was not 200 - status_code: Optional[int] = None + status_code: int | None = None # set if an exception occurred - exception: Optional[Exception] = None + exception: Exception | None = None # first arg is add-on id -DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]] +DownloadLogEntry = tuple[int, Union[DownloadError, InstallError, InstallOk]] @dataclass @@ -101,21 +101,21 @@ current_point_version = anki.utils.pointVersion() @dataclass class AddonMeta: dir_name: str - provided_name: Optional[str] + provided_name: str | None enabled: bool installed_at: int - conflicts: List[str] + conflicts: list[str] min_point_version: int max_point_version: int branch_index: int - human_version: Optional[str] + human_version: str | None update_enabled: bool - homepage: Optional[str] + homepage: str | None def human_name(self) -> str: return self.provided_name or self.dir_name - def ankiweb_id(self) -> Optional[int]: + def ankiweb_id(self) -> int | None: m = ANKIWEB_ID_RE.match(self.dir_name) if m: return int(m.group(0)) @@ -134,13 +134,13 @@ class AddonMeta: def is_latest(self, server_update_time: int) -> bool: return self.installed_at >= server_update_time - def page(self) -> Optional[str]: + def page(self) -> str | None: if self.ankiweb_id(): return f"{aqt.appShared}info/{self.dir_name}" return self.homepage @staticmethod - def from_json_meta(dir_name: str, json_meta: Dict[str, Any]) -> AddonMeta: + def from_json_meta(dir_name: str, json_meta: dict[str, Any]) -> AddonMeta: return AddonMeta( dir_name=dir_name, provided_name=json_meta.get("name"), @@ -207,7 +207,7 @@ class AddonManager: sys.path.insert(0, self.addonsFolder()) # in new code, you may want all_addon_meta() instead - def allAddons(self) -> List[str]: + def allAddons(self) -> list[str]: l = [] for d in os.listdir(self.addonsFolder()): path = self.addonsFolder(d) @@ -222,7 +222,7 @@ class AddonManager: def all_addon_meta(self) -> Iterable[AddonMeta]: return map(self.addon_meta, self.allAddons()) - def addonsFolder(self, dir: Optional[str] = None) -> str: + def addonsFolder(self, dir: str | None = None) -> str: root = self.mw.pm.addonFolder() if dir is None: return root @@ -280,7 +280,7 @@ class AddonManager: return os.path.join(self.addonsFolder(dir), "meta.json") # in new code, use self.addon_meta() instead - def addonMeta(self, dir: str) -> Dict[str, Any]: + def addonMeta(self, dir: str) -> dict[str, Any]: path = self._addonMetaPath(dir) try: with open(path, encoding="utf8") as f: @@ -293,12 +293,12 @@ class AddonManager: return dict() # in new code, use write_addon_meta() instead - def writeAddonMeta(self, dir: str, meta: Dict[str, Any]) -> None: + def writeAddonMeta(self, dir: str, meta: dict[str, Any]) -> None: path = self._addonMetaPath(dir) with open(path, "w", encoding="utf8") as f: json.dump(meta, f) - def toggleEnabled(self, dir: str, enable: Optional[bool] = None) -> None: + def toggleEnabled(self, dir: str, enable: bool | None = None) -> None: addon = self.addon_meta(dir) should_enable = enable if enable is not None else not addon.enabled if should_enable is True: @@ -316,7 +316,7 @@ class AddonManager: addon.enabled = should_enable self.write_addon_meta(addon) - def ankiweb_addons(self) -> List[int]: + def ankiweb_addons(self) -> list[int]: ids = [] for meta in self.all_addon_meta(): if meta.ankiweb_id() is not None: @@ -332,7 +332,7 @@ class AddonManager: def addonName(self, dir: str) -> str: return self.addon_meta(dir).human_name() - def addonConflicts(self, dir: str) -> List[str]: + def addonConflicts(self, dir: str) -> list[str]: return self.addon_meta(dir).conflicts def annotatedName(self, dir: str) -> str: @@ -345,8 +345,8 @@ class AddonManager: # Conflict resolution ###################################################################### - def allAddonConflicts(self) -> Dict[str, List[str]]: - all_conflicts: Dict[str, List[str]] = defaultdict(list) + def allAddonConflicts(self) -> dict[str, list[str]]: + all_conflicts: dict[str, list[str]] = defaultdict(list) for addon in self.all_addon_meta(): if not addon.enabled: continue @@ -354,7 +354,7 @@ class AddonManager: all_conflicts[other_dir].append(addon.dir_name) return all_conflicts - def _disableConflicting(self, dir: str, conflicts: List[str] = None) -> List[str]: + def _disableConflicting(self, dir: str, conflicts: list[str] = None) -> list[str]: conflicts = conflicts or self.addonConflicts(dir) installed = self.allAddons() @@ -371,7 +371,7 @@ class AddonManager: # Installing and deleting add-ons ###################################################################### - def readManifestFile(self, zfile: ZipFile) -> Dict[Any, Any]: + def readManifestFile(self, zfile: ZipFile) -> dict[Any, Any]: try: with zfile.open("manifest.json") as f: data = json.loads(f.read()) @@ -385,8 +385,8 @@ class AddonManager: return manifest def install( - self, file: Union[IO, str], manifest: Dict[str, Any] = None - ) -> Union[InstallOk, InstallError]: + self, file: IO | str, manifest: dict[str, Any] = None + ) -> InstallOk | InstallError: """Install add-on from path or file-like object. Metadata is read from the manifest file, with keys overriden by supplying a 'manifest' dictionary""" @@ -463,8 +463,8 @@ class AddonManager: ###################################################################### def processPackages( - self, paths: List[str], parent: QWidget = None - ) -> Tuple[List[str], List[str]]: + self, paths: list[str], parent: QWidget = None + ) -> tuple[list[str], list[str]]: log = [] errs = [] @@ -493,7 +493,7 @@ class AddonManager: def _installationErrorReport( self, result: InstallError, base: str, mode: str = "download" - ) -> List[str]: + ) -> list[str]: messages = { "zip": tr.addons_corrupt_addon_file(), @@ -511,7 +511,7 @@ class AddonManager: def _installationSuccessReport( self, result: InstallOk, base: str, mode: str = "download" - ) -> List[str]: + ) -> list[str]: name = result.name or base if mode == "download": @@ -536,8 +536,8 @@ class AddonManager: # Updating ###################################################################### - def extract_update_info(self, items: List[Dict]) -> List[UpdateInfo]: - def extract_one(item: Dict) -> UpdateInfo: + def extract_update_info(self, items: list[dict]) -> list[UpdateInfo]: + def extract_one(item: dict) -> UpdateInfo: id = item["id"] meta = self.addon_meta(str(id)) branch_idx = meta.branch_index @@ -545,7 +545,7 @@ class AddonManager: return list(map(extract_one, items)) - def update_supported_versions(self, items: List[UpdateInfo]) -> None: + def update_supported_versions(self, items: list[UpdateInfo]) -> None: for item in items: self.update_supported_version(item) @@ -581,7 +581,7 @@ class AddonManager: if updated: self.write_addon_meta(addon) - def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]: + def updates_required(self, items: list[UpdateInfo]) -> list[UpdateInfo]: """Return ids of add-ons requiring an update.""" need_update = [] for item in items: @@ -600,10 +600,10 @@ class AddonManager: # Add-on Config ###################################################################### - _configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {} - _configUpdatedActions: Dict[str, Callable[[Any], None]] = {} + _configButtonActions: dict[str, Callable[[], bool | None]] = {} + _configUpdatedActions: dict[str, Callable[[Any], None]] = {} - def addonConfigDefaults(self, dir: str) -> Optional[Dict[str, Any]]: + def addonConfigDefaults(self, dir: str) -> dict[str, Any] | None: path = os.path.join(self.addonsFolder(dir), "config.json") try: with open(path, encoding="utf8") as f: @@ -622,7 +622,7 @@ class AddonManager: def addonFromModule(self, module: str) -> str: return module.split(".")[0] - def configAction(self, addon: str) -> Callable[[], Optional[bool]]: + def configAction(self, addon: str) -> Callable[[], bool | None]: return self._configButtonActions.get(addon) def configUpdatedAction(self, addon: str) -> Callable[[Any], None]: @@ -649,7 +649,7 @@ class AddonManager: # Add-on Config API ###################################################################### - def getConfig(self, module: str) -> Optional[Dict[str, Any]]: + def getConfig(self, module: str) -> dict[str, Any] | None: addon = self.addonFromModule(module) # get default config config = self.addonConfigDefaults(addon) @@ -661,7 +661,7 @@ class AddonManager: config.update(userConf) return config - def setConfigAction(self, module: str, fn: Callable[[], Optional[bool]]) -> None: + def setConfigAction(self, module: str, fn: Callable[[], bool | None]) -> None: addon = self.addonFromModule(module) self._configButtonActions[addon] = fn @@ -700,7 +700,7 @@ class AddonManager: # Web Exports ###################################################################### - _webExports: Dict[str, str] = {} + _webExports: dict[str, str] = {} def setWebExports(self, module: str, pattern: str) -> None: addon = self.addonFromModule(module) @@ -825,18 +825,18 @@ class AddonsDialog(QDialog): gui_hooks.addons_dialog_did_change_selected_addon(self, addon) return - def selectedAddons(self) -> List[str]: + def selectedAddons(self) -> list[str]: idxs = [x.row() for x in self.form.addonList.selectedIndexes()] return [self.addons[idx].dir_name for idx in idxs] - def onlyOneSelected(self) -> Optional[str]: + def onlyOneSelected(self) -> str | None: dirs = self.selectedAddons() if len(dirs) != 1: showInfo(tr.addons_please_select_a_single_addon_first()) return None return dirs[0] - def selected_addon_meta(self) -> Optional[AddonMeta]: + def selected_addon_meta(self) -> AddonMeta | None: idxs = [x.row() for x in self.form.addonList.selectedIndexes()] if len(idxs) != 1: showInfo(tr.addons_please_select_a_single_addon_first()) @@ -887,14 +887,14 @@ class AddonsDialog(QDialog): if obj.ids: download_addons(self, self.mgr, obj.ids, self.after_downloading) - def after_downloading(self, log: List[DownloadLogEntry]) -> None: + def after_downloading(self, log: list[DownloadLogEntry]) -> None: self.redrawAddons() if log: show_log_to_user(self, log) else: tooltip(tr.addons_no_updates_available()) - def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]: + def onInstallFiles(self, paths: list[str] | None = None) -> bool | None: if not paths: key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})" paths_ = getFile( @@ -943,7 +943,7 @@ class GetAddons(QDialog): self.addonsDlg = dlg self.mgr = dlg.mgr self.mw = self.mgr.mw - self.ids: List[int] = [] + self.ids: list[int] = [] self.form = aqt.forms.getaddons.Ui_Dialog() self.form.setupUi(self) b = self.form.buttonBox.addButton( @@ -974,7 +974,7 @@ class GetAddons(QDialog): ###################################################################### -def download_addon(client: HttpClient, id: int) -> Union[DownloadOk, DownloadError]: +def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError: "Fetch a single add-on from AnkiWeb." try: resp = client.get( @@ -1025,7 +1025,7 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta: return meta -def download_log_to_html(log: List[DownloadLogEntry]) -> str: +def download_log_to_html(log: list[DownloadLogEntry]) -> str: return "
".join(map(describe_log_entry, log)) @@ -1053,7 +1053,7 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str: return buf -def download_encountered_problem(log: List[DownloadLogEntry]) -> bool: +def download_encountered_problem(log: list[DownloadLogEntry]) -> bool: return any(not isinstance(e[1], InstallOk) for e in log) @@ -1099,10 +1099,10 @@ class DownloaderInstaller(QObject): self.client.progress_hook = bg_thread_progress def download( - self, ids: List[int], on_done: Callable[[List[DownloadLogEntry]], None] + self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None] ) -> None: self.ids = ids - self.log: List[DownloadLogEntry] = [] + self.log: list[DownloadLogEntry] = [] self.dl_bytes = 0 self.last_tooltip = 0 @@ -1135,7 +1135,7 @@ class DownloaderInstaller(QObject): self.mgr.mw.progress.timer(50, lambda: self.on_done(self.log), False) -def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None: +def show_log_to_user(parent: QWidget, log: list[DownloadLogEntry]) -> None: have_problem = download_encountered_problem(log) if have_problem: @@ -1153,9 +1153,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None: def download_addons( parent: QWidget, mgr: AddonManager, - ids: List[int], - on_done: Callable[[List[DownloadLogEntry]], None], - client: Optional[HttpClient] = None, + ids: list[int], + on_done: Callable[[list[DownloadLogEntry]], None], + client: HttpClient | None = None, ) -> None: if client is None: client = HttpClient() @@ -1174,7 +1174,7 @@ class ChooseAddonsToUpdateList(QListWidget): self, parent: QWidget, mgr: AddonManager, - updated_addons: List[UpdateInfo], + updated_addons: list[UpdateInfo], ) -> None: QListWidget.__init__(self, parent) self.mgr = mgr @@ -1266,7 +1266,7 @@ class ChooseAddonsToUpdateList(QListWidget): return self.check_item(self.header_item, Qt.Checked) - def get_selected_addon_ids(self) -> List[int]: + def get_selected_addon_ids(self) -> list[int]: addon_ids = [] for i in range(1, self.count()): item = self.item(i) @@ -1286,7 +1286,7 @@ class ChooseAddonsToUpdateList(QListWidget): class ChooseAddonsToUpdateDialog(QDialog): def __init__( - self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo] + self, parent: QWidget, mgr: AddonManager, updated_addons: list[UpdateInfo] ) -> None: QDialog.__init__(self, parent) self.setWindowTitle(tr.addons_choose_update_window_title()) @@ -1312,7 +1312,7 @@ class ChooseAddonsToUpdateDialog(QDialog): layout.addWidget(button_box) self.setLayout(layout) - def ask(self) -> List[int]: + def ask(self) -> list[int]: "Returns a list of selected addons' ids" ret = self.exec_() saveGeom(self, "addonsChooseUpdate") @@ -1323,9 +1323,9 @@ class ChooseAddonsToUpdateDialog(QDialog): return [] -def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]: +def fetch_update_info(client: HttpClient, ids: list[int]) -> list[dict]: """Fetch update info from AnkiWeb in one or more batches.""" - all_info: List[Dict] = [] + all_info: list[dict] = [] while ids: # get another chunk @@ -1340,7 +1340,7 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]: def _fetch_update_info_batch( client: HttpClient, chunk: Iterable[str] -) -> Iterable[Dict]: +) -> Iterable[dict]: """Get update info from AnkiWeb. Chunk must not contain more than 25 ids.""" @@ -1354,21 +1354,21 @@ def _fetch_update_info_batch( def check_and_prompt_for_updates( parent: QWidget, mgr: AddonManager, - on_done: Callable[[List[DownloadLogEntry]], None], + on_done: Callable[[list[DownloadLogEntry]], None], requested_by_user: bool = True, ) -> None: - def on_updates_received(client: HttpClient, items: List[Dict]) -> None: + def on_updates_received(client: HttpClient, items: list[dict]) -> None: handle_update_info(parent, mgr, client, items, on_done, requested_by_user) check_for_updates(mgr, on_updates_received) def check_for_updates( - mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None] + mgr: AddonManager, on_done: Callable[[HttpClient, list[dict]], None] ) -> None: client = HttpClient() - def check() -> List[Dict]: + def check() -> list[dict]: return fetch_update_info(client, mgr.ankiweb_addons()) def update_info_received(future: Future) -> None: @@ -1395,7 +1395,7 @@ def check_for_updates( def extract_update_info( - current_point_version: int, current_branch_idx: int, info_json: Dict + current_point_version: int, current_branch_idx: int, info_json: dict ) -> UpdateInfo: "Process branches to determine the updated mod time and min/max versions." branches = info_json["branches"] @@ -1425,8 +1425,8 @@ def handle_update_info( parent: QWidget, mgr: AddonManager, client: HttpClient, - items: List[Dict], - on_done: Callable[[List[DownloadLogEntry]], None], + items: list[dict], + on_done: Callable[[list[DownloadLogEntry]], None], requested_by_user: bool = True, ) -> None: update_info = mgr.extract_update_info(items) @@ -1445,8 +1445,8 @@ def prompt_to_update( parent: QWidget, mgr: AddonManager, client: HttpClient, - updated_addons: List[UpdateInfo], - on_done: Callable[[List[DownloadLogEntry]], None], + updated_addons: list[UpdateInfo], + on_done: Callable[[list[DownloadLogEntry]], None], requested_by_user: bool = True, ) -> None: if not requested_by_user: @@ -1468,7 +1468,7 @@ def prompt_to_update( class ConfigEditor(QDialog): - def __init__(self, dlg: AddonsDialog, addon: str, conf: Dict) -> None: + def __init__(self, dlg: AddonsDialog, addon: str, conf: dict) -> None: super().__init__(dlg) self.addon = addon self.conf = conf @@ -1509,7 +1509,7 @@ class ConfigEditor(QDialog): else: self.form.scrollArea.setVisible(False) - def updateText(self, conf: Dict[str, Any]) -> None: + def updateText(self, conf: dict[str, Any]) -> None: text = json.dumps( conf, ensure_ascii=False, @@ -1584,8 +1584,8 @@ class ConfigEditor(QDialog): def installAddonPackages( addonsManager: AddonManager, - paths: List[str], - parent: Optional[QWidget] = None, + paths: list[str], + parent: QWidget | None = None, warn: bool = False, strictly_modal: bool = False, advise_restart: bool = False, diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 39341f84e..34a6133fe 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Callable, Optional, Sequence, Tuple, Union +from typing import Callable, Sequence import aqt import aqt.forms @@ -89,14 +89,14 @@ class MockModel: class Browser(QMainWindow): mw: AnkiQt col: Collection - editor: Optional[Editor] + editor: Editor | None table: Table def __init__( self, mw: AnkiQt, - card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchNode]]] = None, + card: Card | None = None, + search: tuple[str | SearchNode] | None = None, ) -> None: """ card -- try to select the provided card after executing "search" or @@ -108,8 +108,8 @@ class Browser(QMainWindow): self.mw = mw self.col = self.mw.col self.lastFilter = "" - self.focusTo: Optional[int] = None - self._previewer: Optional[Previewer] = None + self.focusTo: int | None = None + self._previewer: Previewer | None = None self._closeEventHasCleanedUp = False self.form = aqt.forms.browser.Ui_Dialog() self.form.setupUi(self) @@ -119,8 +119,8 @@ class Browser(QMainWindow): restoreSplitter(self.form.splitter, "editor3") self.form.splitter.setChildrenCollapsible(False) # set if exactly 1 row is selected; used by the previewer - self.card: Optional[Card] = None - self.current_card: Optional[Card] = None + self.card: Card | None = None + self.current_card: Card | None = None self.setup_table() self.setupMenus() self.setupHooks() @@ -134,7 +134,7 @@ class Browser(QMainWindow): self.setupSearch(card, search) def on_operation_did_execute( - self, changes: OpChanges, handler: Optional[object] + self, changes: OpChanges, handler: object | None ) -> None: focused = current_window() == self self.table.op_executed(changes, handler, focused) @@ -161,7 +161,7 @@ class Browser(QMainWindow): if changes.note_text or changes.card: self._renderPreview() - def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None: + def on_focus_change(self, new: QWidget | None, old: QWidget | None) -> None: if current_window() == self: self.setUpdatesEnabled(True) self.table.redraw_cells() @@ -263,8 +263,8 @@ class Browser(QMainWindow): def reopen( self, _mw: AnkiQt, - card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchNode]]] = None, + card: Card | None = None, + search: tuple[str | SearchNode] | None = None, ) -> None: if search is not None: self.search_for_terms(*search) @@ -281,8 +281,8 @@ class Browser(QMainWindow): def setupSearch( self, - card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchNode]]] = None, + card: Card | None = None, + search: tuple[str | SearchNode] | None = None, ) -> None: qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) @@ -310,7 +310,7 @@ class Browser(QMainWindow): self.search_for(normed) self.update_history() - def search_for(self, search: str, prompt: Optional[str] = None) -> None: + def search_for(self, search: str, prompt: str | None = None) -> None: """Keep track of search string so that we reuse identical search when refreshing, rather than whatever is currently in the search field. Optionally set the search bar to a different text than the actual search. @@ -354,12 +354,12 @@ class Browser(QMainWindow): without_unicode_isolation(tr_title(total=cur, selected=selected)) ) - def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None: + def search_for_terms(self, *search_terms: str | SearchNode) -> None: search = self.col.build_search_string(*search_terms) self.form.searchEdit.setEditText(search) self.onSearchActivated() - def _default_search(self, card: Optional[Card] = None) -> None: + def _default_search(self, card: Card | None = None) -> None: default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT) if default.strip(): search = default @@ -674,7 +674,7 @@ class Browser(QMainWindow): @ensure_editor_saved def add_tags_to_selected_notes( self, - tags: Optional[str] = None, + tags: str | None = None, ) -> None: "Shows prompt if tags not provided." if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())): @@ -686,7 +686,7 @@ class Browser(QMainWindow): @no_arg_trigger @skip_if_selection_is_empty @ensure_editor_saved - def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None: + def remove_tags_from_selected_notes(self, tags: str | None = None) -> None: "Shows prompt if tags not provided." if not ( tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete()) @@ -697,7 +697,7 @@ class Browser(QMainWindow): parent=self, note_ids=self.selected_notes(), space_separated_tags=tags ).run_in_background(initiator=self) - def _prompt_for_tags(self, prompt: str) -> Optional[str]: + def _prompt_for_tags(self, prompt: str) -> str | None: (tags, ok) = getTag(self, self.col, prompt) if not ok: return None diff --git a/qt/aqt/browser/find_and_replace.py b/qt/aqt/browser/find_and_replace.py index 254c18be1..60b2f3d77 100644 --- a/qt/aqt/browser/find_and_replace.py +++ b/qt/aqt/browser/find_and_replace.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import List, Optional, Sequence +from typing import Sequence import aqt from anki.notes import NoteId @@ -39,7 +39,7 @@ class FindAndReplaceDialog(QDialog): *, mw: AnkiQt, note_ids: Sequence[NoteId], - field: Optional[str] = None, + field: str | None = None, ) -> None: """ If 'field' is passed, only this is added to the field selector. @@ -48,7 +48,7 @@ class FindAndReplaceDialog(QDialog): super().__init__(parent) self.mw = mw self.note_ids = note_ids - self.field_names: List[str] = [] + self.field_names: list[str] = [] self._field = field if field: diff --git a/qt/aqt/browser/find_duplicates.py b/qt/aqt/browser/find_duplicates.py index 69b1c7420..30bdbb13e 100644 --- a/qt/aqt/browser/find_duplicates.py +++ b/qt/aqt/browser/find_duplicates.py @@ -4,7 +4,7 @@ from __future__ import annotations import html -from typing import Any, List, Optional, Tuple +from typing import Any import anki import anki.find @@ -45,7 +45,7 @@ class FindDuplicatesDialog(QDialog): ) form.fields.addItems(fields) restore_combo_index_for_session(form.fields, fields, "findDupesFields") - self._dupesButton: Optional[QPushButton] = None + self._dupesButton: QPushButton | None = None # links form.webView.set_title("find duplicates") @@ -75,7 +75,7 @@ class FindDuplicatesDialog(QDialog): qconnect(search.clicked, on_click) self.show() - def show_duplicates_report(self, dupes: List[Tuple[str, List[NoteId]]]) -> None: + def show_duplicates_report(self, dupes: list[tuple[str, list[NoteId]]]) -> None: if not self._dupesButton: self._dupesButton = b = self.form.buttonBox.addButton( tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole @@ -104,7 +104,7 @@ class FindDuplicatesDialog(QDialog): text += "" self.form.webView.stdHtml(text, context=self) - def _tag_duplicates(self, dupes: List[Tuple[str, List[NoteId]]]) -> None: + def _tag_duplicates(self, dupes: list[tuple[str, list[NoteId]]]) -> None: if not dupes: return diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py index b325655ae..d3a4c882d 100644 --- a/qt/aqt/browser/previewer.py +++ b/qt/aqt/browser/previewer.py @@ -6,7 +6,7 @@ from __future__ import annotations import json import re import time -from typing import Any, Callable, Optional, Tuple, Union +from typing import Any, Callable import aqt.browser from anki.cards import Card @@ -33,14 +33,14 @@ from aqt.theme import theme_manager from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr from aqt.webview import AnkiWebView -LastStateAndMod = Tuple[str, int, int] +LastStateAndMod = tuple[str, int, int] class Previewer(QDialog): - _last_state: Optional[LastStateAndMod] = None + _last_state: LastStateAndMod | None = None _card_changed = False - _last_render: Union[int, float] = 0 - _timer: Optional[QTimer] = None + _last_render: int | float = 0 + _timer: QTimer | None = None _show_both_sides = False def __init__( @@ -57,7 +57,7 @@ class Previewer(QDialog): disable_help_button(self) self.setWindowIcon(icon) - def card(self) -> Optional[Card]: + def card(self) -> Card | None: raise NotImplementedError def card_changed(self) -> bool: @@ -143,7 +143,7 @@ class Previewer(QDialog): if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) - def _update_flag_and_mark_icons(self, card: Optional[Card]) -> None: + def _update_flag_and_mark_icons(self, card: Card | None) -> None: if card: flag = card.user_flag() marked = card.note(reload=True).has_tag(MARKED_TAG) @@ -247,7 +247,7 @@ class Previewer(QDialog): self._state = "question" self.render_card() - def _state_and_mod(self) -> Tuple[str, int, int]: + def _state_and_mod(self) -> tuple[str, int, int]: c = self.card() n = c.note() n.load() @@ -258,7 +258,7 @@ class Previewer(QDialog): class MultiCardPreviewer(Previewer): - def card(self) -> Optional[Card]: + def card(self) -> Card | None: # need to state explicitly it's not implement to avoid W0223 raise NotImplementedError @@ -321,14 +321,14 @@ class MultiCardPreviewer(Previewer): class BrowserPreviewer(MultiCardPreviewer): _last_card_id = 0 - _parent: Optional[aqt.browser.Browser] + _parent: aqt.browser.Browser | None def __init__( self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None] ) -> None: super().__init__(parent=parent, mw=mw, on_close=on_close) - def card(self) -> Optional[Card]: + def card(self) -> Card | None: if self._parent.singleCard: return self._parent.card else: diff --git a/qt/aqt/browser/sidebar/item.py b/qt/aqt/browser/sidebar/item.py index 935719401..085b43604 100644 --- a/qt/aqt/browser/sidebar/item.py +++ b/qt/aqt/browser/sidebar/item.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum, auto -from typing import Callable, Iterable, List, Optional, Union +from typing import Callable, Iterable from anki.collection import SearchNode from aqt.theme import ColoredIcon @@ -59,8 +59,8 @@ class SidebarItem: def __init__( self, name: str, - icon: Union[str, ColoredIcon], - search_node: Optional[SearchNode] = None, + icon: str | ColoredIcon, + search_node: SearchNode | None = None, on_expanded: Callable[[bool], None] = None, expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, @@ -75,24 +75,24 @@ class SidebarItem: self.id = id self.search_node = search_node self.on_expanded = on_expanded - self.children: List["SidebarItem"] = [] - self.tooltip: Optional[str] = None - self._parent_item: Optional["SidebarItem"] = None + self.children: list[SidebarItem] = [] + self.tooltip: str | None = None + self._parent_item: SidebarItem | None = None self._expanded = expanded - self._row_in_parent: Optional[int] = None + self._row_in_parent: int | None = None self._search_matches_self = False self._search_matches_child = False - def add_child(self, cb: "SidebarItem") -> None: + def add_child(self, cb: SidebarItem) -> None: self.children.append(cb) cb._parent_item = self def add_simple( self, name: str, - icon: Union[str, ColoredIcon], + icon: str | ColoredIcon, type: SidebarItemType, - search_node: Optional[SearchNode], + search_node: SearchNode | None, ) -> SidebarItem: "Add child sidebar item, and return it." item = SidebarItem( diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py index 92a282fd3..0f9c53fc3 100644 --- a/qt/aqt/browser/sidebar/toolbar.py +++ b/qt/aqt/browser/sidebar/toolbar.py @@ -4,7 +4,6 @@ from __future__ import annotations from enum import Enum, auto -from typing import Callable, Tuple import aqt from aqt.qt import * @@ -18,7 +17,7 @@ class SidebarTool(Enum): class SidebarToolbar(QToolBar): - _tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = ( + _tools: tuple[tuple[SidebarTool, str, Callable[[], str]], ...] = ( (SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search), (SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select), ) diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py index 0c68c3d98..28da752b8 100644 --- a/qt/aqt/browser/sidebar/tree.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum, auto -from typing import Dict, Iterable, List, Optional, Tuple, cast +from typing import Iterable, cast import aqt from anki.collection import ( @@ -75,8 +75,8 @@ class SidebarTreeView(QTreeView): self.browser = browser self.mw = browser.mw self.col = self.mw.col - self.current_search: Optional[str] = None - self.valid_drop_types: Tuple[SidebarItemType, ...] = () + self.current_search: str | None = None + self.valid_drop_types: tuple[SidebarItemType, ...] = () self._refresh_needed = False self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -140,7 +140,7 @@ class SidebarTreeView(QTreeView): ########################### def op_executed( - self, changes: OpChanges, handler: Optional[object], focused: bool + self, changes: OpChanges, handler: object | None, focused: bool ) -> None: if changes.browser_sidebar and not handler is self: self._refresh_needed = True @@ -198,9 +198,9 @@ class SidebarTreeView(QTreeView): def find_item( self, is_target: Callable[[SidebarItem], bool], - parent: Optional[SidebarItem] = None, - ) -> Optional[SidebarItem]: - def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]: + parent: SidebarItem | None = None, + ) -> SidebarItem | None: + def find_item_rec(parent: SidebarItem) -> SidebarItem | None: if is_target(parent): return parent for child in parent.children: @@ -226,7 +226,7 @@ class SidebarTreeView(QTreeView): def _expand_where_necessary( self, model: SidebarModel, - parent: Optional[QModelIndex] = None, + parent: QModelIndex | None = None, searching: bool = False, ) -> None: scroll_to_first_match = searching @@ -348,7 +348,7 @@ class SidebarTreeView(QTreeView): self.valid_drop_types = tuple(valid_drop_types) - def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool: + def handle_drag_drop(self, sources: list[SidebarItem], target: SidebarItem) -> bool: if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT): return self._handle_drag_drop_decks(sources, target) if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT): @@ -361,7 +361,7 @@ class SidebarTreeView(QTreeView): return False def _handle_drag_drop_decks( - self, sources: List[SidebarItem], target: SidebarItem + self, sources: list[SidebarItem], target: SidebarItem ) -> bool: deck_ids = [ DeckId(source.id) @@ -380,7 +380,7 @@ class SidebarTreeView(QTreeView): return True def _handle_drag_drop_tags( - self, sources: List[SidebarItem], target: SidebarItem + self, sources: list[SidebarItem], target: SidebarItem ) -> bool: tags = [ source.full_name @@ -402,7 +402,7 @@ class SidebarTreeView(QTreeView): return True def _handle_drag_drop_saved_search( - self, sources: List[SidebarItem], _target: SidebarItem + self, sources: list[SidebarItem], _target: SidebarItem ) -> bool: if len(sources) != 1 or sources[0].search_node is None: return False @@ -464,7 +464,7 @@ class SidebarTreeView(QTreeView): ########################### def _root_tree(self) -> SidebarItem: - root: Optional[SidebarItem] = None + root: SidebarItem | None = None for stage in SidebarStage: if stage == SidebarStage.ROOT: @@ -504,7 +504,7 @@ class SidebarTreeView(QTreeView): name: str, icon: Union[str, ColoredIcon], collapse_key: Config.Bool.V, - type: Optional[SidebarItemType] = None, + type: SidebarItemType | None = None, ) -> SidebarItem: def update(expanded: bool) -> None: CollectionOp( @@ -1112,13 +1112,13 @@ class SidebarTreeView(QTreeView): _saved_searches_key = "savedFilters" - def _get_saved_searches(self) -> Dict[str, str]: + def _get_saved_searches(self) -> dict[str, str]: return self.col.get_config(self._saved_searches_key, {}) - def _set_saved_searches(self, searches: Dict[str, str]) -> None: + def _set_saved_searches(self, searches: dict[str, str]) -> None: self.col.set_config(self._saved_searches_key, searches) - def _get_current_search(self) -> Optional[str]: + def _get_current_search(self) -> str | None: try: return self.col.build_search_string(self.browser.current_search()) except Exception as e: @@ -1198,24 +1198,24 @@ class SidebarTreeView(QTreeView): # Helpers #################################### - def _selected_items(self) -> List[SidebarItem]: + def _selected_items(self) -> list[SidebarItem]: return [self.model().item_for_index(idx) for idx in self.selectedIndexes()] - def _selected_decks(self) -> List[DeckId]: + def _selected_decks(self) -> list[DeckId]: return [ DeckId(item.id) for item in self._selected_items() if item.item_type == SidebarItemType.DECK ] - def _selected_saved_searches(self) -> List[str]: + def _selected_saved_searches(self) -> list[str]: return [ item.name for item in self._selected_items() if item.item_type == SidebarItemType.SAVED_SEARCH ] - def _selected_tags(self) -> List[str]: + def _selected_tags(self) -> list[str]: return [ item.full_name for item in self._selected_items() diff --git a/qt/aqt/browser/table/__init__.py b/qt/aqt/browser/table/__init__.py index 4d19ba618..a35cd8ce5 100644 --- a/qt/aqt/browser/table/__init__.py +++ b/qt/aqt/browser/table/__init__.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations import time from dataclasses import dataclass -from typing import TYPE_CHECKING, Generator, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Generator, Sequence, Union import aqt from anki.cards import CardId @@ -24,10 +23,10 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]] class SearchContext: search: str browser: aqt.browser.Browser - order: Union[bool, str, Column] = True + order: bool | str | Column = True reverse: bool = False # if set, provided ids will be used instead of the regular search - ids: Optional[Sequence[ItemId]] = None + ids: Sequence[ItemId] | None = None @dataclass @@ -41,14 +40,14 @@ class CellRow: def __init__( self, - cells: Generator[Tuple[str, bool], None, None], + cells: Generator[tuple[str, bool], None, None], color: BrowserRow.Color.V, font_name: str, font_size: int, ) -> None: self.refreshed_at: float = time.time() - self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells) - self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color) + self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells) + self.color: tuple[str, str] | None = backend_color_to_aqt_color(color) self.font_name: str = font_name or "arial" self.font_size: int = font_size if font_size > 0 else 12 @@ -75,7 +74,7 @@ class CellRow: return row -def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]: +def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> tuple[str, str] | None: if color == BrowserRow.COLOR_MARKED: return colors.MARKED_BG if color == BrowserRow.COLOR_SUSPENDED: diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 96be01121..f2ab6270b 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations import time -from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast +from typing import Any, Callable, Sequence, cast import aqt from anki.cards import Card, CardId @@ -41,13 +40,13 @@ class DataModel(QAbstractTableModel): ) -> None: QAbstractTableModel.__init__(self) self.col: Collection = col - self.columns: Dict[str, Column] = dict( - ((c.key, c) for c in self.col.all_browser_columns()) - ) + self.columns: dict[str, Column] = { + c.key: c for c in self.col.all_browser_columns() + } gui_hooks.browser_did_fetch_columns(self.columns) self._state: ItemState = state self._items: Sequence[ItemId] = [] - self._rows: Dict[int, CellRow] = {} + self._rows: dict[int, CellRow] = {} self._block_updates = False self._stale_cutoff = 0.0 self._on_row_state_will_change = row_state_will_change_callback @@ -77,7 +76,7 @@ class DataModel(QAbstractTableModel): return self._fetch_row_and_update_cache(index, item, None) def _fetch_row_and_update_cache( - self, index: QModelIndex, item: ItemId, old_row: Optional[CellRow] + self, index: QModelIndex, item: ItemId, old_row: CellRow | None ) -> CellRow: """Fetch a row from the backend, add it to the cache and return it. Then fire callbacks if the row is being deleted or restored. @@ -119,7 +118,7 @@ class DataModel(QAbstractTableModel): ) return row - def get_cached_row(self, index: QModelIndex) -> Optional[CellRow]: + def get_cached_row(self, index: QModelIndex) -> CellRow | None: """Get row if it is cached, regardless of staleness.""" return self._rows.get(self.get_item(index)) @@ -175,41 +174,41 @@ class DataModel(QAbstractTableModel): def get_item(self, index: QModelIndex) -> ItemId: return self._items[index.row()] - def get_items(self, indices: List[QModelIndex]) -> Sequence[ItemId]: + def get_items(self, indices: list[QModelIndex]) -> Sequence[ItemId]: return [self.get_item(index) for index in indices] - def get_card_ids(self, indices: List[QModelIndex]) -> Sequence[CardId]: + def get_card_ids(self, indices: list[QModelIndex]) -> Sequence[CardId]: return self._state.get_card_ids(self.get_items(indices)) - def get_note_ids(self, indices: List[QModelIndex]) -> Sequence[NoteId]: + def get_note_ids(self, indices: list[QModelIndex]) -> Sequence[NoteId]: return self._state.get_note_ids(self.get_items(indices)) - def get_note_id(self, index: QModelIndex) -> Optional[NoteId]: + def get_note_id(self, index: QModelIndex) -> NoteId | None: if nid_list := self._state.get_note_ids([self.get_item(index)]): return nid_list[0] return None # Get row numbers from items - def get_item_row(self, item: ItemId) -> Optional[int]: + def get_item_row(self, item: ItemId) -> int | None: for row, i in enumerate(self._items): if i == item: return row return None - def get_item_rows(self, items: Sequence[ItemId]) -> List[int]: + def get_item_rows(self, items: Sequence[ItemId]) -> list[int]: rows = [] for row, i in enumerate(self._items): if i in items: rows.append(row) return rows - def get_card_row(self, card_id: CardId) -> Optional[int]: + def get_card_row(self, card_id: CardId) -> int | None: return self.get_item_row(self._state.get_item_from_card_id(card_id)) # Get objects (cards or notes) - def get_card(self, index: QModelIndex) -> Optional[Card]: + def get_card(self, index: QModelIndex) -> Card | None: """Try to return the indicated, possibly deleted card.""" if not index.isValid(): return None @@ -218,7 +217,7 @@ class DataModel(QAbstractTableModel): except NotFoundError: return None - def get_note(self, index: QModelIndex) -> Optional[Note]: + def get_note(self, index: QModelIndex) -> Note | None: """Try to return the indicated, possibly deleted note.""" if not index.isValid(): return None @@ -280,7 +279,7 @@ class DataModel(QAbstractTableModel): self.columns[key] = addon_column_fillin(key) return self.columns[key] - def active_column_index(self, column: str) -> Optional[int]: + def active_column_index(self, column: str) -> int | None: return ( self._state.active_columns.index(column) if column in self._state.active_columns @@ -317,7 +316,7 @@ class DataModel(QAbstractTableModel): qfont.setPixelSize(row.font_size) return qfont elif role == Qt.TextAlignmentRole: - align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter + align: Qt.AlignmentFlag | int = Qt.AlignVCenter if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER: align |= Qt.AlignHCenter return align @@ -329,7 +328,7 @@ class DataModel(QAbstractTableModel): def headerData( self, section: int, orientation: Qt.Orientation, role: int = 0 - ) -> Optional[str]: + ) -> str | None: if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self._state.column_label(self.column_at_section(section)) return None diff --git a/qt/aqt/browser/table/state.py b/qt/aqt/browser/table/state.py index fc11699d8..4c7043b11 100644 --- a/qt/aqt/browser/table/state.py +++ b/qt/aqt/browser/table/state.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations from abc import ABC, abstractmethod, abstractproperty -from typing import List, Sequence, Union, cast +from typing import Sequence, cast from anki.browser import BrowserConfig from anki.cards import Card, CardId @@ -18,7 +17,7 @@ class ItemState(ABC): GEOMETRY_KEY_PREFIX: str SORT_COLUMN_KEY: str SORT_BACKWARDS_KEY: str - _active_columns: List[str] + _active_columns: list[str] def __init__(self, col: Collection) -> None: self.col = col @@ -57,7 +56,7 @@ class ItemState(ABC): # abstractproperty is deprecated but used due to mypy limitations # (https://github.com/python/mypy/issues/1362) @abstractproperty # pylint: disable=deprecated-decorator - def active_columns(self) -> List[str]: + def active_columns(self) -> list[str]: """Return the saved or default columns for the state.""" @abstractmethod @@ -96,7 +95,7 @@ class ItemState(ABC): @abstractmethod def find_items( - self, search: str, order: Union[bool, str, Column], reverse: bool + self, search: str, order: bool | str | Column, reverse: bool ) -> Sequence[ItemId]: """Return the item ids fitting the given search and order.""" @@ -133,7 +132,7 @@ class CardState(ItemState): self._active_columns = self.col.load_browser_card_columns() @property - def active_columns(self) -> List[str]: + def active_columns(self) -> list[str]: return self._active_columns def toggle_active_column(self, column: str) -> None: @@ -150,7 +149,7 @@ class CardState(ItemState): return self.get_card(item).note() def find_items( - self, search: str, order: Union[bool, str, Column], reverse: bool + self, search: str, order: bool | str | Column, reverse: bool ) -> Sequence[ItemId]: return self.col.find_cards(search, order, reverse) @@ -180,7 +179,7 @@ class NoteState(ItemState): self._active_columns = self.col.load_browser_note_columns() @property - def active_columns(self) -> List[str]: + def active_columns(self) -> list[str]: return self._active_columns def toggle_active_column(self, column: str) -> None: @@ -197,7 +196,7 @@ class NoteState(ItemState): return self.col.get_note(NoteId(item)) def find_items( - self, search: str, order: Union[bool, str, Column], reverse: bool + self, search: str, order: bool | str | Column, reverse: bool ) -> Sequence[ItemId]: return self.col.find_notes(search, order, reverse) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index fd627a871..e7812e052 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations -from typing import Any, Callable, List, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Sequence, cast import aqt import aqt.forms @@ -45,12 +44,12 @@ class Table: self._on_row_state_will_change, self._on_row_state_changed, ) - self._view: Optional[QTableView] = None + self._view: QTableView | None = None # cached for performance self._len_selection = 0 - self._selected_rows: Optional[List[QModelIndex]] = None + self._selected_rows: list[QModelIndex] | None = None # temporarily set for selection preservation - self._current_item: Optional[ItemId] = None + self._current_item: ItemId | None = None self._selected_items: Sequence[ItemId] = [] def set_view(self, view: QTableView) -> None: @@ -89,13 +88,13 @@ class Table: # Get objects - def get_current_card(self) -> Optional[Card]: + def get_current_card(self) -> Card | None: return self._model.get_card(self._current()) - def get_current_note(self) -> Optional[Note]: + def get_current_note(self) -> Note | None: return self._model.get_note(self._current()) - def get_single_selected_card(self) -> Optional[Card]: + def get_single_selected_card(self) -> Card | None: """If there is only one row selected return its card, else None. This may be a different one than the current card.""" if self.len_selection() != 1: @@ -171,7 +170,7 @@ class Table: self._model.redraw_cells() def op_executed( - self, changes: OpChanges, handler: Optional[object], focused: bool + self, changes: OpChanges, handler: object | None, focused: bool ) -> None: if changes.browser_table: self._model.mark_cache_stale() @@ -260,7 +259,7 @@ class Table: def _current(self) -> QModelIndex: return self._view.selectionModel().currentIndex() - def _selected(self) -> List[QModelIndex]: + def _selected(self) -> list[QModelIndex]: if self._selected_rows is None: self._selected_rows = self._view.selectionModel().selectedRows() return self._selected_rows @@ -280,7 +279,7 @@ class Table: self._len_selection = 0 self._selected_rows = None - def _select_rows(self, rows: List[int]) -> None: + def _select_rows(self, rows: list[int]) -> None: selection = QItemSelection() for row in rows: selection.select( @@ -532,9 +531,7 @@ class Table: self._selected_items = [] self._current_item = None - def _qualify_selected_rows( - self, rows: List[int], current: Optional[int] - ) -> List[int]: + def _qualify_selected_rows(self, rows: list[int], current: int | None) -> list[int]: """Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current.""" if rows: if len(rows) < self.SELECTION_LIMIT: @@ -544,7 +541,7 @@ class Table: return rows[0:1] return [current if current else 0] - def _intersected_selection(self) -> Tuple[List[int], Optional[int]]: + def _intersected_selection(self) -> tuple[list[int], int | None]: """Return all rows of items that were in the saved selection and the row of the saved current element if present. """ @@ -554,7 +551,7 @@ class Table: ) return selected_rows, current_row - def _toggled_selection(self) -> Tuple[List[int], Optional[int]]: + def _toggled_selection(self) -> tuple[list[int], int | None]: """Convert the items of the saved selection and current element to the new state and return their rows. """ diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index ae8951b88..aa024bf78 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -3,7 +3,7 @@ import json import re from concurrent.futures import Future -from typing import Any, Dict, List, Match, Optional +from typing import Any, Match, Optional import aqt from anki.collection import OpChanges @@ -135,7 +135,7 @@ class CardLayout(QDialog): combo.setEnabled(not self._isCloze()) self.ignore_change_signals = False - def _summarizedName(self, idx: int, tmpl: Dict) -> str: + def _summarizedName(self, idx: int, tmpl: dict) -> str: return "{}: {}: {} -> {}".format( idx + 1, tmpl["name"], @@ -146,7 +146,7 @@ class CardLayout(QDialog): def _fieldsOnTemplate(self, fmt: str) -> str: matches = re.findall("{{[^#/}]+?}}", fmt) chars_allowed = 30 - field_names: List[str] = [] + field_names: list[str] = [] for m in matches: # strip off mustache m = re.sub(r"[{}]", "", m) @@ -440,7 +440,7 @@ class CardLayout(QDialog): # Reading/writing question/answer/css ########################################################################## - def current_template(self) -> Dict: + def current_template(self) -> dict: if self._isCloze(): return self.templates[0] return self.templates[self.ord] @@ -592,7 +592,7 @@ class CardLayout(QDialog): self.mw.taskman.with_progress(get_count, on_done) - def onRemoveInner(self, template: Dict) -> None: + def onRemoveInner(self, template: dict) -> None: self.mm.remove_template(self.model, template) # ensure current ordinal is within bounds @@ -668,7 +668,7 @@ class CardLayout(QDialog): self._flipQA(old, old) self.redraw_everything() - def _flipQA(self, src: Dict, dst: Dict) -> None: + def _flipQA(self, src: dict, dst: dict) -> None: m = re.match("(?s)(.+)
(.+)", src["afmt"]) if not m: showInfo(tr.card_templates_anki_couldnt_find_the_line_between()) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 008dae830..fc65f3e33 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass -from typing import Any, Optional +from typing import Any import aqt from anki.collection import OpChanges @@ -79,7 +79,7 @@ class DeckBrowser: self.refresh() def op_executed( - self, changes: OpChanges, handler: Optional[object], focused: bool + self, changes: OpChanges, handler: object | None, focused: bool ) -> bool: if changes.study_queues and handler is not self: self._refresh_needed = True @@ -175,11 +175,11 @@ class DeckBrowser: def _renderDeckTree(self, top: DeckTreeNode) -> str: buf = """ -%s -%s +{} +{} -%s -""" % ( +{} +""".format( tr.decks_deck(), tr.actions_new(), tr.statistics_due_count(), diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index c5cf42e5d..a41f6ccee 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -4,7 +4,7 @@ from __future__ import annotations from operator import itemgetter -from typing import Any, Dict, List, Optional +from typing import Any from PyQt5.QtWidgets import QLineEdit @@ -30,7 +30,7 @@ from aqt.utils import ( class DeckConf(QDialog): - def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None: + def __init__(self, mw: aqt.AnkiQt, deck: dict) -> None: QDialog.__init__(self, mw) self.mw = mw self.deck = deck @@ -74,7 +74,7 @@ class DeckConf(QDialog): def setupConfs(self) -> None: qconnect(self.form.dconf.currentIndexChanged, self.onConfChange) - self.conf: Optional[DeckConfigDict] = None + self.conf: DeckConfigDict | None = None self.loadConfs() def loadConfs(self) -> None: @@ -175,7 +175,7 @@ class DeckConf(QDialog): # Loading ################################################## - def listToUser(self, l: List[Union[int, float]]) -> str: + def listToUser(self, l: list[Union[int, float]]) -> str: def num_to_user(n: Union[int, float]) -> str: if n == round(n): return str(int(n)) diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py index a8e5fda46..fe68763f1 100644 --- a/qt/aqt/deckoptions.py +++ b/qt/aqt/deckoptions.py @@ -3,8 +3,6 @@ from __future__ import annotations -from typing import List, Optional - import aqt import aqt.deckconf from anki.cards import Card @@ -67,7 +65,7 @@ class DeckOptionsDialog(QDialog): QDialog.reject(self) -def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> None: +def confirm_deck_then_display_options(active_card: Card | None = None) -> None: decks = [aqt.mw.col.decks.current()] if card := active_card: if card.odid and card.odid != decks[0]["id"]: @@ -83,7 +81,7 @@ def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> Non _deck_prompt_dialog(decks) -def _deck_prompt_dialog(decks: List[DeckDict]) -> None: +def _deck_prompt_dialog(decks: list[DeckDict]) -> None: diag = QDialog(aqt.mw.app.activeWindow()) diag.setWindowTitle("Anki") box = QVBoxLayout() diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 3ce7e5dc5..d80d04ac8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -14,7 +14,7 @@ import urllib.parse import urllib.request import warnings from random import randrange -from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast +from typing import Any, Callable, Match, cast import bs4 import requests @@ -104,14 +104,14 @@ class Editor: self.mw = mw self.widget = widget self.parentWindow = parentWindow - self.note: Optional[Note] = None + self.note: Note | None = None self.addMode = addMode - self.currentField: Optional[int] = None + self.currentField: int | None = None # Similar to currentField, but not set to None on a blur. May be # outside the bounds of the current notetype. - self.last_field_index: Optional[int] = None + self.last_field_index: int | None = None # current card, for card layout - self.card: Optional[Card] = None + self.card: Card | None = None self.setupOuter() self.setupWeb() self.setupShortcuts() @@ -147,7 +147,7 @@ class Editor: default_css=True, ) - lefttopbtns: List[str] = [] + lefttopbtns: list[str] = [] gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) lefttopbtns_defs = [ @@ -156,7 +156,7 @@ class Editor: ] lefttopbtns_js = "\n".join(lefttopbtns_defs) - righttopbtns: List[str] = [] + righttopbtns: list[str] = [] gui_hooks.editor_did_init_buttons(righttopbtns, self) # legacy filter righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) @@ -191,9 +191,9 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def addButton( self, - icon: Optional[str], + icon: str | None, cmd: str, - func: Callable[["Editor"], None], + func: Callable[[Editor], None], tip: str = "", label: str = "", id: str = None, @@ -242,11 +242,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def _addButton( self, - icon: Optional[str], + icon: str | None, cmd: str, tip: str = "", label: str = "", - id: Optional[str] = None, + id: str | None = None, toggleable: bool = False, disables: bool = True, rightside: bool = True, @@ -302,7 +302,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def setupShortcuts(self) -> None: # if a third element is provided, enable shortcut even when no field selected - cuts: List[Tuple] = [] + cuts: list[tuple] = [] gui_hooks.editor_did_init_shortcuts(cuts, self) for row in cuts: if len(row) == 2: @@ -449,7 +449,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ ###################################################################### def set_note( - self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None + self, note: Note | None, hide: bool = True, focusTo: int | None = None ) -> None: "Make NOTE the current note." self.note = note @@ -462,7 +462,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def loadNoteKeepingFocus(self) -> None: self.loadNote(self.currentField) - def loadNote(self, focusTo: Optional[int] = None) -> None: + def loadNote(self, focusTo: int | None = None) -> None: if not self.note: return @@ -488,7 +488,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ text_color = self.mw.pm.profile.get("lastTextColor", "#00f") highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f") - js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s); setColorButtons(%s); setTags(%s); " % ( + js = "setFields({}); setFonts({}); focusField({}); setNoteId({}); setColorButtons({}); setTags({}); ".format( json.dumps(data), json.dumps(self.fonts()), json.dumps(focusTo), @@ -510,7 +510,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ initiator=self ) - def fonts(self) -> List[Tuple[str, int, bool]]: + def fonts(self) -> list[tuple[str, int, bool]]: return [ (gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"]) for f in self.note.note_type()["flds"] @@ -573,7 +573,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ ), ) - def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool: + def fieldsAreBlank(self, previousNote: Note | None = None) -> bool: if not self.note: return True m = self.note.note_type() @@ -700,7 +700,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ # Media downloads ###################################################################### - def urlToLink(self, url: str) -> Optional[str]: + def urlToLink(self, url: str) -> str | None: fname = self.urlToFile(url) if not fname: return None @@ -715,7 +715,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ av_player.play_file(fname) return f"[sound:{html.escape(fname, quote=False)}]" - def urlToFile(self, url: str) -> Optional[str]: + def urlToFile(self, url: str) -> str | None: l = url.lower() for suffix in pics + audio: if l.endswith(f".{suffix}"): @@ -760,7 +760,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ fname = f"paste-{csum}{ext}" return self._addMediaFromData(fname, data) - def _retrieveURL(self, url: str) -> Optional[str]: + def _retrieveURL(self, url: str) -> str | None: "Download file into media folder and return local filename or None." # urllib doesn't understand percent-escaped utf8, but requires things like # '#' to be escaped. @@ -774,7 +774,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ # fetch it into a temporary folder self.mw.progress.start(immediate=not local, parent=self.parentWindow) content_type = None - error_msg: Optional[str] = None + error_msg: str | None = None try: if local: req = urllib.request.Request( @@ -972,7 +972,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ for name, val in list(self.note.items()): m = re.findall(r"\{\{c(\d+)::", val) if m: - highest = max(highest, sorted([int(x) for x in m])[-1]) + highest = max(highest, sorted(int(x) for x in m)[-1]) # reuse last? if not KeyboardModifiersPressed().alt: highest += 1 @@ -1069,7 +1069,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ # Links from HTML ###################################################################### - _links: Dict[str, Callable] = dict( + _links: dict[str, Callable] = dict( fields=onFields, cards=onCardLayout, bold=toggleBold, @@ -1163,7 +1163,7 @@ class EditorWebView(AnkiWebView): # returns (html, isInternal) def _processMime( self, mime: QMimeData, extended: bool = False, drop_event: bool = False - ) -> Tuple[str, bool]: + ) -> tuple[str, bool]: # print("html=%s image=%s urls=%s txt=%s" % ( # mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText())) # print("html", mime.html()) @@ -1193,7 +1193,7 @@ class EditorWebView(AnkiWebView): return html, True return "", False - def _processUrls(self, mime: QMimeData, extended: bool = False) -> Optional[str]: + def _processUrls(self, mime: QMimeData, extended: bool = False) -> str | None: if not mime.hasUrls(): return None @@ -1206,7 +1206,7 @@ class EditorWebView(AnkiWebView): return buf - def _processText(self, mime: QMimeData, extended: bool = False) -> Optional[str]: + def _processText(self, mime: QMimeData, extended: bool = False) -> str | None: if not mime.hasText(): return None @@ -1245,7 +1245,7 @@ class EditorWebView(AnkiWebView): processed.pop() return "".join(processed) - def _processImage(self, mime: QMimeData, extended: bool = False) -> Optional[str]: + def _processImage(self, mime: QMimeData, extended: bool = False) -> str | None: if not mime.hasImage(): return None im = QImage(mime.imageData()) diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 95ed26787..a84bc309f 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -5,7 +5,7 @@ from __future__ import annotations import re from concurrent.futures import Future -from typing import Any, List +from typing import Any import aqt from anki.cards import CardId @@ -89,7 +89,7 @@ class EmptyCardsDialog(QDialog): self.mw.taskman.run_in_background(delete, on_done) def _delete_cards(self, keep_notes: bool) -> int: - to_delete: List[CardId] = [] + to_delete: list[CardId] = [] note: EmptyCardsReport.NoteWithEmptyCards for note in self.report.notes: if keep_notes and note.will_delete_note: diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index 1fb1fcce3..6868373de 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -7,7 +7,6 @@ import os import re import time from concurrent.futures import Future -from typing import List, Optional import aqt from anki import hooks @@ -29,21 +28,21 @@ class ExportDialog(QDialog): def __init__( self, mw: aqt.main.AnkiQt, - did: Optional[DeckId] = None, - cids: Optional[List[CardId]] = None, + did: DeckId | None = None, + cids: list[CardId] | None = None, ): QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.col = mw.col.weakref() self.frm = aqt.forms.exporting.Ui_ExportDialog() self.frm.setupUi(self) - self.exporter: Optional[Exporter] = None + self.exporter: Exporter | None = None self.cids = cids disable_help_button(self) self.setup(did) self.exec_() - def setup(self, did: Optional[DeckId]) -> None: + def setup(self, did: DeckId | None) -> None: self.exporters = exporters(self.col) # if a deck specified, start with .apkg type selected idx = 0 diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index da4894ba2..368f988ef 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional + import aqt from anki.collection import OpChanges from anki.consts import * diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py index d62922d98..1876521ba 100644 --- a/qt/aqt/filtered_deck.py +++ b/qt/aqt/filtered_deck.py @@ -1,7 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import List, Optional, Tuple +from __future__ import annotations import aqt from anki.collection import OpChangesWithId, SearchNode @@ -36,8 +36,8 @@ class FilteredDeckConfigDialog(QDialog): self, mw: AnkiQt, deck_id: DeckId = DeckId(0), - search: Optional[str] = None, - search_2: Optional[str] = None, + search: str | None = None, + search_2: str | None = None, ) -> None: """If 'deck_id' is non-zero, load and modify its settings. Otherwise, build a new deck and derive settings from the current deck. @@ -162,15 +162,13 @@ class FilteredDeckConfigDialog(QDialog): def reopen( self, _mw: AnkiQt, - search: Optional[str] = None, - search_2: Optional[str] = None, - _deck: Optional[DeckDict] = None, + search: str | None = None, + search_2: str | None = None, + _deck: DeckDict | None = None, ) -> None: self.set_custom_searches(search, search_2) - def set_custom_searches( - self, search: Optional[str], search_2: Optional[str] - ) -> None: + def set_custom_searches(self, search: str | None, search_2: str | None) -> None: if search is not None: self.form.search.setText(search) self.form.search.setFocus() @@ -218,12 +216,12 @@ class FilteredDeckConfigDialog(QDialog): else: aqt.dialogs.open("Browser", self.mw, search=(search,)) - def _second_filter(self) -> Tuple[str, ...]: + def _second_filter(self) -> tuple[str, ...]: if self.form.secondFilter.isChecked(): return (self.form.search_2.text(),) return () - def _learning_search_node(self) -> Tuple[SearchNode, ...]: + def _learning_search_node(self) -> tuple[SearchNode, ...]: """Return a search node that matches learning cards if the old scheduler is enabled. If it's a rebuild, exclude cards from this filtered deck as those will be reset. """ @@ -238,7 +236,7 @@ class FilteredDeckConfigDialog(QDialog): return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),) return () - def _filtered_search_node(self) -> Tuple[SearchNode]: + def _filtered_search_node(self) -> tuple[SearchNode]: """Return a search node that matches cards in filtered decks, if applicable excluding those in the deck being rebuild.""" if self.deck.id: @@ -320,12 +318,12 @@ class FilteredDeckConfigDialog(QDialog): ######################################################## # fixme: remove once we drop support for v1 - def listToUser(self, values: List[Union[float, int]]) -> str: + def listToUser(self, values: list[Union[float, int]]) -> str: return " ".join( [str(int(val)) if int(val) == val else str(val) for val in values] ) - def userToList(self, line: QLineEdit, minSize: int = 1) -> Optional[List[float]]: + def userToList(self, line: QLineEdit, minSize: int = 1) -> list[float] | None: items = str(line.text()).split(" ") ret = [] for item in items: diff --git a/qt/aqt/flags.py b/qt/aqt/flags.py index d2767912c..e1e209aeb 100644 --- a/qt/aqt/flags.py +++ b/qt/aqt/flags.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, List, Optional, cast +from typing import cast import aqt from anki.collection import SearchNode @@ -33,9 +33,9 @@ class Flag: class FlagManager: def __init__(self, mw: aqt.main.AnkiQt) -> None: self.mw = mw - self._flags: Optional[List[Flag]] = None + self._flags: list[Flag] | None = None - def all(self) -> List[Flag]: + def all(self) -> list[Flag]: """Return a list of all flags.""" if self._flags is None: self._load_flags() @@ -55,7 +55,7 @@ class FlagManager: gui_hooks.flag_label_did_change() def _load_flags(self) -> None: - labels = cast(Dict[str, str], self.mw.col.get_config("flagLabels", {})) + labels = cast(dict[str, str], self.mw.col.get_config("flagLabels", {})) icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED) self._flags = [ diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index 3ee272bb9..f8211d005 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -8,7 +8,7 @@ import traceback import unicodedata import zipfile from concurrent.futures import Future -from typing import Any, Dict, Optional +from typing import Any, Optional import anki.importing as importing import aqt.deckchooser @@ -34,7 +34,7 @@ from aqt.utils import ( class ChangeMap(QDialog): - def __init__(self, mw: AnkiQt, model: Dict, current: str) -> None: + def __init__(self, mw: AnkiQt, model: dict, current: str) -> None: QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.model = model diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index 8be89c6c0..7b450c3e7 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -5,7 +5,9 @@ Legacy support """ -from typing import Any, List +from __future__ import annotations + +from typing import Any import anki import aqt @@ -20,7 +22,7 @@ def bodyClass(col, card) -> str: # type: ignore return theme_manager.body_classes_for_card_ord(card.ord) -def allSounds(text) -> List: # type: ignore +def allSounds(text) -> list: # type: ignore print("allSounds() deprecated") return aqt.mw.col.media._extract_filenames(text) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index b45c82457..6769a8686 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -13,19 +13,7 @@ import zipfile from argparse import Namespace from concurrent.futures import Future from threading import Thread -from typing import ( - Any, - Callable, - Dict, - List, - Literal, - Optional, - Sequence, - TextIO, - Tuple, - TypeVar, - cast, -) +from typing import Any, Literal, Sequence, TextIO, TypeVar, cast import anki import aqt @@ -105,13 +93,13 @@ class AnkiQt(QMainWindow): profileManager: ProfileManagerType, backend: _RustBackend, opts: Namespace, - args: List[Any], + args: list[Any], ) -> None: QMainWindow.__init__(self) self.backend = backend self.state: MainWindowState = "startup" self.opts = opts - self.col: Optional[Collection] = None + self.col: Collection | None = None self.taskman = TaskManager(self) self.media_syncer = MediaSyncer(self) aqt.mw = self @@ -230,7 +218,7 @@ class AnkiQt(QMainWindow): self.pm.meta["firstRun"] = False self.pm.save() - self.pendingImport: Optional[str] = None + self.pendingImport: str | None = None self.restoringBackup = False # profile not provided on command line? if not self.pm.name: @@ -380,7 +368,7 @@ class AnkiQt(QMainWindow): self.progress.start() profiles = self.pm.profiles() - def downgrade() -> List[str]: + def downgrade() -> list[str]: return self.pm.downgrade(profiles) def on_done(future: Future) -> None: @@ -399,7 +387,7 @@ class AnkiQt(QMainWindow): self.taskman.run_in_background(downgrade, on_done) - def loadProfile(self, onsuccess: Optional[Callable] = None) -> None: + def loadProfile(self, onsuccess: Callable | None = None) -> None: if not self.loadCollection(): return @@ -678,7 +666,7 @@ class AnkiQt(QMainWindow): self.maybe_check_for_addon_updates() self.deckBrowser.show() - def _selectedDeck(self) -> Optional[DeckDict]: + def _selectedDeck(self) -> DeckDict | None: did = self.col.decks.selected() if not self.col.decks.name_if_exists(did): showInfo(tr.qt_misc_please_select_a_deck()) @@ -721,7 +709,7 @@ class AnkiQt(QMainWindow): gui_hooks.operation_did_execute(op, None) def on_operation_did_execute( - self, changes: OpChanges, handler: Optional[object] + self, changes: OpChanges, handler: object | None ) -> None: "Notify current screen of changes." focused = current_window() == self @@ -741,7 +729,7 @@ class AnkiQt(QMainWindow): self.toolbar.update_sync_status() def on_focus_did_change( - self, new_focus: Optional[QWidget], _old: Optional[QWidget] + self, new_focus: QWidget | None, _old: QWidget | None ) -> None: "If main window has received focus, ensure current UI state is updated." if new_focus and new_focus.window() == self: @@ -799,7 +787,7 @@ class AnkiQt(QMainWindow): self, link: str, name: str, - key: Optional[str] = None, + key: str | None = None, class_: str = "", id: str = "", extra: str = "", @@ -810,8 +798,8 @@ class AnkiQt(QMainWindow): else: key = "" return """ -""" % ( +""".format( id, class_, link, @@ -878,7 +866,7 @@ title="%s" %s>%s""" % ( self.errorHandler = aqt.errors.ErrorHandler(self) - def setupAddons(self, args: Optional[List]) -> None: + def setupAddons(self, args: list | None) -> None: import aqt.addons self.addonManager = aqt.addons.AddonManager(self) @@ -903,7 +891,7 @@ title="%s" %s>%s""" % ( ) self.pm.set_last_addon_update_check(intTime()) - def on_updates_installed(self, log: List[DownloadLogEntry]) -> None: + def on_updates_installed(self, log: list[DownloadLogEntry]) -> None: if log: show_log_to_user(self, log) @@ -1024,11 +1012,11 @@ title="%s" %s>%s""" % ( ("y", self.on_sync_button_clicked), ] self.applyShortcuts(globalShortcuts) - self.stateShortcuts: List[QShortcut] = [] + self.stateShortcuts: list[QShortcut] = [] def applyShortcuts( - self, shortcuts: Sequence[Tuple[str, Callable]] - ) -> List[QShortcut]: + self, shortcuts: Sequence[tuple[str, Callable]] + ) -> list[QShortcut]: qshortcuts = [] for key, fn in shortcuts: scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore @@ -1036,7 +1024,7 @@ title="%s" %s>%s""" % ( qshortcuts.append(scut) return qshortcuts - def setStateShortcuts(self, shortcuts: List[Tuple[str, Callable]]) -> None: + def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None: gui_hooks.state_shortcuts_will_change(self.state, shortcuts) # legacy hook runHook(f"{self.state}StateShortcuts", shortcuts) @@ -1154,7 +1142,7 @@ title="%s" %s>%s""" % ( # legacy - def onDeckConf(self, deck: Optional[DeckDict] = None) -> None: + def onDeckConf(self, deck: DeckDict | None = None) -> None: pass # Importing & exporting @@ -1175,7 +1163,7 @@ title="%s" %s>%s""" % ( aqt.importing.onImport(self) - def onExport(self, did: Optional[DeckId] = None) -> None: + def onExport(self, did: DeckId | None = None) -> None: import aqt.exporting aqt.exporting.ExportDialog(self, did=did) @@ -1246,7 +1234,7 @@ title="%s" %s>%s""" % ( if self.pm.meta.get("suppressUpdate", None) != ver: aqt.update.askAndUpdate(self, ver) - def newMsg(self, data: Dict) -> None: + def newMsg(self, data: dict) -> None: aqt.update.showMessages(self, data) def clockIsOff(self, diff: int) -> None: @@ -1299,7 +1287,7 @@ title="%s" %s>%s""" % ( gui_hooks.operation_did_execute.append(self.on_operation_did_execute) gui_hooks.focus_did_change.append(self.on_focus_did_change) - self._activeWindowOnPlay: Optional[QWidget] = None + self._activeWindowOnPlay: QWidget | None = None def onOdueInvalid(self) -> None: showWarning(tr.qt_misc_invalid_property_found_on_card_please()) @@ -1486,12 +1474,12 @@ title="%s" %s>%s""" % ( c._render_output = None pprint.pprint(c.__dict__) - def _debugCard(self) -> Optional[anki.cards.Card]: + def _debugCard(self) -> anki.cards.Card | None: card = self.reviewer.card self._card_repr(card) return card - def _debugBrowserCard(self) -> Optional[anki.cards.Card]: + def _debugBrowserCard(self) -> anki.cards.Card | None: card = aqt.dialogs._dialogs["Browser"][1].card self._card_repr(card) return card @@ -1564,7 +1552,7 @@ title="%s" %s>%s""" % ( _dummy1 = windll _dummy2 = wintypes - def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None: + def maybeHideAccelerators(self, tgt: Any | None = None) -> None: if not self.hideMenuAccels: return tgt = tgt or self diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index c3ed3de36..69c766cbd 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -6,7 +6,7 @@ from __future__ import annotations import itertools import time from concurrent.futures import Future -from typing import Iterable, List, Optional, Sequence, TypeVar +from typing import Iterable, Sequence, TypeVar import aqt from anki.collection import SearchNode @@ -26,7 +26,7 @@ from aqt.utils import ( T = TypeVar("T") -def chunked_list(l: Iterable[T], n: int) -> Iterable[List[T]]: +def chunked_list(l: Iterable[T], n: int) -> Iterable[list[T]]: l = iter(l) while True: res = list(itertools.islice(l, n)) @@ -41,11 +41,11 @@ def check_media_db(mw: aqt.AnkiQt) -> None: class MediaChecker: - progress_dialog: Optional[aqt.progress.ProgressDialog] + progress_dialog: aqt.progress.ProgressDialog | None def __init__(self, mw: aqt.AnkiQt) -> None: self.mw = mw - self._progress_timer: Optional[QTimer] = None + self._progress_timer: QTimer | None = None def check(self) -> None: self.progress_dialog = self.mw.progress.start() diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 09e094fce..fa3cfe26b 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -11,7 +11,6 @@ import threading import time import traceback from http import HTTPStatus -from typing import Tuple import flask import flask_cors # type: ignore @@ -179,7 +178,7 @@ def allroutes(pathin: str) -> Response: ) -def _redirectWebExports(path: str) -> Tuple[str, str]: +def _redirectWebExports(path: str) -> tuple[str, str]: # catch /_anki references and rewrite them to web export folder targetPath = "_anki/" if path.startswith(targetPath): diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 72457be06..8b8e4a92d 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -6,7 +6,7 @@ from __future__ import annotations import time from concurrent.futures import Future from dataclasses import dataclass -from typing import Any, Callable, List, Optional, Union +from typing import Any, Callable, Union import aqt from anki.collection import Progress @@ -30,8 +30,8 @@ class MediaSyncer: def __init__(self, mw: aqt.main.AnkiQt) -> None: self.mw = mw self._syncing: bool = False - self._log: List[LogEntryWithTime] = [] - self._progress_timer: Optional[QTimer] = None + self._log: list[LogEntryWithTime] = [] + self._progress_timer: QTimer | None = None gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop) def _on_progress(self) -> None: @@ -98,7 +98,7 @@ class MediaSyncer: self._log_and_notify(tr.sync_media_failed()) showWarning(str(exc)) - def entries(self) -> List[LogEntryWithTime]: + def entries(self) -> list[LogEntryWithTime]: return self._log def abort(self) -> None: @@ -125,7 +125,7 @@ class MediaSyncer: diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True) diag.show() - timer: Optional[QTimer] = None + timer: QTimer | None = None def check_finished() -> None: if not self.is_syncing(): diff --git a/qt/aqt/modelchooser.py b/qt/aqt/modelchooser.py index 0ba83e022..06e3e4a92 100644 --- a/qt/aqt/modelchooser.py +++ b/qt/aqt/modelchooser.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import List, Optional + +from typing import Optional from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -75,7 +76,7 @@ class ModelChooser(QHBoxLayout): # edit button edit = QPushButton(tr.qt_misc_manage(), clicked=self.onEdit) # type: ignore - def nameFunc() -> List[str]: + def nameFunc() -> list[str]: return [nt.name for nt in self.deck.models.all_names_and_ids()] ret = StudyDeck( diff --git a/qt/aqt/models.py b/qt/aqt/models.py index b65be9d34..a87c7e04f 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -1,9 +1,11 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + from concurrent.futures import Future from operator import itemgetter -from typing import Any, List, Optional, Sequence +from typing import Any, Optional, Sequence import aqt.clayout from anki import stdmodels @@ -239,7 +241,7 @@ class AddModel(QDialog): self.dialog.setupUi(self) disable_help_button(self) # standard models - self.notetypes: List[ + self.notetypes: list[ Union[NotetypeDict, Callable[[Collection], NotetypeDict]] ] = [] for (name, func) in stdmodels.get_stock_notetypes(self.col): diff --git a/qt/aqt/mpv.py b/qt/aqt/mpv.py index ae996a939..188a1be75 100644 --- a/qt/aqt/mpv.py +++ b/qt/aqt/mpv.py @@ -1,4 +1,3 @@ -# coding: utf-8 # ------------------------------------------------------------------------------ # # mpv.py - Control mpv from Python using JSON IPC @@ -41,7 +40,7 @@ from distutils.spawn import ( # pylint: disable=import-error,no-name-in-module find_executable, ) from queue import Empty, Full, Queue -from typing import Dict, Optional +from typing import Optional from anki.utils import isWin @@ -80,7 +79,7 @@ class MPVBase: """ executable = find_executable("mpv") - popenEnv: Optional[Dict[str, str]] = None + popenEnv: Optional[dict[str, str]] = None default_argv = [ "--idle", diff --git a/qt/aqt/notetypechooser.py b/qt/aqt/notetypechooser.py index 406534e9c..fceb1ca0d 100644 --- a/qt/aqt/notetypechooser.py +++ b/qt/aqt/notetypechooser.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import List, Optional + +from typing import Optional from anki.models import NotetypeId from aqt import AnkiQt, gui_hooks @@ -98,7 +99,7 @@ class NotetypeChooser(QHBoxLayout): edit = QPushButton(tr.qt_misc_manage()) qconnect(edit.clicked, self.onEdit) - def nameFunc() -> List[str]: + def nameFunc() -> list[str]: return sorted(self.mw.col.models.all_names()) ret = StudyDeck( diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index 01e6d17e8..c6019abfd 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from concurrent.futures._base import Future -from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union +from typing import Any, Callable, Generic, Protocol, TypeVar, Union import aqt from anki.collection import ( @@ -61,26 +61,26 @@ class CollectionOp(Generic[ResultWithChanges]): passed to `failure` if it is provided. """ - _success: Optional[Callable[[ResultWithChanges], Any]] = None - _failure: Optional[Callable[[Exception], Any]] = None + _success: Callable[[ResultWithChanges], Any] | None = None + _failure: Callable[[Exception], Any] | None = None def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]): self._parent = parent self._op = op def success( - self, success: Optional[Callable[[ResultWithChanges], Any]] + self, success: Callable[[ResultWithChanges], Any] | None ) -> CollectionOp[ResultWithChanges]: self._success = success return self def failure( - self, failure: Optional[Callable[[Exception], Any]] + self, failure: Callable[[Exception], Any] | None ) -> CollectionOp[ResultWithChanges]: self._failure = failure return self - def run_in_background(self, *, initiator: Optional[object] = None) -> None: + def run_in_background(self, *, initiator: object | None = None) -> None: from aqt import mw assert mw @@ -121,7 +121,7 @@ class CollectionOp(Generic[ResultWithChanges]): def _fire_change_hooks_after_op_performed( self, result: ResultWithChanges, - handler: Optional[object], + handler: object | None, ) -> None: from aqt import mw @@ -158,8 +158,8 @@ class QueryOp(Generic[T]): passed to `failure` if it is provided. """ - _failure: Optional[Callable[[Exception], Any]] = None - _progress: Union[bool, str] = False + _failure: Callable[[Exception], Any] | None = None + _progress: bool | str = False def __init__( self, @@ -172,11 +172,11 @@ class QueryOp(Generic[T]): self._op = op self._success = success - def failure(self, failure: Optional[Callable[[Exception], Any]]) -> QueryOp[T]: + def failure(self, failure: Callable[[Exception], Any] | None) -> QueryOp[T]: self._failure = failure return self - def with_progress(self, label: Optional[str] = None) -> QueryOp[T]: + def with_progress(self, label: str | None = None) -> QueryOp[T]: self._progress = label or True return self @@ -190,7 +190,7 @@ class QueryOp(Generic[T]): def wrapped_op() -> T: assert mw if self._progress: - label: Optional[str] + label: str | None if isinstance(self._progress, str): label = self._progress else: diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index 9edc99c9c..60e6f0be3 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs @@ -50,7 +50,7 @@ def add_deck_dialog( *, parent: QWidget, default_text: str = "", -) -> Optional[CollectionOp[OpChangesWithId]]: +) -> CollectionOp[OpChangesWithId] | None: if name := getOnlyText( tr.decks_new_deck_name(), default=default_text, parent=parent ).strip(): diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index de5be2f6b..c76971a4a 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence from anki.collection import OpChanges, OpChangesWithCount from anki.decks import DeckId @@ -43,7 +43,7 @@ def find_and_replace( search: str, replacement: str, regex: bool, - field_name: Optional[str], + field_name: str | None, match_case: bool, ) -> CollectionOp[OpChangesWithCount]: return CollectionOp( diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index 3e8eb6c89..7f397f6c8 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import Sequence import aqt from anki.cards import CardId @@ -29,8 +29,8 @@ def set_due_date_dialog( *, parent: QWidget, card_ids: Sequence[CardId], - config_key: Optional[Config.String.V], -) -> Optional[CollectionOp[OpChanges]]: + config_key: Config.String.V | None, +) -> CollectionOp[OpChanges] | None: assert aqt.mw if not card_ids: return None @@ -77,7 +77,7 @@ def forget_cards( def reposition_new_cards_dialog( *, parent: QWidget, card_ids: Sequence[CardId] -) -> Optional[CollectionOp[OpChangesWithCount]]: +) -> CollectionOp[OpChangesWithCount] | None: from aqt import mw assert mw diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 43210ad73..b7587af0b 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable import aqt from anki.collection import OpChanges @@ -72,7 +72,7 @@ class Overview: self.refresh() def op_executed( - self, changes: OpChanges, handler: Optional[object], focused: bool + self, changes: OpChanges, handler: object | None, focused: bool ) -> bool: if changes.study_queues: self._refresh_needed = True @@ -115,7 +115,7 @@ class Overview: openLink(url) return False - def _shortcutKeys(self) -> List[Tuple[str, Callable]]: + def _shortcutKeys(self) -> list[tuple[str, Callable]]: return [ ("o", lambda: display_options_for_deck(self.mw.col.decks.current())), ("r", self.rebuild_current_filtered_deck), @@ -202,7 +202,7 @@ class Overview: def _show_finished_screen(self) -> None: self.web.load_ts_page("congrats") - def _desc(self, deck: Dict[str, Any]) -> str: + def _desc(self, deck: dict[str, Any]) -> str: if deck["dyn"]: desc = tr.studying_this_is_a_special_deck_for() desc += f" {tr.studying_cards_will_be_automatically_returned_to()}" @@ -219,19 +219,19 @@ class Overview: dyn = "" return f'
{desc}
' - def _table(self) -> Optional[str]: + def _table(self) -> str | None: counts = list(self.mw.col.sched.counts()) but = self.mw.button return """
- - - + + +
%s:%s
%s:%s
%s:%s
{}:{}
{}:{}
{}:{}
-%s
""" % ( +{}""".format( tr.actions_new(), counts[0], tr.scheduling_learning(), diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 27a692c4e..0b22a983f 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -9,7 +9,7 @@ import random import shutil import traceback from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any from send2trash import send2trash @@ -62,7 +62,7 @@ class VideoDriver(Enum): return VideoDriver.Software @staticmethod - def all_for_platform() -> List[VideoDriver]: + def all_for_platform() -> list[VideoDriver]: all = [VideoDriver.OpenGL] if isWin: all.append(VideoDriver.ANGLE) @@ -81,7 +81,7 @@ metaConf = dict( defaultLang=None, ) -profileConf: Dict[str, Any] = dict( +profileConf: dict[str, Any] = dict( # profile mainWindowGeom=None, mainWindowState=None, @@ -118,12 +118,12 @@ class AnkiRestart(SystemExit): class ProfileManager: - def __init__(self, base: Optional[str] = None) -> None: # + def __init__(self, base: str | None = None) -> None: # ## Settings which should be forgotten each Anki restart - self.session: Dict[str, Any] = {} - self.name: Optional[str] = None - self.db: Optional[DB] = None - self.profile: Optional[Dict] = None + self.session: dict[str, Any] = {} + self.name: str | None = None + self.db: DB | None = None + self.profile: dict | None = None # instantiate base folder self.base: str self._setBaseFolder(base) @@ -245,8 +245,8 @@ class ProfileManager: # Profile load/save ###################################################################### - def profiles(self) -> List: - def names() -> List: + def profiles(self) -> list: + def names() -> list: return self.db.list("select name from profiles where name != '_global'") n = names() @@ -393,7 +393,7 @@ class ProfileManager: # Downgrade ###################################################################### - def downgrade(self, profiles: List[str]) -> List[str]: + def downgrade(self, profiles: list[str]) -> list[str]: "Downgrade all profiles. Return a list of profiles that couldn't be opened." problem_profiles = [] for name in profiles: @@ -420,7 +420,7 @@ class ProfileManager: os.makedirs(path) return path - def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None: + def _setBaseFolder(self, cmdlineBase: str | None) -> None: if cmdlineBase: self.base = os.path.abspath(cmdlineBase) elif os.environ.get("ANKI_BASE"): @@ -612,13 +612,13 @@ create table if not exists profiles # Profile-specific ###################################################################### - def set_sync_key(self, val: Optional[str]) -> None: + def set_sync_key(self, val: str | None) -> None: self.profile["syncKey"] = val - def set_sync_username(self, val: Optional[str]) -> None: + def set_sync_username(self, val: str | None) -> None: self.profile["syncUser"] = val - def set_host_number(self, val: Optional[int]) -> None: + def set_host_number(self, val: int | None) -> None: self.profile["hostNum"] = val or 0 def media_syncing_enabled(self) -> bool: @@ -627,7 +627,7 @@ create table if not exists profiles def auto_syncing_enabled(self) -> bool: return self.profile["autoSync"] - def sync_auth(self) -> Optional[SyncAuth]: + def sync_auth(self) -> SyncAuth | None: hkey = self.profile.get("syncKey") if not hkey: return None diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index 1e11d10f3..2d8b8e290 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -3,7 +3,6 @@ from __future__ import annotations import time -from typing import Callable, Optional import aqt.forms from aqt.qt import * @@ -19,9 +18,9 @@ class ProgressManager: self.app = mw.app self.inDB = False self.blockUpdates = False - self._show_timer: Optional[QTimer] = None - self._busy_cursor_timer: Optional[QTimer] = None - self._win: Optional[ProgressDialog] = None + self._show_timer: QTimer | None = None + self._busy_cursor_timer: QTimer | None = None + self._win: ProgressDialog | None = None self._levels = 0 # Safer timers @@ -74,10 +73,10 @@ class ProgressManager: self, max: int = 0, min: int = 0, - label: Optional[str] = None, - parent: Optional[QWidget] = None, + label: str | None = None, + parent: QWidget | None = None, immediate: bool = False, - ) -> Optional[ProgressDialog]: + ) -> ProgressDialog | None: self._levels += 1 if self._levels > 1: return None @@ -112,11 +111,11 @@ class ProgressManager: def update( self, - label: Optional[str] = None, - value: Optional[int] = None, + label: str | None = None, + value: int | None = None, process: bool = True, maybeShow: bool = True, - max: Optional[int] = None, + max: int | None = None, ) -> None: # print self._min, self._counter, self._max, label, time.time() - self._lastTime if not self.mw.inMainThread(): diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index c32aedf60..57613b99f 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -11,7 +11,7 @@ import re import unicodedata as ucd from dataclasses import dataclass from enum import Enum, auto -from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Literal, Match, Sequence, cast from PyQt5.QtCore import Qt @@ -85,7 +85,7 @@ class V3CardInfo: def top_card(self) -> QueuedCards.QueuedCard: return self.queued_cards.cards[0] - def counts(self) -> Tuple[int, List[int]]: + def counts(self) -> tuple[int, list[int]]: "Returns (idx, counts)." counts = [ self.queued_cards.new_count, @@ -117,16 +117,16 @@ class Reviewer: def __init__(self, mw: AnkiQt) -> None: self.mw = mw self.web = mw.web - self.card: Optional[Card] = None - self.cardQueue: List[Card] = [] - self.previous_card: Optional[Card] = None + self.card: Card | None = None + self.cardQueue: list[Card] = [] + self.previous_card: Card | None = None self.hadCardQueue = False - self._answeredIds: List[CardId] = [] - self._recordedAudio: Optional[str] = None + self._answeredIds: list[CardId] = [] + self._recordedAudio: str | None = None self.typeCorrect: str = None # web init happens before this is set - self.state: Optional[str] = None - self._refresh_needed: Optional[RefreshNeeded] = None - self._v3: Optional[V3CardInfo] = None + self.state: str | None = None + self._refresh_needed: RefreshNeeded | None = None + self._v3: V3CardInfo | None = None self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1)) self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -141,7 +141,7 @@ class Reviewer: self.refresh_if_needed() # this is only used by add-ons - def lastCard(self) -> Optional[Card]: + def lastCard(self) -> Card | None: if self._answeredIds: if not self.card or self._answeredIds[-1] != self.card.id: try: @@ -167,7 +167,7 @@ class Reviewer: self._refresh_needed = None def op_executed( - self, changes: OpChanges, handler: Optional[object], focused: bool + self, changes: OpChanges, handler: object | None, focused: bool ) -> bool: if handler is not self: if changes.study_queues: @@ -234,7 +234,7 @@ class Reviewer: self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) self.card.start_timer() - def get_next_states(self) -> Optional[NextStates]: + def get_next_states(self) -> NextStates | None: if v3 := self._v3: return v3.next_states else: @@ -434,7 +434,7 @@ class Reviewer: def _shortcutKeys( self, - ) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]: + ) -> Sequence[Union[tuple[str, Callable], tuple[Qt.Key, Callable]]]: return [ ("e", self.mw.onEditCurrent), (" ", self.onEnterKey), @@ -587,7 +587,7 @@ class Reviewer: # can't pass a string in directly, and can't use re.escape as it # escapes too much s = """ -%s""" % ( +{}""".format( self.typeFont, self.typeSize, res, @@ -620,23 +620,23 @@ class Reviewer: def tokenizeComparison( self, given: str, correct: str - ) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]: + ) -> tuple[list[tuple[bool, str]], list[tuple[bool, str]]]: # compare in NFC form so accents appear correct given = ucd.normalize("NFC", given) correct = ucd.normalize("NFC", correct) s = difflib.SequenceMatcher(None, given, correct, autojunk=False) - givenElems: List[Tuple[bool, str]] = [] - correctElems: List[Tuple[bool, str]] = [] + givenElems: list[tuple[bool, str]] = [] + correctElems: list[tuple[bool, str]] = [] givenPoint = 0 correctPoint = 0 offby = 0 - def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None: + def logBad(old: int, new: int, s: str, array: list[tuple[bool, str]]) -> None: if old != new: array.append((False, s[old:new])) def logGood( - start: int, cnt: int, s: str, array: List[Tuple[bool, str]] + start: int, cnt: int, s: str, array: list[tuple[bool, str]] ) -> None: if cnt: array.append((True, s[start : start + cnt])) @@ -737,8 +737,8 @@ time = %(time)d; def _showAnswerButton(self) -> None: middle = """ -%s
-""" % ( +{}
+""".format( self._remaining(), tr.actions_shortcut_key(val=tr.studying_space()), tr.studying_show_answer(), @@ -763,10 +763,10 @@ time = %(time)d; if not self.mw.col.conf["dueCounts"]: return "" - counts: List[Union[int, str]] + counts: list[Union[int, str]] if v3 := self._v3: idx, counts_ = v3.counts() - counts = cast(List[Union[int, str]], counts_) + counts = cast(list[Union[int, str]], counts_) else: # v1/v2 scheduler if self.hadCardQueue: @@ -790,10 +790,10 @@ time = %(time)d; else: return 2 - def _answerButtonList(self) -> Tuple[Tuple[int, str], ...]: + def _answerButtonList(self) -> tuple[tuple[int, str], ...]: button_count = self.mw.col.sched.answerButtons(self.card) if button_count == 2: - buttons_tuple: Tuple[Tuple[int, str], ...] = ( + buttons_tuple: tuple[tuple[int, str], ...] = ( (1, tr.studying_again()), (2, tr.studying_good()), ) @@ -847,7 +847,7 @@ time = %(time)d; buf += "" return buf - def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str: + def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str: if not self.mw.col.conf["estTimes"]: return "
" if v3_labels: @@ -859,7 +859,7 @@ time = %(time)d; # Leeches ########################################################################## - def onLeech(self, card: Optional[Card] = None) -> None: + def onLeech(self, card: Card | None = None) -> None: # for now s = tr.studying_card_was_a_leech() # v3 scheduler doesn't report this @@ -891,7 +891,7 @@ time = %(time)d; ########################################################################## # note the shortcuts listed here also need to be defined above - def _contextMenu(self) -> List[Any]: + def _contextMenu(self) -> list[Any]: currentFlag = self.card and self.card.user_flag() opts = [ [ diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 9b287b667..f00568fc7 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -15,7 +15,7 @@ import wave from abc import ABC, abstractmethod from concurrent.futures import Future from operator import itemgetter -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast +from typing import TYPE_CHECKING, Any, Callable, cast from markdown import markdown @@ -60,7 +60,7 @@ class Player(ABC): """ @abstractmethod - def rank_for_tag(self, tag: AVTag) -> Optional[int]: + def rank_for_tag(self, tag: AVTag) -> int | None: """How suited this player is to playing tag. AVPlayer will choose the player that returns the highest rank @@ -105,7 +105,7 @@ def is_audio_file(fname: str) -> bool: class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method default_rank = 0 - def rank_for_tag(self, tag: AVTag) -> Optional[int]: + def rank_for_tag(self, tag: AVTag) -> int | None: if isinstance(tag, SoundOrVideoTag): return self.default_rank else: @@ -115,7 +115,7 @@ class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method class SoundPlayer(Player): # pylint: disable=abstract-method default_rank = 0 - def rank_for_tag(self, tag: AVTag) -> Optional[int]: + def rank_for_tag(self, tag: AVTag) -> int | None: if isinstance(tag, SoundOrVideoTag) and is_audio_file(tag.filename): return self.default_rank else: @@ -125,7 +125,7 @@ class SoundPlayer(Player): # pylint: disable=abstract-method class VideoPlayer(Player): # pylint: disable=abstract-method default_rank = 0 - def rank_for_tag(self, tag: AVTag) -> Optional[int]: + def rank_for_tag(self, tag: AVTag) -> int | None: if isinstance(tag, SoundOrVideoTag) and not is_audio_file(tag.filename): return self.default_rank else: @@ -137,16 +137,16 @@ class VideoPlayer(Player): # pylint: disable=abstract-method class AVPlayer: - players: List[Player] = [] + players: list[Player] = [] # when a new batch of audio is played, should the currently playing # audio be stopped? interrupt_current_audio = True def __init__(self) -> None: - self._enqueued: List[AVTag] = [] - self.current_player: Optional[Player] = None + self._enqueued: list[AVTag] = [] + self.current_player: Player | None = None - def play_tags(self, tags: List[AVTag]) -> None: + def play_tags(self, tags: list[AVTag]) -> None: """Clear the existing queue, then start playing provided tags.""" self.clear_queue_and_maybe_interrupt() self._enqueued = tags[:] @@ -185,7 +185,7 @@ class AVPlayer: if self.current_player: self.current_player.stop() - def _pop_next(self) -> Optional[AVTag]: + def _pop_next(self) -> AVTag | None: if not self._enqueued: return None return self._enqueued.pop(0) @@ -212,7 +212,7 @@ class AVPlayer: else: tooltip(f"no players found for {tag}") - def _best_player_for_tag(self, tag: AVTag) -> Optional[Player]: + def _best_player_for_tag(self, tag: AVTag) -> Player | None: ranked = [] for p in self.players: rank = p.rank_for_tag(tag) @@ -234,7 +234,7 @@ av_player = AVPlayer() # return modified command array that points to bundled command, and return # required environment -def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]: +def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]: cmd = cmd[:] env = os.environ.copy() if "LD_LIBRARY_PATH" in env: @@ -275,13 +275,13 @@ def retryWait(proc: subprocess.Popen) -> int: class SimpleProcessPlayer(Player): # pylint: disable=abstract-method "A player that invokes a new process for each tag to play." - args: List[str] = [] - env: Optional[Dict[str, str]] = None + args: list[str] = [] + env: dict[str, str] | None = None def __init__(self, taskman: TaskManager) -> None: self._taskman = taskman self._terminate_flag = False - self._process: Optional[subprocess.Popen] = None + self._process: subprocess.Popen | None = None def play(self, tag: AVTag, on_done: OnDoneCallback) -> None: self._terminate_flag = False @@ -388,7 +388,7 @@ class MpvManager(MPV, SoundOrVideoPlayer): def __init__(self, base_path: str) -> None: mpvPath, self.popenEnv = _packagedCmd(["mpv"]) self.executable = mpvPath[0] - self._on_done: Optional[OnDoneCallback] = None + self._on_done: OnDoneCallback | None = None self.default_argv += [f"--config-dir={base_path}"] super().__init__(window_id=None, debug=False) @@ -635,7 +635,7 @@ except: PYAU_CHANNELS = 1 -PYAU_INPUT_INDEX: Optional[int] = None +PYAU_INPUT_INDEX: int | None = None class PyAudioThreadedRecorder(threading.Thread): @@ -839,7 +839,7 @@ def playFromText(text: Any) -> None: # legacy globals _player = play _queueEraser = clearAudioQueue -mpvManager: Optional["MpvManager"] = None +mpvManager: MpvManager | None = None # add everything from this module into anki.sound for backwards compat _exports = [i for i in locals().items() if not i[0].startswith("__")] diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 199106e94..f1a2953c9 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -1,7 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import List, Optional +from typing import Optional import aqt from anki.collection import OpChangesWithId @@ -34,7 +34,7 @@ class StudyDeck(QDialog): cancel: bool = True, parent: Optional[QWidget] = None, dyn: bool = False, - buttons: Optional[List[Union[str, QPushButton]]] = None, + buttons: Optional[list[Union[str, QPushButton]]] = None, geomKey: str = "default", ) -> None: QDialog.__init__(self, parent or mw) diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index cd594911d..bc97b44fd 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Tuple from aqt import colors from aqt.qt import * @@ -22,8 +20,8 @@ class Switch(QAbstractButton): radius: int = 10, left_label: str = "", right_label: str = "", - left_color: Tuple[str, str] = colors.FLAG4_BG, - right_color: Tuple[str, str] = colors.FLAG3_BG, + left_color: tuple[str, str] = colors.FLAG4_BG, + right_color: tuple[str, str] = colors.FLAG3_BG, parent: QWidget = None, ) -> None: super().__init__(parent=parent) diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index c1e3b4573..80eb550c1 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -6,7 +6,7 @@ from __future__ import annotations import enum import os from concurrent.futures import Future -from typing import Callable, Tuple +from typing import Callable import aqt from anki.errors import Interrupted, SyncError, SyncErrorKind @@ -288,7 +288,7 @@ def ask_user_to_decide_direction() -> FullSyncChoice: def get_id_and_pass_from_user( mw: aqt.main.AnkiQt, username: str = "", password: str = "" -) -> Tuple[str, str]: +) -> tuple[str, str]: diag = QDialog(mw) diag.setWindowTitle("Anki") disable_help_button(diag) diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index df01f3acf..b616b1737 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from typing import Iterable, List, Optional, Union +from typing import Iterable from anki.collection import Collection from aqt import gui_hooks @@ -12,14 +12,14 @@ from aqt.qt import * class TagEdit(QLineEdit): - _completer: Union[QCompleter, TagCompleter] + _completer: QCompleter | TagCompleter lostFocus = pyqtSignal() # 0 = tags, 1 = decks def __init__(self, parent: QWidget, type: int = 0) -> None: QLineEdit.__init__(self, parent) - self.col: Optional[Collection] = None + self.col: Collection | None = None self.model = QStringListModel() self.type = type if type == 0: @@ -112,11 +112,11 @@ class TagCompleter(QCompleter): edit: TagEdit, ) -> None: QCompleter.__init__(self, model, parent) - self.tags: List[str] = [] + self.tags: list[str] = [] self.edit = edit - self.cursor: Optional[int] = None + self.cursor: int | None = None - def splitPath(self, tags: str) -> List[str]: + def splitPath(self, tags: str) -> list[str]: stripped_tags = tags.strip() stripped_tags = re.sub(" +", " ", stripped_tags) self.tags = self.edit.col.tags.split(stripped_tags) diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index d0d4706a8..f380f7819 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html -from typing import List, Optional + +from typing import Optional import aqt from anki.lang import with_collapsed_whitespace @@ -14,7 +15,7 @@ class TagLimit(QDialog): def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None: QDialog.__init__(self, parent, Qt.Window) self.tags: str = "" - self.tags_list: List[str] = [] + self.tags_list: list[str] = [] self.mw = mw self.parent_: Optional[CustomStudy] = parent self.deck = self.parent_.deck diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index 18c1bdbfc..3c62679ab 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -12,7 +12,7 @@ from __future__ import annotations from concurrent.futures import Future from concurrent.futures.thread import ThreadPoolExecutor from threading import Lock -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from PyQt5.QtCore import QObject, pyqtSignal @@ -29,7 +29,7 @@ class TaskManager(QObject): QObject.__init__(self) self.mw = mw.weakref() self._executor = ThreadPoolExecutor() - self._closures: List[Closure] = [] + self._closures: list[Closure] = [] self._closures_lock = Lock() qconnect(self._closures_pending, self._on_closures_pending) @@ -42,8 +42,8 @@ class TaskManager(QObject): def run_in_background( self, task: Callable, - on_done: Optional[Callable[[Future], None]] = None, - args: Optional[Dict[str, Any]] = None, + on_done: Callable[[Future], None] | None = None, + args: dict[str, Any] | None = None, ) -> Future: """Use QueryOp()/CollectionOp() in new code. @@ -76,9 +76,9 @@ class TaskManager(QObject): def with_progress( self, task: Callable, - on_done: Optional[Callable[[Future], None]] = None, - parent: Optional[QWidget] = None, - label: Optional[str] = None, + on_done: Callable[[Future], None] | None = None, + parent: QWidget | None = None, + label: str | None = None, immediate: bool = False, ) -> None: "Use QueryOp()/CollectionOp() in new code." diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 75a10e96f..b3faad78e 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -5,7 +5,6 @@ from __future__ import annotations import platform from dataclasses import dataclass -from typing import Dict, Optional, Tuple, Union from anki.utils import isMac from aqt import QApplication, colors, gui_hooks, isWin @@ -17,7 +16,7 @@ from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt class ColoredIcon: path: str # (day, night) - color: Tuple[str, str] + color: tuple[str, str] def current_color(self, night_mode: bool) -> str: if night_mode: @@ -25,17 +24,17 @@ class ColoredIcon: else: return self.color[0] - def with_color(self, color: Tuple[str, str]) -> ColoredIcon: + def with_color(self, color: tuple[str, str]) -> ColoredIcon: return ColoredIcon(path=self.path, color=color) class ThemeManager: _night_mode_preference = False - _icon_cache_light: Dict[str, QIcon] = {} - _icon_cache_dark: Dict[str, QIcon] = {} + _icon_cache_light: dict[str, QIcon] = {} + _icon_cache_dark: dict[str, QIcon] = {} _icon_size = 128 - _dark_mode_available: Optional[bool] = None - default_palette: Optional[QPalette] = None + _dark_mode_available: bool | None = None + default_palette: QPalette | None = None # Qt applies a gradient to the buttons in dark mode # from about #505050 to #606060. @@ -65,7 +64,7 @@ class ThemeManager: night_mode = property(get_night_mode, set_night_mode) - def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon: + def icon_from_resources(self, path: str | ColoredIcon) -> QIcon: "Fetch icon from Qt resources, and invert if in night mode." if self.night_mode: cache = self._icon_cache_light @@ -101,7 +100,7 @@ class ThemeManager: return cache.setdefault(path, icon) - def body_class(self, night_mode: Optional[bool] = None) -> str: + def body_class(self, night_mode: bool | None = None) -> str: "Returns space-separated class list for platform/theme." classes = [] if isWin: @@ -120,17 +119,17 @@ class ThemeManager: return " ".join(classes) def body_classes_for_card_ord( - self, card_ord: int, night_mode: Optional[bool] = None + self, card_ord: int, night_mode: bool | None = None ) -> str: "Returns body classes used when showing a card." return f"card card{card_ord+1} {self.body_class(night_mode)}" - def color(self, colors: Tuple[str, str]) -> str: + def color(self, colors: tuple[str, str]) -> str: """Given day/night colors, return the correct one for the current theme.""" idx = 1 if self.night_mode else 0 return colors[idx] - def qcolor(self, colors: Tuple[str, str]) -> QColor: + def qcolor(self, colors: tuple[str, str]) -> QColor: return QColor(self.color(colors)) def apply_style(self, app: QApplication) -> None: @@ -166,27 +165,27 @@ QToolTip { if not self.macos_dark_mode(): buf += """ -QScrollBar { background-color: %s; } -QScrollBar::handle { background-color: %s; border-radius: 5px; } +QScrollBar {{ background-color: {}; }} +QScrollBar::handle {{ background-color: {}; border-radius: 5px; }} -QScrollBar:horizontal { height: 12px; } -QScrollBar::handle:horizontal { min-width: 50px; } +QScrollBar:horizontal {{ height: 12px; }} +QScrollBar::handle:horizontal {{ min-width: 50px; }} -QScrollBar:vertical { width: 12px; } -QScrollBar::handle:vertical { min-height: 50px; } +QScrollBar:vertical {{ width: 12px; }} +QScrollBar::handle:vertical {{ min-height: 50px; }} -QScrollBar::add-line { +QScrollBar::add-line {{ border: none; background: none; -} +}} -QScrollBar::sub-line { +QScrollBar::sub-line {{ border: none; background: none; -} +}} -QTabWidget { background-color: %s; } -""" % ( +QTabWidget {{ background-color: {}; }} +""".format( self.color(colors.WINDOW_BG), # fushion-button-hover-bg "#656565", diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index a4b06a104..b12ab94cb 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Any import aqt from anki.sync import SyncStatus @@ -29,7 +29,7 @@ class Toolbar: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: self.mw = mw self.web = web - self.link_handlers: Dict[str, Callable] = { + self.link_handlers: dict[str, Callable] = { "study": self._studyLinkHandler, } self.web.setFixedHeight(30) @@ -38,8 +38,8 @@ class Toolbar: def draw( self, buf: str = "", - web_context: Optional[Any] = None, - link_handler: Optional[Callable[[str], Any]] = None, + web_context: Any | None = None, + link_handler: Callable[[str], Any] | None = None, ) -> None: web_context = web_context or TopToolbar(self) link_handler = link_handler or self._linkHandler @@ -65,8 +65,8 @@ class Toolbar: cmd: str, label: str, func: Callable, - tip: Optional[str] = None, - id: Optional[str] = None, + tip: str | None = None, + id: str | None = None, ) -> str: """Generates HTML link element and registers link handler @@ -218,8 +218,8 @@ class BottomBar(Toolbar): def draw( self, buf: str = "", - web_context: Optional[Any] = None, - link_handler: Optional[Callable[[str], Any]] = None, + web_context: Any | None = None, + link_handler: Callable[[str], Any] | None = None, ) -> None: # note: some screens may override this web_context = web_context or BottomToolbar(self) diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index fb8e52035..4a49c4f4d 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -36,7 +36,7 @@ import threading from concurrent.futures import Future from dataclasses import dataclass from operator import attrgetter -from typing import Any, List, Optional, cast +from typing import Any, cast import anki from anki import hooks @@ -61,17 +61,17 @@ class TTSVoiceMatch: class TTSPlayer: default_rank = 0 - _available_voices: Optional[List[TTSVoice]] = None + _available_voices: list[TTSVoice] | None = None - def get_available_voices(self) -> List[TTSVoice]: + def get_available_voices(self) -> list[TTSVoice]: return [] - def voices(self) -> List[TTSVoice]: + def voices(self) -> list[TTSVoice]: if self._available_voices is None: self._available_voices = self.get_available_voices() return self._available_voices - def voice_for_tag(self, tag: TTSTag) -> Optional[TTSVoiceMatch]: + def voice_for_tag(self, tag: TTSTag) -> TTSVoiceMatch | None: avail_voices = self.voices() rank = self.default_rank @@ -103,7 +103,7 @@ class TTSPlayer: class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer): # mypy gets confused if rank_for_tag is defined in TTSPlayer - def rank_for_tag(self, tag: AVTag) -> Optional[int]: + def rank_for_tag(self, tag: AVTag) -> int | None: if not isinstance(tag, TTSTag): return None @@ -118,10 +118,10 @@ class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer): ########################################################################## -def all_tts_voices() -> List[TTSVoice]: +def all_tts_voices() -> list[TTSVoice]: from aqt.sound import av_player - all_voices: List[TTSVoice] = [] + all_voices: list[TTSVoice] = [] for p in av_player.players: getter = getattr(p, "voices", None) if not getter: @@ -185,7 +185,7 @@ class MacTTSPlayer(TTSProcessPlayer): self._process.stdin.close() self._wait_for_termination(tag) - def get_available_voices(self) -> List[TTSVoice]: + def get_available_voices(self) -> list[TTSVoice]: cmd = subprocess.run( ["say", "-v", "?"], capture_output=True, check=True, encoding="utf8" ) @@ -197,7 +197,7 @@ class MacTTSPlayer(TTSProcessPlayer): voices.append(voice) return voices - def _parse_voice_line(self, line: str) -> Optional[TTSVoice]: + def _parse_voice_line(self, line: str) -> TTSVoice | None: m = self.VOICE_HELP_LINE_RE.match(line) if not m: return None @@ -484,7 +484,7 @@ if isWin: except: speaker = None - def get_available_voices(self) -> List[TTSVoice]: + def get_available_voices(self) -> list[TTSVoice]: if self.speaker is None: return [] return list(map(self._voice_to_object, self.speaker.GetVoices())) @@ -533,7 +533,7 @@ if isWin: id: Any class WindowsRTTTSFilePlayer(TTSProcessPlayer): - voice_list: List[Any] = [] + voice_list: list[Any] = [] tmppath = os.path.join(tmpdir(), "tts.wav") def import_voices(self) -> None: @@ -545,7 +545,7 @@ if isWin: print("winrt tts voices unavailable:", e) self.voice_list = [] - def get_available_voices(self) -> List[TTSVoice]: + def get_available_voices(self) -> list[TTSVoice]: t = threading.Thread(target=self.import_voices) t.start() t.join() diff --git a/qt/aqt/update.py b/qt/aqt/update.py index 231660259..d0f5ee144 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import time -from typing import Any, Dict +from typing import Any import requests @@ -24,7 +24,7 @@ class LatestVersionFinder(QThread): self.main = main self.config = main.pm.meta - def _data(self) -> Dict[str, Any]: + def _data(self) -> dict[str, Any]: return { "ver": versionWithBuild(), "os": platDesc(), @@ -73,6 +73,6 @@ def askAndUpdate(mw: aqt.AnkiQt, ver: str) -> None: openLink(aqt.appWebsite) -def showMessages(mw: aqt.AnkiQt, data: Dict) -> None: +def showMessages(mw: aqt.AnkiQt, data: dict) -> None: showText(data["msg"], parent=mw, type="html") mw.pm.meta["lastMsg"] = data["msgId"] diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 99c486b2b..b120ae17f 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -7,18 +7,7 @@ import re import subprocess import sys from functools import wraps -from typing import ( - TYPE_CHECKING, - Any, - Callable, - List, - Literal, - Optional, - Sequence, - Tuple, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Literal, Sequence, cast from PyQt5.QtWidgets import ( QAction, @@ -79,7 +68,7 @@ def openHelp(section: HelpPageArgument) -> None: openLink(link) -def openLink(link: Union[str, QUrl]) -> None: +def openLink(link: str | QUrl) -> None: tooltip(tr.qt_misc_loading(), period=1000) with noBundledLibs(): QDesktopServices.openUrl(QUrl(link)) @@ -87,10 +76,10 @@ def openLink(link: Union[str, QUrl]) -> None: def showWarning( text: str, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, help: HelpPageArgument = "", title: str = "Anki", - textFormat: Optional[TextFormat] = None, + textFormat: TextFormat | None = None, ) -> int: "Show a small warning with an OK button." return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat) @@ -98,10 +87,10 @@ def showWarning( def showCritical( text: str, - parent: Optional[QDialog] = None, + parent: QDialog | None = None, help: str = "", title: str = "Anki", - textFormat: Optional[TextFormat] = None, + textFormat: TextFormat | None = None, ) -> int: "Show a small critical error with an OK button." return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat) @@ -109,12 +98,12 @@ def showCritical( def showInfo( text: str, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, help: HelpPageArgument = "", type: str = "info", title: str = "Anki", - textFormat: Optional[TextFormat] = None, - customBtns: Optional[List[QMessageBox.StandardButton]] = None, + textFormat: TextFormat | None = None, + customBtns: list[QMessageBox.StandardButton] | None = None, ) -> int: "Show a small info window with an OK button." parent_widget: QWidget @@ -157,16 +146,16 @@ def showInfo( def showText( txt: str, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, type: str = "text", run: bool = True, - geomKey: Optional[str] = None, + geomKey: str | None = None, minWidth: int = 500, minHeight: int = 400, title: str = "Anki", copyBtn: bool = False, plain_text_edit: bool = False, -) -> Optional[Tuple[QDialog, QDialogButtonBox]]: +) -> tuple[QDialog, QDialogButtonBox] | None: if not parent: parent = aqt.mw.app.activeWindow() or aqt.mw diag = QDialog(parent) @@ -174,7 +163,7 @@ def showText( disable_help_button(diag) layout = QVBoxLayout(diag) diag.setLayout(layout) - text: Union[QPlainTextEdit, QTextBrowser] + text: QPlainTextEdit | QTextBrowser if plain_text_edit: # used by the importer text = QPlainTextEdit() @@ -228,7 +217,7 @@ def askUser( parent: QWidget = None, help: HelpPageArgument = None, defaultno: bool = False, - msgfunc: Optional[Callable] = None, + msgfunc: Callable | None = None, title: str = "Anki", ) -> bool: "Show a yes/no question. Return true if yes." @@ -256,13 +245,13 @@ class ButtonedDialog(QMessageBox): def __init__( self, text: str, - buttons: List[str], - parent: Optional[QWidget] = None, + buttons: list[str], + parent: QWidget | None = None, help: HelpPageArgument = None, title: str = "Anki", ): QMessageBox.__init__(self, parent) - self._buttons: List[QPushButton] = [] + self._buttons: list[QPushButton] = [] self.setWindowTitle(title) self.help = help self.setIcon(QMessageBox.Warning) @@ -289,8 +278,8 @@ class ButtonedDialog(QMessageBox): def askUserDialog( text: str, - buttons: List[str], - parent: Optional[QWidget] = None, + buttons: list[str], + parent: QWidget | None = None, help: HelpPageArgument = None, title: str = "Anki", ) -> ButtonedDialog: @@ -303,10 +292,10 @@ def askUserDialog( class GetTextDialog(QDialog): def __init__( self, - parent: Optional[QWidget], + parent: QWidget | None, question: str, help: HelpPageArgument = None, - edit: Optional[QLineEdit] = None, + edit: QLineEdit | None = None, default: str = "", title: str = "Anki", minWidth: int = 400, @@ -350,14 +339,14 @@ class GetTextDialog(QDialog): def getText( prompt: str, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, help: HelpPageArgument = None, - edit: Optional[QLineEdit] = None, + edit: QLineEdit | None = None, default: str = "", title: str = "Anki", - geomKey: Optional[str] = None, + geomKey: str | None = None, **kwargs: Any, -) -> Tuple[str, int]: +) -> tuple[str, int]: "Returns (string, succeeded)." if not parent: parent = aqt.mw.app.activeWindow() or aqt.mw @@ -384,7 +373,7 @@ def getOnlyText(*args: Any, **kwargs: Any) -> str: # fixme: these utilities could be combined into a single base class # unused by Anki, but used by add-ons def chooseList( - prompt: str, choices: List[str], startrow: int = 0, parent: Any = None + prompt: str, choices: list[str], startrow: int = 0, parent: Any = None ) -> int: if not parent: parent = aqt.mw.app.activeWindow() @@ -408,7 +397,7 @@ def chooseList( def getTag( parent: QWidget, deck: Collection, question: str, **kwargs: Any -) -> Tuple[str, int]: +) -> tuple[str, int]: from aqt.tagedit import TagEdit te = TagEdit(parent) @@ -433,12 +422,12 @@ def getFile( parent: QWidget, title: str, # single file returned unless multi=True - cb: Optional[Callable[[Union[str, Sequence[str]]], None]], + cb: Callable[[str | Sequence[str]], None] | None, filter: str = "*.*", - dir: Optional[str] = None, - key: Optional[str] = None, + dir: str | None = None, + key: str | None = None, multi: bool = False, # controls whether a single or multiple files is returned -) -> Optional[Union[Sequence[str], str]]: +) -> Sequence[str] | str | None: "Ask the user for a file." assert not dir or not key if not dir: @@ -480,7 +469,7 @@ def getSaveFile( dir_description: str, key: str, ext: str, - fname: Optional[str] = None, + fname: str | None = None, ) -> str: """Ask the user for a file to save. Use DIR_DESCRIPTION as config variable. The file dialog will default to open with FNAME.""" @@ -520,7 +509,7 @@ def saveGeom(widget: QWidget, key: str) -> None: def restoreGeom( - widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False + widget: QWidget, key: str, offset: int | None = None, adjustSize: bool = False ) -> None: key += "Geom" if aqt.mw.pm.profile.get(key): @@ -562,12 +551,12 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: widget.move(x, y) -def saveState(widget: Union[QFileDialog, QMainWindow], key: str) -> None: +def saveState(widget: QFileDialog | QMainWindow, key: str) -> None: key += "State" aqt.mw.pm.profile[key] = widget.saveState() -def restoreState(widget: Union[QFileDialog, QMainWindow], key: str) -> None: +def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None: key += "State" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key]) @@ -614,7 +603,7 @@ def save_combo_index_for_session(widget: QComboBox, key: str) -> None: def restore_combo_index_for_session( - widget: QComboBox, history: List[str], key: str + widget: QComboBox, history: list[str], key: str ) -> None: textKey = f"{key}ComboActiveText" indexKey = f"{key}ComboActiveIndex" @@ -625,7 +614,7 @@ def restore_combo_index_for_session( widget.setCurrentIndex(index) -def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str: +def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str: name += "BoxHistory" text_input = comboBox.lineEdit().text() if text_input in history: @@ -639,7 +628,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> st return text_input -def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]: +def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]: name += "BoxHistory" history = aqt.mw.pm.profile.get(name, []) comboBox.addItems([""] + history) @@ -693,7 +682,7 @@ def downArrow() -> str: return "▾" -def current_window() -> Optional[QWidget]: +def current_window() -> QWidget | None: if widget := QApplication.focusWidget(): return widget.window() else: @@ -703,14 +692,14 @@ def current_window() -> Optional[QWidget]: # Tooltips ###################################################################### -_tooltipTimer: Optional[QTimer] = None -_tooltipLabel: Optional[QLabel] = None +_tooltipTimer: QTimer | None = None +_tooltipLabel: QLabel | None = None def tooltip( msg: str, period: int = 3000, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, x_offset: int = 0, y_offset: int = 100, ) -> None: @@ -785,7 +774,7 @@ class MenuList: print( "MenuList will be removed; please copy it into your add-on's code if you need it." ) - self.children: List[MenuListChild] = [] + self.children: list[MenuListChild] = [] def addItem(self, title: str, func: Callable) -> MenuItem: item = MenuItem(title, func) @@ -800,7 +789,7 @@ class MenuList: self.children.append(submenu) return submenu - def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None: + def addChild(self, child: SubMenu | QAction | MenuList) -> None: self.children.append(child) def renderTo(self, qmenu: QMenu) -> None: @@ -894,7 +883,7 @@ Add-ons, last update check: {} ###################################################################### # adapted from version detection in qutebrowser -def opengl_vendor() -> Optional[str]: +def opengl_vendor() -> str | None: old_context = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 5e5f50e7a..49e53cc01 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -5,7 +5,7 @@ import dataclasses import json import re import sys -from typing import Any, Callable, List, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Optional, Sequence, cast import anki from anki.lang import is_rtl @@ -206,8 +206,8 @@ class WebContent: body: str = "" head: str = "" - css: List[str] = dataclasses.field(default_factory=lambda: []) - js: List[str] = dataclasses.field(default_factory=lambda: []) + css: list[str] = dataclasses.field(default_factory=lambda: []) + js: list[str] = dataclasses.field(default_factory=lambda: []) # Main web view @@ -232,7 +232,7 @@ class AnkiWebView(QWebEngineView): self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd self._domDone = True - self._pendingActions: List[Tuple[str, Sequence[Any]]] = [] + self._pendingActions: list[tuple[str, Sequence[Any]]] = [] self.requiresCol = True self.setPage(self._page) @@ -415,22 +415,22 @@ border-radius:5px; font-family: Helvetica }""" font = f'font-size:14px;font-family:"{family}";' button_style = """ /* Buttons */ -button{ - background-color: %(color_btn)s; - font-family:"%(family)s"; } -button:focus{ border-color: %(color_hl)s } -button:active, button:active:hover { background-color: %(color_hl)s; color: %(color_hl_txt)s;} +button{{ + background-color: {color_btn}; + font-family:"{family}"; }} +button:focus{{ border-color: {color_hl} }} +button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}} /* Input field focus outline */ textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus, -div[contenteditable="true"]:focus { +div[contenteditable="true"]:focus {{ outline: 0 none; - border-color: %(color_hl)s; -}""" % { - "family": family, - "color_btn": color_btn, - "color_hl": color_hl, - "color_hl_txt": color_hl_txt, - } + border-color: {color_hl}; +}}""".format( + family=family, + color_btn=color_btn, + color_hl=color_hl, + color_hl_txt=color_hl_txt, + ) zoom = self.zoomFactor() @@ -454,8 +454,8 @@ html {{ {font} }} def stdHtml( self, body: str, - css: Optional[List[str]] = None, - js: Optional[List[str]] = None, + css: Optional[list[str]] = None, + js: Optional[list[str]] = None, head: str = "", context: Optional[Any] = None, default_css: bool = True, diff --git a/qt/aqt/winpaths.py b/qt/aqt/winpaths.py index ce5530a35..e53a47c06 100644 --- a/qt/aqt/winpaths.py +++ b/qt/aqt/winpaths.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ System File Locations Retrieves common system path names on Windows XP/Vista @@ -15,7 +13,7 @@ __author__ = "Ryan Ginstrom" __description__ = "Retrieves common Windows system paths as Unicode strings" -class PathConstants(object): +class PathConstants: """ Define constants here to avoid dependency on shellcon. Put it in a class to avoid polluting namespace