From b9251290ca33d4118ffe9247cc44a1cb71dca6b1 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 3 Oct 2021 18:59:42 +1000 Subject: [PATCH] run pyupgrade over codebase [python upgrade required] This adds Python 3.9 and 3.10 typing syntax to files that import attributions from __future___. Python 3.9 should be able to cope with the 3.10 syntax, but Python 3.8 will no longer work. On Windows/Mac, install the latest Python 3.9 version from python.org. There are currently no orjson wheels for Python 3.10 on Windows/Mac, which will break the build unless you have Rust installed separately. On Linux, modern distros should have Python 3.9 available already. If you're on an older distro, you'll need to build Python from source first. --- pylib/anki/_backend/__init__.py | 20 ++-- pylib/anki/_backend/genfluent.py | 4 +- pylib/anki/_legacy.py | 8 +- pylib/anki/browser.py | 1 - pylib/anki/buildinfo.py | 3 +- pylib/anki/cards.py | 15 ++- pylib/anki/collection.py | 74 ++++++------- pylib/anki/consts.py | 10 +- pylib/anki/db.py | 10 +- pylib/anki/dbproxy.py | 14 +-- pylib/anki/decks.py | 92 +++++++--------- pylib/anki/exporting.py | 22 ++-- pylib/anki/find.py | 8 +- pylib/anki/hooks.py | 4 +- pylib/anki/httpclient.py | 12 +-- pylib/anki/importing/__init__.py | 4 +- pylib/anki/importing/anki2.py | 18 ++-- pylib/anki/importing/apkg.py | 4 +- pylib/anki/importing/base.py | 4 +- pylib/anki/importing/csvfile.py | 12 ++- pylib/anki/importing/noteimp.py | 46 ++++---- pylib/anki/importing/supermemo_xml.py | 6 +- pylib/anki/lang.py | 5 +- pylib/anki/latex.py | 8 +- pylib/anki/media.py | 20 ++-- pylib/anki/models.py | 42 ++++---- pylib/anki/notes.py | 16 +-- pylib/anki/scheduler/base.py | 14 +-- pylib/anki/scheduler/legacy.py | 10 +- pylib/anki/scheduler/v1.py | 17 ++- pylib/anki/scheduler/v2.py | 52 ++++----- pylib/anki/scheduler/v3.py | 6 +- pylib/anki/sound.py | 6 +- pylib/anki/stats.py | 70 ++++++------ pylib/anki/stdmodels.py | 10 +- pylib/anki/syncserver/__init__.py | 2 +- pylib/anki/tags.py | 22 ++-- pylib/anki/template.py | 26 ++--- pylib/anki/utils.py | 16 +-- pylib/tests/test_models.py | 4 +- pylib/tests/test_schedv2.py | 4 +- pylib/tools/hookslib.py | 8 +- qt/aqt/__init__.py | 12 ++- qt/aqt/addcards.py | 4 +- qt/aqt/addons.py | 150 +++++++++++++------------- qt/aqt/browser/browser.py | 40 +++---- qt/aqt/browser/find_and_replace.py | 6 +- qt/aqt/browser/find_duplicates.py | 8 +- qt/aqt/browser/previewer.py | 22 ++-- qt/aqt/browser/sidebar/item.py | 20 ++-- qt/aqt/browser/sidebar/toolbar.py | 3 +- qt/aqt/browser/sidebar/tree.py | 42 ++++---- qt/aqt/browser/table/__init__.py | 15 ++- qt/aqt/browser/table/model.py | 39 ++++--- qt/aqt/browser/table/state.py | 17 ++- qt/aqt/browser/table/table.py | 29 +++-- qt/aqt/clayout.py | 12 +-- qt/aqt/deckbrowser.py | 12 +-- qt/aqt/deckconf.py | 8 +- qt/aqt/deckoptions.py | 6 +- qt/aqt/editor.py | 54 +++++----- qt/aqt/emptycards.py | 4 +- qt/aqt/exporting.py | 9 +- qt/aqt/fields.py | 2 + qt/aqt/filtered_deck.py | 26 +++-- qt/aqt/flags.py | 8 +- qt/aqt/importing.py | 4 +- qt/aqt/legacy.py | 6 +- qt/aqt/main.py | 62 +++++------ qt/aqt/mediacheck.py | 8 +- qt/aqt/mediasrv.py | 3 +- qt/aqt/mediasync.py | 10 +- qt/aqt/modelchooser.py | 5 +- qt/aqt/models.py | 6 +- qt/aqt/mpv.py | 5 +- qt/aqt/notetypechooser.py | 5 +- qt/aqt/operations/__init__.py | 24 ++--- qt/aqt/operations/deck.py | 4 +- qt/aqt/operations/note.py | 4 +- qt/aqt/operations/scheduling.py | 8 +- qt/aqt/overview.py | 18 ++-- qt/aqt/profiles.py | 32 +++--- qt/aqt/progress.py | 19 ++-- qt/aqt/reviewer.py | 58 +++++----- qt/aqt/sound.py | 36 +++---- qt/aqt/studydeck.py | 4 +- qt/aqt/switch.py | 6 +- qt/aqt/sync.py | 4 +- qt/aqt/tagedit.py | 12 +-- qt/aqt/taglimit.py | 5 +- qt/aqt/taskman.py | 14 +-- qt/aqt/theme.py | 47 ++++---- qt/aqt/toolbar.py | 16 +-- qt/aqt/tts.py | 26 ++--- qt/aqt/update.py | 6 +- qt/aqt/utils.py | 101 ++++++++--------- qt/aqt/webview.py | 38 +++---- qt/aqt/winpaths.py | 4 +- 98 files changed, 926 insertions(+), 971 deletions(-) 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