mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
run pyupgrade over codebase [python upgrade required]
This adds Python 3.9 and 3.10 typing syntax to files that import attributions from __future___. Python 3.9 should be able to cope with the 3.10 syntax, but Python 3.8 will no longer work. On Windows/Mac, install the latest Python 3.9 version from python.org. There are currently no orjson wheels for Python 3.10 on Windows/Mac, which will break the build unless you have Rust installed separately. On Linux, modern distros should have Python 3.9 available already. If you're on an older distro, you'll need to build Python from source first.
This commit is contained in:
parent
8833175bb4
commit
b9251290ca
98 changed files with 926 additions and 971 deletions
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import annotations
|
|||
import datetime
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
from typing import Sequence
|
||||
|
||||
import anki.cards
|
||||
import anki.collection
|
||||
|
@ -37,12 +37,12 @@ class CardStats:
|
|||
|
||||
# legacy
|
||||
|
||||
def addLine(self, k: str, v: Union[int, str]) -> None:
|
||||
def addLine(self, k: str, v: int | str) -> None:
|
||||
self.txt += self.makeLine(k, v)
|
||||
|
||||
def makeLine(self, k: str, v: Union[str, int]) -> str:
|
||||
def makeLine(self, k: str, v: str | int) -> str:
|
||||
txt = "<tr><td align=left style='padding-right: 3px;'>"
|
||||
txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v)
|
||||
txt += f"<b>{k}</b></td><td>{v}</td></tr>"
|
||||
return txt
|
||||
|
||||
def date(self, tm: float) -> str:
|
||||
|
@ -183,7 +183,7 @@ from revlog where id > ? """
|
|||
# Due and cumulative due
|
||||
######################################################################
|
||||
|
||||
def get_start_end_chunk(self, by: str = "review") -> Tuple[int, Optional[int], int]:
|
||||
def get_start_end_chunk(self, by: str = "review") -> tuple[int, int | None, int]:
|
||||
start = 0
|
||||
if self.type == PERIOD_MONTH:
|
||||
end, chunk = 31, 1
|
||||
|
@ -245,7 +245,7 @@ from revlog where id > ? """
|
|||
return txt
|
||||
|
||||
def _dueInfo(self, tot: int, num: int) -> str:
|
||||
i: List[str] = []
|
||||
i: list[str] = []
|
||||
self._line(
|
||||
i,
|
||||
"Total",
|
||||
|
@ -264,7 +264,7 @@ and due = ?"""
|
|||
return self._lineTbl(i)
|
||||
|
||||
def _due(
|
||||
self, start: Optional[int] = None, end: Optional[int] = None, chunk: int = 1
|
||||
self, start: int | None = None, end: int | None = None, chunk: int = 1
|
||||
) -> Any:
|
||||
lim = ""
|
||||
if start is not None:
|
||||
|
@ -293,7 +293,7 @@ group by day order by day"""
|
|||
data = self._added(days, chunk)
|
||||
if not data:
|
||||
return ""
|
||||
conf: Dict[str, Any] = dict(
|
||||
conf: dict[str, Any] = dict(
|
||||
xaxis=dict(tickDecimals=0, max=0.5),
|
||||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||
)
|
||||
|
@ -311,12 +311,12 @@ group by day order by day"""
|
|||
txt = self._title("Added", "The number of new cards you have added.")
|
||||
txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards")
|
||||
# total and per day average
|
||||
tot = sum([i[1] for i in data])
|
||||
tot = sum(i[1] for i in data)
|
||||
period = self._periodDays()
|
||||
if not period:
|
||||
# base off date of earliest added card
|
||||
period = self._deckAge("add")
|
||||
i: List[str] = []
|
||||
i: list[str] = []
|
||||
self._line(i, "Total", "%d cards" % tot)
|
||||
self._line(i, "Average", self._avgDay(tot, period, "cards"))
|
||||
txt += self._lineTbl(i)
|
||||
|
@ -328,7 +328,7 @@ group by day order by day"""
|
|||
data = self._done(days, chunk)
|
||||
if not data:
|
||||
return ""
|
||||
conf: Dict[str, Any] = dict(
|
||||
conf: dict[str, Any] = dict(
|
||||
xaxis=dict(tickDecimals=0, max=0.5),
|
||||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||
)
|
||||
|
@ -384,20 +384,20 @@ group by day order by day"""
|
|||
|
||||
def _ansInfo(
|
||||
self,
|
||||
totd: List[Tuple[int, float]],
|
||||
totd: list[tuple[int, float]],
|
||||
studied: int,
|
||||
first: int,
|
||||
unit: str,
|
||||
convHours: bool = False,
|
||||
total: Optional[int] = None,
|
||||
) -> Tuple[str, int]:
|
||||
total: int | None = None,
|
||||
) -> tuple[str, int]:
|
||||
assert totd
|
||||
tot = totd[-1][1]
|
||||
period = self._periodDays()
|
||||
if not period:
|
||||
# base off earliest repetition date
|
||||
period = self._deckAge("review")
|
||||
i: List[str] = []
|
||||
i: list[str] = []
|
||||
self._line(
|
||||
i,
|
||||
"Days studied",
|
||||
|
@ -432,12 +432,12 @@ group by day order by day"""
|
|||
|
||||
def _splitRepData(
|
||||
self,
|
||||
data: List[Tuple[Any, ...]],
|
||||
spec: Sequence[Tuple[int, str, str]],
|
||||
) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]:
|
||||
sep: Dict[int, Any] = {}
|
||||
data: list[tuple[Any, ...]],
|
||||
spec: Sequence[tuple[int, str, str]],
|
||||
) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
|
||||
sep: dict[int, Any] = {}
|
||||
totcnt = {}
|
||||
totd: Dict[int, Any] = {}
|
||||
totd: dict[int, Any] = {}
|
||||
alltot = []
|
||||
allcnt: float = 0
|
||||
for (n, col, lab) in spec:
|
||||
|
@ -471,7 +471,7 @@ group by day order by day"""
|
|||
)
|
||||
return (ret, alltot)
|
||||
|
||||
def _added(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
|
||||
def _added(self, num: int | None = 7, chunk: int = 1) -> Any:
|
||||
lims = []
|
||||
if num is not None:
|
||||
lims.append(
|
||||
|
@ -498,7 +498,7 @@ group by day order by day"""
|
|||
chunk,
|
||||
)
|
||||
|
||||
def _done(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
|
||||
def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
|
||||
lims = []
|
||||
if num is not None:
|
||||
lims.append(
|
||||
|
@ -605,12 +605,12 @@ group by day order by day)"""
|
|||
yaxes=[dict(), dict(position="right", max=105)],
|
||||
),
|
||||
)
|
||||
i: List[str] = []
|
||||
i: list[str] = []
|
||||
self._line(i, "Average interval", self.col.format_timespan(avg * 86400))
|
||||
self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400))
|
||||
return txt + self._lineTbl(i)
|
||||
|
||||
def _ivls(self) -> Tuple[List[Any], int]:
|
||||
def _ivls(self) -> tuple[list[Any], int]:
|
||||
start, end, chunk = self.get_start_end_chunk()
|
||||
lim = "and grp <= %d" % end if end else ""
|
||||
data = [
|
||||
|
@ -643,7 +643,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
|||
# 3 + 4 + 4 + spaces on sides and middle = 15
|
||||
# yng starts at 1+3+1 = 5
|
||||
# mtr starts at 5+4+1 = 10
|
||||
d: Dict[str, List] = {"lrn": [], "yng": [], "mtr": []}
|
||||
d: dict[str, list] = {"lrn": [], "yng": [], "mtr": []}
|
||||
types = ("lrn", "yng", "mtr")
|
||||
eases = self._eases()
|
||||
for (type, ease, cnt) in eases:
|
||||
|
@ -685,7 +685,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
|||
txt += self._easeInfo(eases)
|
||||
return txt
|
||||
|
||||
def _easeInfo(self, eases: List[Tuple[int, int, int]]) -> str:
|
||||
def _easeInfo(self, eases: list[tuple[int, int, int]]) -> str:
|
||||
types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
|
||||
for (type, ease, cnt) in eases:
|
||||
if ease == 1:
|
||||
|
@ -752,7 +752,7 @@ order by thetype, ease"""
|
|||
shifted = []
|
||||
counts = []
|
||||
mcount = 0
|
||||
trend: List[Tuple[int, int]] = []
|
||||
trend: list[tuple[int, int]] = []
|
||||
peak = 0
|
||||
for d in data:
|
||||
hour = (d[0] - 4) % 24
|
||||
|
@ -852,9 +852,9 @@ group by hour having count() > 30 order by hour"""
|
|||
("Suspended+Buried", colSusp),
|
||||
)
|
||||
):
|
||||
d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col))
|
||||
d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col))
|
||||
# text data
|
||||
i: List[str] = []
|
||||
i: list[str] = []
|
||||
(c, f) = self.col.db.first(
|
||||
"""
|
||||
select count(id), count(distinct nid) from cards
|
||||
|
@ -880,9 +880,7 @@ when you answer "good" on a review."""
|
|||
)
|
||||
return txt
|
||||
|
||||
def _line(
|
||||
self, i: List[str], a: str, b: Union[int, str], bold: bool = True
|
||||
) -> None:
|
||||
def _line(self, i: list[str], a: str, b: int | str, bold: bool = True) -> None:
|
||||
# T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
|
||||
colon = ":"
|
||||
if bold:
|
||||
|
@ -896,7 +894,7 @@ when you answer "good" on a review."""
|
|||
% (a, colon, b)
|
||||
)
|
||||
|
||||
def _lineTbl(self, i: List[str]) -> str:
|
||||
def _lineTbl(self, i: list[str]) -> str:
|
||||
return "<table width=400>" + "".join(i) + "</table>"
|
||||
|
||||
def _factors(self) -> Any:
|
||||
|
@ -945,7 +943,7 @@ from cards where did in %s"""
|
|||
self,
|
||||
id: str,
|
||||
data: Any,
|
||||
conf: Optional[Any] = None,
|
||||
conf: Any | None = None,
|
||||
type: str = "bars",
|
||||
xunit: int = 1,
|
||||
ylabel: str = "Cards",
|
||||
|
@ -1069,7 +1067,7 @@ $(function () {
|
|||
)
|
||||
|
||||
def _title(self, title: str, subtitle: str = "") -> str:
|
||||
return "<h1>%s</h1>%s" % (title, subtitle)
|
||||
return f"<h1>{title}</h1>{subtitle}"
|
||||
|
||||
def _deckAge(self, by: str) -> int:
|
||||
lim = self._revlogLimit()
|
||||
|
@ -1089,7 +1087,7 @@ $(function () {
|
|||
period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400)))
|
||||
return period
|
||||
|
||||
def _periodDays(self) -> Optional[int]:
|
||||
def _periodDays(self) -> int | None:
|
||||
start, end, chunk = self.get_start_end_chunk()
|
||||
if end is None:
|
||||
return None
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -268,7 +268,9 @@ def test_chained_mods():
|
|||
a1 = "<b>sentence</b>"
|
||||
q2 = '<span style="color:red">en chaine</span>'
|
||||
a2 = "<i>chained</i>"
|
||||
note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
|
||||
note[
|
||||
"Text"
|
||||
] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format(
|
||||
q1,
|
||||
a1,
|
||||
q2,
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
150
qt/aqt/addons.py
150
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 "<br>".join(map(describe_log_entry, log))
|
||||
|
||||
|
||||
|
@ -1053,7 +1053,7 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
|
|||
return buf
|
||||
|
||||
|
||||
def download_encountered_problem(log: List[DownloadLogEntry]) -> bool:
|
||||
def download_encountered_problem(log: list[DownloadLogEntry]) -> bool:
|
||||
return any(not isinstance(e[1], InstallOk) for e in log)
|
||||
|
||||
|
||||
|
@ -1099,10 +1099,10 @@ class DownloaderInstaller(QObject):
|
|||
self.client.progress_hook = bg_thread_progress
|
||||
|
||||
def download(
|
||||
self, ids: List[int], on_done: Callable[[List[DownloadLogEntry]], None]
|
||||
self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None]
|
||||
) -> None:
|
||||
self.ids = ids
|
||||
self.log: List[DownloadLogEntry] = []
|
||||
self.log: list[DownloadLogEntry] = []
|
||||
|
||||
self.dl_bytes = 0
|
||||
self.last_tooltip = 0
|
||||
|
@ -1135,7 +1135,7 @@ class DownloaderInstaller(QObject):
|
|||
self.mgr.mw.progress.timer(50, lambda: self.on_done(self.log), False)
|
||||
|
||||
|
||||
def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
|
||||
def show_log_to_user(parent: QWidget, log: list[DownloadLogEntry]) -> None:
|
||||
have_problem = download_encountered_problem(log)
|
||||
|
||||
if have_problem:
|
||||
|
@ -1153,9 +1153,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
|
|||
def download_addons(
|
||||
parent: QWidget,
|
||||
mgr: AddonManager,
|
||||
ids: List[int],
|
||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||
client: Optional[HttpClient] = None,
|
||||
ids: list[int],
|
||||
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||
client: HttpClient | None = None,
|
||||
) -> None:
|
||||
if client is None:
|
||||
client = HttpClient()
|
||||
|
@ -1174,7 +1174,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
|||
self,
|
||||
parent: QWidget,
|
||||
mgr: AddonManager,
|
||||
updated_addons: List[UpdateInfo],
|
||||
updated_addons: list[UpdateInfo],
|
||||
) -> None:
|
||||
QListWidget.__init__(self, parent)
|
||||
self.mgr = mgr
|
||||
|
@ -1266,7 +1266,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
|||
return
|
||||
self.check_item(self.header_item, Qt.Checked)
|
||||
|
||||
def get_selected_addon_ids(self) -> List[int]:
|
||||
def get_selected_addon_ids(self) -> list[int]:
|
||||
addon_ids = []
|
||||
for i in range(1, self.count()):
|
||||
item = self.item(i)
|
||||
|
@ -1286,7 +1286,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
|||
|
||||
class ChooseAddonsToUpdateDialog(QDialog):
|
||||
def __init__(
|
||||
self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo]
|
||||
self, parent: QWidget, mgr: AddonManager, updated_addons: list[UpdateInfo]
|
||||
) -> None:
|
||||
QDialog.__init__(self, parent)
|
||||
self.setWindowTitle(tr.addons_choose_update_window_title())
|
||||
|
@ -1312,7 +1312,7 @@ class ChooseAddonsToUpdateDialog(QDialog):
|
|||
layout.addWidget(button_box)
|
||||
self.setLayout(layout)
|
||||
|
||||
def ask(self) -> List[int]:
|
||||
def ask(self) -> list[int]:
|
||||
"Returns a list of selected addons' ids"
|
||||
ret = self.exec_()
|
||||
saveGeom(self, "addonsChooseUpdate")
|
||||
|
@ -1323,9 +1323,9 @@ class ChooseAddonsToUpdateDialog(QDialog):
|
|||
return []
|
||||
|
||||
|
||||
def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
||||
def fetch_update_info(client: HttpClient, ids: list[int]) -> list[dict]:
|
||||
"""Fetch update info from AnkiWeb in one or more batches."""
|
||||
all_info: List[Dict] = []
|
||||
all_info: list[dict] = []
|
||||
|
||||
while ids:
|
||||
# get another chunk
|
||||
|
@ -1340,7 +1340,7 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
|||
|
||||
def _fetch_update_info_batch(
|
||||
client: HttpClient, chunk: Iterable[str]
|
||||
) -> Iterable[Dict]:
|
||||
) -> Iterable[dict]:
|
||||
"""Get update info from AnkiWeb.
|
||||
|
||||
Chunk must not contain more than 25 ids."""
|
||||
|
@ -1354,21 +1354,21 @@ def _fetch_update_info_batch(
|
|||
def check_and_prompt_for_updates(
|
||||
parent: QWidget,
|
||||
mgr: AddonManager,
|
||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||
requested_by_user: bool = True,
|
||||
) -> None:
|
||||
def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
|
||||
def on_updates_received(client: HttpClient, items: list[dict]) -> None:
|
||||
handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
|
||||
|
||||
check_for_updates(mgr, on_updates_received)
|
||||
|
||||
|
||||
def check_for_updates(
|
||||
mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None]
|
||||
mgr: AddonManager, on_done: Callable[[HttpClient, list[dict]], None]
|
||||
) -> None:
|
||||
client = HttpClient()
|
||||
|
||||
def check() -> List[Dict]:
|
||||
def check() -> list[dict]:
|
||||
return fetch_update_info(client, mgr.ankiweb_addons())
|
||||
|
||||
def update_info_received(future: Future) -> None:
|
||||
|
@ -1395,7 +1395,7 @@ def check_for_updates(
|
|||
|
||||
|
||||
def extract_update_info(
|
||||
current_point_version: int, current_branch_idx: int, info_json: Dict
|
||||
current_point_version: int, current_branch_idx: int, info_json: dict
|
||||
) -> UpdateInfo:
|
||||
"Process branches to determine the updated mod time and min/max versions."
|
||||
branches = info_json["branches"]
|
||||
|
@ -1425,8 +1425,8 @@ def handle_update_info(
|
|||
parent: QWidget,
|
||||
mgr: AddonManager,
|
||||
client: HttpClient,
|
||||
items: List[Dict],
|
||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||
items: list[dict],
|
||||
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||
requested_by_user: bool = True,
|
||||
) -> None:
|
||||
update_info = mgr.extract_update_info(items)
|
||||
|
@ -1445,8 +1445,8 @@ def prompt_to_update(
|
|||
parent: QWidget,
|
||||
mgr: AddonManager,
|
||||
client: HttpClient,
|
||||
updated_addons: List[UpdateInfo],
|
||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
||||
updated_addons: list[UpdateInfo],
|
||||
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||
requested_by_user: bool = True,
|
||||
) -> None:
|
||||
if not requested_by_user:
|
||||
|
@ -1468,7 +1468,7 @@ def prompt_to_update(
|
|||
|
||||
|
||||
class ConfigEditor(QDialog):
|
||||
def __init__(self, dlg: AddonsDialog, addon: str, conf: Dict) -> None:
|
||||
def __init__(self, dlg: AddonsDialog, addon: str, conf: dict) -> None:
|
||||
super().__init__(dlg)
|
||||
self.addon = addon
|
||||
self.conf = conf
|
||||
|
@ -1509,7 +1509,7 @@ class ConfigEditor(QDialog):
|
|||
else:
|
||||
self.form.scrollArea.setVisible(False)
|
||||
|
||||
def updateText(self, conf: Dict[str, Any]) -> None:
|
||||
def updateText(self, conf: dict[str, Any]) -> None:
|
||||
text = json.dumps(
|
||||
conf,
|
||||
ensure_ascii=False,
|
||||
|
@ -1584,8 +1584,8 @@ class ConfigEditor(QDialog):
|
|||
|
||||
def installAddonPackages(
|
||||
addonsManager: AddonManager,
|
||||
paths: List[str],
|
||||
parent: Optional[QWidget] = None,
|
||||
paths: list[str],
|
||||
parent: QWidget | None = None,
|
||||
warn: bool = False,
|
||||
strictly_modal: bool = False,
|
||||
advise_restart: bool = False,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from typing import Any
|
||||
|
||||
import anki
|
||||
import anki.find
|
||||
|
@ -45,7 +45,7 @@ class FindDuplicatesDialog(QDialog):
|
|||
)
|
||||
form.fields.addItems(fields)
|
||||
restore_combo_index_for_session(form.fields, fields, "findDupesFields")
|
||||
self._dupesButton: Optional[QPushButton] = None
|
||||
self._dupesButton: QPushButton | None = None
|
||||
|
||||
# links
|
||||
form.webView.set_title("find duplicates")
|
||||
|
@ -75,7 +75,7 @@ class FindDuplicatesDialog(QDialog):
|
|||
qconnect(search.clicked, on_click)
|
||||
self.show()
|
||||
|
||||
def show_duplicates_report(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
|
||||
def show_duplicates_report(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
|
||||
if not self._dupesButton:
|
||||
self._dupesButton = b = self.form.buttonBox.addButton(
|
||||
tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole
|
||||
|
@ -104,7 +104,7 @@ class FindDuplicatesDialog(QDialog):
|
|||
text += "</ol>"
|
||||
self.form.webView.stdHtml(text, context=self)
|
||||
|
||||
def _tag_duplicates(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
|
||||
def _tag_duplicates(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
|
||||
if not dupes:
|
||||
return
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import json
|
||||
import re
|
||||
from concurrent.futures import Future
|
||||
from typing import Any, Dict, List, Match, Optional
|
||||
from typing import Any, Match, Optional
|
||||
|
||||
import aqt
|
||||
from anki.collection import OpChanges
|
||||
|
@ -135,7 +135,7 @@ class CardLayout(QDialog):
|
|||
combo.setEnabled(not self._isCloze())
|
||||
self.ignore_change_signals = False
|
||||
|
||||
def _summarizedName(self, idx: int, tmpl: Dict) -> str:
|
||||
def _summarizedName(self, idx: int, tmpl: dict) -> str:
|
||||
return "{}: {}: {} -> {}".format(
|
||||
idx + 1,
|
||||
tmpl["name"],
|
||||
|
@ -146,7 +146,7 @@ class CardLayout(QDialog):
|
|||
def _fieldsOnTemplate(self, fmt: str) -> str:
|
||||
matches = re.findall("{{[^#/}]+?}}", fmt)
|
||||
chars_allowed = 30
|
||||
field_names: List[str] = []
|
||||
field_names: list[str] = []
|
||||
for m in matches:
|
||||
# strip off mustache
|
||||
m = re.sub(r"[{}]", "", m)
|
||||
|
@ -440,7 +440,7 @@ class CardLayout(QDialog):
|
|||
# Reading/writing question/answer/css
|
||||
##########################################################################
|
||||
|
||||
def current_template(self) -> Dict:
|
||||
def current_template(self) -> dict:
|
||||
if self._isCloze():
|
||||
return self.templates[0]
|
||||
return self.templates[self.ord]
|
||||
|
@ -592,7 +592,7 @@ class CardLayout(QDialog):
|
|||
|
||||
self.mw.taskman.with_progress(get_count, on_done)
|
||||
|
||||
def onRemoveInner(self, template: Dict) -> None:
|
||||
def onRemoveInner(self, template: dict) -> None:
|
||||
self.mm.remove_template(self.model, template)
|
||||
|
||||
# ensure current ordinal is within bounds
|
||||
|
@ -668,7 +668,7 @@ class CardLayout(QDialog):
|
|||
self._flipQA(old, old)
|
||||
self.redraw_everything()
|
||||
|
||||
def _flipQA(self, src: Dict, dst: Dict) -> None:
|
||||
def _flipQA(self, src: dict, dst: dict) -> None:
|
||||
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
||||
if not m:
|
||||
showInfo(tr.card_templates_anki_couldnt_find_the_line_between())
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import aqt
|
||||
from anki.collection import OpChanges
|
||||
|
@ -79,7 +79,7 @@ class DeckBrowser:
|
|||
self.refresh()
|
||||
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
self, changes: OpChanges, handler: object | None, focused: bool
|
||||
) -> bool:
|
||||
if changes.study_queues and handler is not self:
|
||||
self._refresh_needed = True
|
||||
|
@ -175,11 +175,11 @@ class DeckBrowser:
|
|||
|
||||
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
||||
buf = """
|
||||
<tr><th colspan=5 align=start>%s</th>
|
||||
<th class=count>%s</th>
|
||||
<tr><th colspan=5 align=start>{}</th>
|
||||
<th class=count>{}</th>
|
||||
<th class=count></th>
|
||||
<th class=count>%s</th>
|
||||
<th class=optscol></th></tr>""" % (
|
||||
<th class=count>{}</th>
|
||||
<th class=optscol></th></tr>""".format(
|
||||
tr.decks_deck(),
|
||||
tr.actions_new(),
|
||||
tr.statistics_due_count(),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -13,19 +13,7 @@ import zipfile
|
|||
from argparse import Namespace
|
||||
from concurrent.futures import Future
|
||||
from threading import Thread
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
TextIO,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from typing import Any, Literal, Sequence, TextIO, TypeVar, cast
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
|
@ -105,13 +93,13 @@ class AnkiQt(QMainWindow):
|
|||
profileManager: ProfileManagerType,
|
||||
backend: _RustBackend,
|
||||
opts: Namespace,
|
||||
args: List[Any],
|
||||
args: list[Any],
|
||||
) -> None:
|
||||
QMainWindow.__init__(self)
|
||||
self.backend = backend
|
||||
self.state: MainWindowState = "startup"
|
||||
self.opts = opts
|
||||
self.col: Optional[Collection] = None
|
||||
self.col: Collection | None = None
|
||||
self.taskman = TaskManager(self)
|
||||
self.media_syncer = MediaSyncer(self)
|
||||
aqt.mw = self
|
||||
|
@ -230,7 +218,7 @@ class AnkiQt(QMainWindow):
|
|||
self.pm.meta["firstRun"] = False
|
||||
self.pm.save()
|
||||
|
||||
self.pendingImport: Optional[str] = None
|
||||
self.pendingImport: str | None = None
|
||||
self.restoringBackup = False
|
||||
# profile not provided on command line?
|
||||
if not self.pm.name:
|
||||
|
@ -380,7 +368,7 @@ class AnkiQt(QMainWindow):
|
|||
self.progress.start()
|
||||
profiles = self.pm.profiles()
|
||||
|
||||
def downgrade() -> List[str]:
|
||||
def downgrade() -> list[str]:
|
||||
return self.pm.downgrade(profiles)
|
||||
|
||||
def on_done(future: Future) -> None:
|
||||
|
@ -399,7 +387,7 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
self.taskman.run_in_background(downgrade, on_done)
|
||||
|
||||
def loadProfile(self, onsuccess: Optional[Callable] = None) -> None:
|
||||
def loadProfile(self, onsuccess: Callable | None = None) -> None:
|
||||
if not self.loadCollection():
|
||||
return
|
||||
|
||||
|
@ -678,7 +666,7 @@ class AnkiQt(QMainWindow):
|
|||
self.maybe_check_for_addon_updates()
|
||||
self.deckBrowser.show()
|
||||
|
||||
def _selectedDeck(self) -> Optional[DeckDict]:
|
||||
def _selectedDeck(self) -> DeckDict | None:
|
||||
did = self.col.decks.selected()
|
||||
if not self.col.decks.name_if_exists(did):
|
||||
showInfo(tr.qt_misc_please_select_a_deck())
|
||||
|
@ -721,7 +709,7 @@ class AnkiQt(QMainWindow):
|
|||
gui_hooks.operation_did_execute(op, None)
|
||||
|
||||
def on_operation_did_execute(
|
||||
self, changes: OpChanges, handler: Optional[object]
|
||||
self, changes: OpChanges, handler: object | None
|
||||
) -> None:
|
||||
"Notify current screen of changes."
|
||||
focused = current_window() == self
|
||||
|
@ -741,7 +729,7 @@ class AnkiQt(QMainWindow):
|
|||
self.toolbar.update_sync_status()
|
||||
|
||||
def on_focus_did_change(
|
||||
self, new_focus: Optional[QWidget], _old: Optional[QWidget]
|
||||
self, new_focus: QWidget | None, _old: QWidget | None
|
||||
) -> None:
|
||||
"If main window has received focus, ensure current UI state is updated."
|
||||
if new_focus and new_focus.window() == self:
|
||||
|
@ -799,7 +787,7 @@ class AnkiQt(QMainWindow):
|
|||
self,
|
||||
link: str,
|
||||
name: str,
|
||||
key: Optional[str] = None,
|
||||
key: str | None = None,
|
||||
class_: str = "",
|
||||
id: str = "",
|
||||
extra: str = "",
|
||||
|
@ -810,8 +798,8 @@ class AnkiQt(QMainWindow):
|
|||
else:
|
||||
key = ""
|
||||
return """
|
||||
<button id="%s" class="%s" onclick="pycmd('%s');return false;"
|
||||
title="%s" %s>%s</button>""" % (
|
||||
<button id="{}" class="{}" onclick="pycmd('{}');return false;"
|
||||
title="{}" {}>{}</button>""".format(
|
||||
id,
|
||||
class_,
|
||||
link,
|
||||
|
@ -878,7 +866,7 @@ title="%s" %s>%s</button>""" % (
|
|||
|
||||
self.errorHandler = aqt.errors.ErrorHandler(self)
|
||||
|
||||
def setupAddons(self, args: Optional[List]) -> None:
|
||||
def setupAddons(self, args: list | None) -> None:
|
||||
import aqt.addons
|
||||
|
||||
self.addonManager = aqt.addons.AddonManager(self)
|
||||
|
@ -903,7 +891,7 @@ title="%s" %s>%s</button>""" % (
|
|||
)
|
||||
self.pm.set_last_addon_update_check(intTime())
|
||||
|
||||
def on_updates_installed(self, log: List[DownloadLogEntry]) -> None:
|
||||
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
|
||||
if log:
|
||||
show_log_to_user(self, log)
|
||||
|
||||
|
@ -1024,11 +1012,11 @@ title="%s" %s>%s</button>""" % (
|
|||
("y", self.on_sync_button_clicked),
|
||||
]
|
||||
self.applyShortcuts(globalShortcuts)
|
||||
self.stateShortcuts: List[QShortcut] = []
|
||||
self.stateShortcuts: list[QShortcut] = []
|
||||
|
||||
def applyShortcuts(
|
||||
self, shortcuts: Sequence[Tuple[str, Callable]]
|
||||
) -> List[QShortcut]:
|
||||
self, shortcuts: Sequence[tuple[str, Callable]]
|
||||
) -> list[QShortcut]:
|
||||
qshortcuts = []
|
||||
for key, fn in shortcuts:
|
||||
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
|
||||
|
@ -1036,7 +1024,7 @@ title="%s" %s>%s</button>""" % (
|
|||
qshortcuts.append(scut)
|
||||
return qshortcuts
|
||||
|
||||
def setStateShortcuts(self, shortcuts: List[Tuple[str, Callable]]) -> None:
|
||||
def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None:
|
||||
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
|
||||
# legacy hook
|
||||
runHook(f"{self.state}StateShortcuts", shortcuts)
|
||||
|
@ -1154,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
|
|||
|
||||
# legacy
|
||||
|
||||
def onDeckConf(self, deck: Optional[DeckDict] = None) -> None:
|
||||
def onDeckConf(self, deck: DeckDict | None = None) -> None:
|
||||
pass
|
||||
|
||||
# Importing & exporting
|
||||
|
@ -1175,7 +1163,7 @@ title="%s" %s>%s</button>""" % (
|
|||
|
||||
aqt.importing.onImport(self)
|
||||
|
||||
def onExport(self, did: Optional[DeckId] = None) -> None:
|
||||
def onExport(self, did: DeckId | None = None) -> None:
|
||||
import aqt.exporting
|
||||
|
||||
aqt.exporting.ExportDialog(self, did=did)
|
||||
|
@ -1246,7 +1234,7 @@ title="%s" %s>%s</button>""" % (
|
|||
if self.pm.meta.get("suppressUpdate", None) != ver:
|
||||
aqt.update.askAndUpdate(self, ver)
|
||||
|
||||
def newMsg(self, data: Dict) -> None:
|
||||
def newMsg(self, data: dict) -> None:
|
||||
aqt.update.showMessages(self, data)
|
||||
|
||||
def clockIsOff(self, diff: int) -> None:
|
||||
|
@ -1299,7 +1287,7 @@ title="%s" %s>%s</button>""" % (
|
|||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||
gui_hooks.focus_did_change.append(self.on_focus_did_change)
|
||||
|
||||
self._activeWindowOnPlay: Optional[QWidget] = None
|
||||
self._activeWindowOnPlay: QWidget | None = None
|
||||
|
||||
def onOdueInvalid(self) -> None:
|
||||
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
|
||||
|
@ -1486,12 +1474,12 @@ title="%s" %s>%s</button>""" % (
|
|||
c._render_output = None
|
||||
pprint.pprint(c.__dict__)
|
||||
|
||||
def _debugCard(self) -> Optional[anki.cards.Card]:
|
||||
def _debugCard(self) -> anki.cards.Card | None:
|
||||
card = self.reviewer.card
|
||||
self._card_repr(card)
|
||||
return card
|
||||
|
||||
def _debugBrowserCard(self) -> Optional[anki.cards.Card]:
|
||||
def _debugBrowserCard(self) -> anki.cards.Card | None:
|
||||
card = aqt.dialogs._dialogs["Browser"][1].card
|
||||
self._card_repr(card)
|
||||
return card
|
||||
|
@ -1564,7 +1552,7 @@ title="%s" %s>%s</button>""" % (
|
|||
_dummy1 = windll
|
||||
_dummy2 = wintypes
|
||||
|
||||
def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None:
|
||||
def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
|
||||
if not self.hideMenuAccels:
|
||||
return
|
||||
tgt = tgt or self
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from typing import Any, Callable
|
||||
|
||||
import aqt
|
||||
from anki.collection import OpChanges
|
||||
|
@ -72,7 +72,7 @@ class Overview:
|
|||
self.refresh()
|
||||
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
self, changes: OpChanges, handler: object | None, focused: bool
|
||||
) -> bool:
|
||||
if changes.study_queues:
|
||||
self._refresh_needed = True
|
||||
|
@ -115,7 +115,7 @@ class Overview:
|
|||
openLink(url)
|
||||
return False
|
||||
|
||||
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
||||
def _shortcutKeys(self) -> list[tuple[str, Callable]]:
|
||||
return [
|
||||
("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
|
||||
("r", self.rebuild_current_filtered_deck),
|
||||
|
@ -202,7 +202,7 @@ class Overview:
|
|||
def _show_finished_screen(self) -> None:
|
||||
self.web.load_ts_page("congrats")
|
||||
|
||||
def _desc(self, deck: Dict[str, Any]) -> str:
|
||||
def _desc(self, deck: dict[str, Any]) -> str:
|
||||
if deck["dyn"]:
|
||||
desc = tr.studying_this_is_a_special_deck_for()
|
||||
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
|
||||
|
@ -219,19 +219,19 @@ class Overview:
|
|||
dyn = ""
|
||||
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
|
||||
|
||||
def _table(self) -> Optional[str]:
|
||||
def _table(self) -> str | None:
|
||||
counts = list(self.mw.col.sched.counts())
|
||||
but = self.mw.button
|
||||
return """
|
||||
<table width=400 cellpadding=5>
|
||||
<tr><td align=center valign=top>
|
||||
<table cellspacing=5>
|
||||
<tr><td>%s:</td><td><b><span class=new-count>%s</span></b></td></tr>
|
||||
<tr><td>%s:</td><td><b><span class=learn-count>%s</span></b></td></tr>
|
||||
<tr><td>%s:</td><td><b><span class=review-count>%s</span></b></td></tr>
|
||||
<tr><td>{}:</td><td><b><span class=new-count>{}</span></b></td></tr>
|
||||
<tr><td>{}:</td><td><b><span class=learn-count>{}</span></b></td></tr>
|
||||
<tr><td>{}:</td><td><b><span class=review-count>{}</span></b></td></tr>
|
||||
</table>
|
||||
</td><td align=center>
|
||||
%s</td></tr></table>""" % (
|
||||
{}</td></tr></table>""".format(
|
||||
tr.actions_new(),
|
||||
counts[0],
|
||||
tr.scheduling_learning(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -11,7 +11,7 @@ import re
|
|||
import unicodedata as ucd
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast
|
||||
from typing import Any, Callable, Literal, Match, Sequence, cast
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
@ -85,7 +85,7 @@ class V3CardInfo:
|
|||
def top_card(self) -> QueuedCards.QueuedCard:
|
||||
return self.queued_cards.cards[0]
|
||||
|
||||
def counts(self) -> Tuple[int, List[int]]:
|
||||
def counts(self) -> tuple[int, list[int]]:
|
||||
"Returns (idx, counts)."
|
||||
counts = [
|
||||
self.queued_cards.new_count,
|
||||
|
@ -117,16 +117,16 @@ class Reviewer:
|
|||
def __init__(self, mw: AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
self.web = mw.web
|
||||
self.card: Optional[Card] = None
|
||||
self.cardQueue: List[Card] = []
|
||||
self.previous_card: Optional[Card] = None
|
||||
self.card: Card | None = None
|
||||
self.cardQueue: list[Card] = []
|
||||
self.previous_card: Card | None = None
|
||||
self.hadCardQueue = False
|
||||
self._answeredIds: List[CardId] = []
|
||||
self._recordedAudio: Optional[str] = None
|
||||
self._answeredIds: list[CardId] = []
|
||||
self._recordedAudio: str | None = None
|
||||
self.typeCorrect: str = None # web init happens before this is set
|
||||
self.state: Optional[str] = None
|
||||
self._refresh_needed: Optional[RefreshNeeded] = None
|
||||
self._v3: Optional[V3CardInfo] = None
|
||||
self.state: str | None = None
|
||||
self._refresh_needed: RefreshNeeded | None = None
|
||||
self._v3: V3CardInfo | None = None
|
||||
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
|
||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
@ -141,7 +141,7 @@ class Reviewer:
|
|||
self.refresh_if_needed()
|
||||
|
||||
# this is only used by add-ons
|
||||
def lastCard(self) -> Optional[Card]:
|
||||
def lastCard(self) -> Card | None:
|
||||
if self._answeredIds:
|
||||
if not self.card or self._answeredIds[-1] != self.card.id:
|
||||
try:
|
||||
|
@ -167,7 +167,7 @@ class Reviewer:
|
|||
self._refresh_needed = None
|
||||
|
||||
def op_executed(
|
||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
||||
self, changes: OpChanges, handler: object | None, focused: bool
|
||||
) -> bool:
|
||||
if handler is not self:
|
||||
if changes.study_queues:
|
||||
|
@ -234,7 +234,7 @@ class Reviewer:
|
|||
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
|
||||
self.card.start_timer()
|
||||
|
||||
def get_next_states(self) -> Optional[NextStates]:
|
||||
def get_next_states(self) -> NextStates | None:
|
||||
if v3 := self._v3:
|
||||
return v3.next_states
|
||||
else:
|
||||
|
@ -434,7 +434,7 @@ class Reviewer:
|
|||
|
||||
def _shortcutKeys(
|
||||
self,
|
||||
) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
|
||||
) -> Sequence[Union[tuple[str, Callable], tuple[Qt.Key, Callable]]]:
|
||||
return [
|
||||
("e", self.mw.onEditCurrent),
|
||||
(" ", self.onEnterKey),
|
||||
|
@ -587,7 +587,7 @@ class Reviewer:
|
|||
# can't pass a string in directly, and can't use re.escape as it
|
||||
# escapes too much
|
||||
s = """
|
||||
<span style="font-family: '%s'; font-size: %spx">%s</span>""" % (
|
||||
<span style="font-family: '{}'; font-size: {}px">{}</span>""".format(
|
||||
self.typeFont,
|
||||
self.typeSize,
|
||||
res,
|
||||
|
@ -620,23 +620,23 @@ class Reviewer:
|
|||
|
||||
def tokenizeComparison(
|
||||
self, given: str, correct: str
|
||||
) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
|
||||
) -> tuple[list[tuple[bool, str]], list[tuple[bool, str]]]:
|
||||
# compare in NFC form so accents appear correct
|
||||
given = ucd.normalize("NFC", given)
|
||||
correct = ucd.normalize("NFC", correct)
|
||||
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
||||
givenElems: List[Tuple[bool, str]] = []
|
||||
correctElems: List[Tuple[bool, str]] = []
|
||||
givenElems: list[tuple[bool, str]] = []
|
||||
correctElems: list[tuple[bool, str]] = []
|
||||
givenPoint = 0
|
||||
correctPoint = 0
|
||||
offby = 0
|
||||
|
||||
def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
|
||||
def logBad(old: int, new: int, s: str, array: list[tuple[bool, str]]) -> None:
|
||||
if old != new:
|
||||
array.append((False, s[old:new]))
|
||||
|
||||
def logGood(
|
||||
start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
|
||||
start: int, cnt: int, s: str, array: list[tuple[bool, str]]
|
||||
) -> None:
|
||||
if cnt:
|
||||
array.append((True, s[start : start + cnt]))
|
||||
|
@ -737,8 +737,8 @@ time = %(time)d;
|
|||
|
||||
def _showAnswerButton(self) -> None:
|
||||
middle = """
|
||||
<span class=stattxt>%s</span><br>
|
||||
<button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % (
|
||||
<span class=stattxt>{}</span><br>
|
||||
<button title="{}" id="ansbut" class="focus" onclick='pycmd("ans");'>{}</button>""".format(
|
||||
self._remaining(),
|
||||
tr.actions_shortcut_key(val=tr.studying_space()),
|
||||
tr.studying_show_answer(),
|
||||
|
@ -763,10 +763,10 @@ time = %(time)d;
|
|||
if not self.mw.col.conf["dueCounts"]:
|
||||
return ""
|
||||
|
||||
counts: List[Union[int, str]]
|
||||
counts: list[Union[int, str]]
|
||||
if v3 := self._v3:
|
||||
idx, counts_ = v3.counts()
|
||||
counts = cast(List[Union[int, str]], counts_)
|
||||
counts = cast(list[Union[int, str]], counts_)
|
||||
else:
|
||||
# v1/v2 scheduler
|
||||
if self.hadCardQueue:
|
||||
|
@ -790,10 +790,10 @@ time = %(time)d;
|
|||
else:
|
||||
return 2
|
||||
|
||||
def _answerButtonList(self) -> Tuple[Tuple[int, str], ...]:
|
||||
def _answerButtonList(self) -> tuple[tuple[int, str], ...]:
|
||||
button_count = self.mw.col.sched.answerButtons(self.card)
|
||||
if button_count == 2:
|
||||
buttons_tuple: Tuple[Tuple[int, str], ...] = (
|
||||
buttons_tuple: tuple[tuple[int, str], ...] = (
|
||||
(1, tr.studying_again()),
|
||||
(2, tr.studying_good()),
|
||||
)
|
||||
|
@ -847,7 +847,7 @@ time = %(time)d;
|
|||
buf += "</tr></table>"
|
||||
return buf
|
||||
|
||||
def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str:
|
||||
def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str:
|
||||
if not self.mw.col.conf["estTimes"]:
|
||||
return "<div class=spacer></div>"
|
||||
if v3_labels:
|
||||
|
@ -859,7 +859,7 @@ time = %(time)d;
|
|||
# Leeches
|
||||
##########################################################################
|
||||
|
||||
def onLeech(self, card: Optional[Card] = None) -> None:
|
||||
def onLeech(self, card: Card | None = None) -> None:
|
||||
# for now
|
||||
s = tr.studying_card_was_a_leech()
|
||||
# v3 scheduler doesn't report this
|
||||
|
@ -891,7 +891,7 @@ time = %(time)d;
|
|||
##########################################################################
|
||||
|
||||
# note the shortcuts listed here also need to be defined above
|
||||
def _contextMenu(self) -> List[Any]:
|
||||
def _contextMenu(self) -> list[Any]:
|
||||
currentFlag = self.card and self.card.user_flag()
|
||||
opts = [
|
||||
[
|
||||
|
|
|
@ -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("__")]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]
|
||||
|
|
101
qt/aqt/utils.py
101
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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue