mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -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 sys
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Sequence, Union
|
||||||
from weakref import ref
|
from weakref import ref
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
@ -59,7 +59,7 @@ class RustBackend(RustBackendGenerated):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
langs: Optional[List[str]] = None,
|
langs: list[str] | None = None,
|
||||||
server: bool = False,
|
server: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
# pick up global defaults if not provided
|
# pick up global defaults if not provided
|
||||||
|
@ -74,12 +74,12 @@ class RustBackend(RustBackendGenerated):
|
||||||
|
|
||||||
def db_query(
|
def db_query(
|
||||||
self, sql: str, args: Sequence[ValueForDB], first_row_only: bool
|
self, sql: str, args: Sequence[ValueForDB], first_row_only: bool
|
||||||
) -> List[DBRow]:
|
) -> list[DBRow]:
|
||||||
return self._db_command(
|
return self._db_command(
|
||||||
dict(kind="query", sql=sql, args=args, first_row_only=first_row_only)
|
dict(kind="query", sql=sql, args=args, first_row_only=first_row_only)
|
||||||
)
|
)
|
||||||
|
|
||||||
def db_execute_many(self, sql: str, args: List[List[ValueForDB]]) -> List[DBRow]:
|
def db_execute_many(self, sql: str, args: list[list[ValueForDB]]) -> list[DBRow]:
|
||||||
return self._db_command(dict(kind="executemany", sql=sql, args=args))
|
return self._db_command(dict(kind="executemany", sql=sql, args=args))
|
||||||
|
|
||||||
def db_begin(self) -> None:
|
def db_begin(self) -> None:
|
||||||
|
@ -91,7 +91,7 @@ class RustBackend(RustBackendGenerated):
|
||||||
def db_rollback(self) -> None:
|
def db_rollback(self) -> None:
|
||||||
return self._db_command(dict(kind="rollback"))
|
return self._db_command(dict(kind="rollback"))
|
||||||
|
|
||||||
def _db_command(self, input: Dict[str, Any]) -> Any:
|
def _db_command(self, input: dict[str, Any]) -> Any:
|
||||||
bytes_input = to_json_bytes(input)
|
bytes_input = to_json_bytes(input)
|
||||||
try:
|
try:
|
||||||
return from_json_bytes(self._backend.db_command(bytes_input))
|
return from_json_bytes(self._backend.db_command(bytes_input))
|
||||||
|
@ -102,7 +102,7 @@ class RustBackend(RustBackendGenerated):
|
||||||
raise backend_exception_to_pylib(err)
|
raise backend_exception_to_pylib(err)
|
||||||
|
|
||||||
def translate(
|
def translate(
|
||||||
self, module_index: int, message_index: int, **kwargs: Union[str, int, float]
|
self, module_index: int, message_index: int, **kwargs: str | int | float
|
||||||
) -> str:
|
) -> str:
|
||||||
return self.translate_string(
|
return self.translate_string(
|
||||||
translate_string_in(
|
translate_string_in(
|
||||||
|
@ -133,7 +133,7 @@ class RustBackend(RustBackendGenerated):
|
||||||
|
|
||||||
|
|
||||||
def translate_string_in(
|
def translate_string_in(
|
||||||
module_index: int, message_index: int, **kwargs: Union[str, int, float]
|
module_index: int, message_index: int, **kwargs: str | int | float
|
||||||
) -> i18n_pb2.TranslateStringRequest:
|
) -> i18n_pb2.TranslateStringRequest:
|
||||||
args = {}
|
args = {}
|
||||||
for (k, v) in kwargs.items():
|
for (k, v) in kwargs.items():
|
||||||
|
@ -147,10 +147,10 @@ def translate_string_in(
|
||||||
|
|
||||||
|
|
||||||
class Translations(GeneratedTranslations):
|
class Translations(GeneratedTranslations):
|
||||||
def __init__(self, backend: Optional[ref[RustBackend]]):
|
def __init__(self, backend: ref[RustBackend] | None):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
def __call__(self, key: Tuple[int, int], **kwargs: Any) -> str:
|
def __call__(self, key: tuple[int, int], **kwargs: Any) -> str:
|
||||||
"Mimic the old col.tr / TR interface"
|
"Mimic the old col.tr / TR interface"
|
||||||
if "pytest" not in sys.modules:
|
if "pytest" not in sys.modules:
|
||||||
traceback.print_stack(file=sys.stdout)
|
traceback.print_stack(file=sys.stdout)
|
||||||
|
@ -162,7 +162,7 @@ class Translations(GeneratedTranslations):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _translate(
|
def _translate(
|
||||||
self, module: int, message: int, args: Dict[str, Union[str, int, float]]
|
self, module: int, message: int, args: dict[str, str | int | float]
|
||||||
) -> str:
|
) -> str:
|
||||||
return self.backend().translate(
|
return self.backend().translate(
|
||||||
module_index=module, message_index=message, **args
|
module_index=module, message_index=message, **args
|
||||||
|
|
|
@ -50,7 +50,7 @@ def methods() -> str:
|
||||||
return "\n".join(out) + "\n"
|
return "\n".join(out) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def get_arg_types(args: List[Variable]) -> str:
|
def get_arg_types(args: list[Variable]) -> str:
|
||||||
|
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
[f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args]
|
[f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args]
|
||||||
|
@ -68,7 +68,7 @@ def arg_kind(arg: Variable) -> str:
|
||||||
return "str"
|
return "str"
|
||||||
|
|
||||||
|
|
||||||
def get_args(args: List[Variable]) -> str:
|
def get_args(args: list[Variable]) -> str:
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
[f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args]
|
[f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args]
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,11 +7,11 @@ import functools
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Callable, Dict, Optional, Tuple, Union, no_type_check
|
from typing import Any, Callable, Union, no_type_check
|
||||||
|
|
||||||
import stringcase
|
import stringcase
|
||||||
|
|
||||||
VariableTarget = Tuple[Any, str]
|
VariableTarget = tuple[Any, str]
|
||||||
DeprecatedAliasTarget = Union[Callable, VariableTarget]
|
DeprecatedAliasTarget = Union[Callable, VariableTarget]
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class DeprecatedNamesMixin:
|
||||||
# the @no_type_check lines are required to prevent mypy allowing arbitrary
|
# the @no_type_check lines are required to prevent mypy allowing arbitrary
|
||||||
# attributes on the consuming class
|
# attributes on the consuming class
|
||||||
|
|
||||||
_deprecated_aliases: Dict[str, str] = {}
|
_deprecated_aliases: dict[str, str] = {}
|
||||||
|
|
||||||
@no_type_check
|
@no_type_check
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
@ -68,7 +68,7 @@ class DeprecatedNamesMixin:
|
||||||
cls._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
|
cls._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
|
||||||
|
|
||||||
|
|
||||||
def deprecated(replaced_by: Optional[Callable] = None, info: str = "") -> Callable:
|
def deprecated(replaced_by: Callable | None = None, info: str = "") -> Callable:
|
||||||
"""Print a deprecation warning, telling users to use `replaced_by`, or show `doc`."""
|
"""Print a deprecation warning, telling users to use `replaced_by`, or show `doc`."""
|
||||||
|
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
|
|
||||||
def _build_info_path() -> str:
|
def _build_info_path() -> str:
|
||||||
|
@ -19,7 +18,7 @@ def _build_info_path() -> str:
|
||||||
raise Exception("missing buildinfo.txt")
|
raise Exception("missing buildinfo.txt")
|
||||||
|
|
||||||
|
|
||||||
def _get_build_info() -> Dict[str, str]:
|
def _get_build_info() -> dict[str, str]:
|
||||||
info = {}
|
info = {}
|
||||||
with open(_build_info_path(), encoding="utf8") as file:
|
with open(_build_info_path(), encoding="utf8") as file:
|
||||||
for line in file.readlines():
|
for line in file.readlines():
|
||||||
|
|
|
@ -7,7 +7,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
import time
|
import time
|
||||||
from typing import List, NewType, Optional
|
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki import cards_pb2, hooks
|
from anki import cards_pb2, hooks
|
||||||
|
@ -34,7 +33,7 @@ BackendCard = cards_pb2.Card
|
||||||
|
|
||||||
|
|
||||||
class Card(DeprecatedNamesMixin):
|
class Card(DeprecatedNamesMixin):
|
||||||
_note: Optional[Note]
|
_note: Note | None
|
||||||
lastIvl: int
|
lastIvl: int
|
||||||
ord: int
|
ord: int
|
||||||
nid: anki.notes.NoteId
|
nid: anki.notes.NoteId
|
||||||
|
@ -47,12 +46,12 @@ class Card(DeprecatedNamesMixin):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
col: anki.collection.Collection,
|
col: anki.collection.Collection,
|
||||||
id: Optional[CardId] = None,
|
id: CardId | None = None,
|
||||||
backend_card: Optional[BackendCard] = None,
|
backend_card: BackendCard | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.timer_started: Optional[float] = None
|
self.timer_started: float | None = None
|
||||||
self._render_output: Optional[anki.template.TemplateRenderOutput] = None
|
self._render_output: anki.template.TemplateRenderOutput | None = None
|
||||||
if id:
|
if id:
|
||||||
# existing card
|
# existing card
|
||||||
self.id = id
|
self.id = id
|
||||||
|
@ -126,10 +125,10 @@ class Card(DeprecatedNamesMixin):
|
||||||
def answer(self) -> str:
|
def answer(self) -> str:
|
||||||
return self.render_output().answer_and_style()
|
return self.render_output().answer_and_style()
|
||||||
|
|
||||||
def question_av_tags(self) -> List[AVTag]:
|
def question_av_tags(self) -> list[AVTag]:
|
||||||
return self.render_output().question_av_tags
|
return self.render_output().question_av_tags
|
||||||
|
|
||||||
def answer_av_tags(self) -> List[AVTag]:
|
def answer_av_tags(self) -> list[AVTag]:
|
||||||
return self.render_output().answer_av_tags
|
return self.render_output().answer_av_tags
|
||||||
|
|
||||||
def render_output(
|
def render_output(
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Generator, List, Literal, Optional, Sequence, Tuple, Union, cast
|
from typing import Any, Generator, Literal, Sequence, Union, cast
|
||||||
|
|
||||||
from anki import (
|
from anki import (
|
||||||
card_rendering_pb2,
|
card_rendering_pb2,
|
||||||
|
@ -92,17 +92,17 @@ LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo]
|
||||||
|
|
||||||
|
|
||||||
class Collection(DeprecatedNamesMixin):
|
class Collection(DeprecatedNamesMixin):
|
||||||
sched: Union[V1Scheduler, V2Scheduler, V3Scheduler]
|
sched: V1Scheduler | V2Scheduler | V3Scheduler
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
backend: Optional[RustBackend] = None,
|
backend: RustBackend | None = None,
|
||||||
server: bool = False,
|
server: bool = False,
|
||||||
log: bool = False,
|
log: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._backend = backend or RustBackend(server=server)
|
self._backend = backend or RustBackend(server=server)
|
||||||
self.db: Optional[DBProxy] = None
|
self.db: DBProxy | None = None
|
||||||
self._should_log = log
|
self._should_log = log
|
||||||
self.server = server
|
self.server = server
|
||||||
self.path = os.path.abspath(path)
|
self.path = os.path.abspath(path)
|
||||||
|
@ -211,7 +211,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
# to check if the backend updated the modification time.
|
# to check if the backend updated the modification time.
|
||||||
return self.db.last_begin_at != self.mod
|
return self.db.last_begin_at != self.mod
|
||||||
|
|
||||||
def save(self, name: Optional[str] = None, trx: bool = True) -> None:
|
def save(self, name: str | None = None, trx: bool = True) -> None:
|
||||||
"Flush, commit DB, and take out another write lock if trx=True."
|
"Flush, commit DB, and take out another write lock if trx=True."
|
||||||
# commit needed?
|
# commit needed?
|
||||||
if self.db.modified_in_python or self.modified_by_backend():
|
if self.db.modified_in_python or self.modified_by_backend():
|
||||||
|
@ -379,7 +379,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
hooks.notes_will_be_deleted(self, note_ids)
|
hooks.notes_will_be_deleted(self, note_ids)
|
||||||
return self._backend.remove_notes(note_ids=note_ids, card_ids=[])
|
return self._backend.remove_notes(note_ids=note_ids, card_ids=[])
|
||||||
|
|
||||||
def remove_notes_by_card(self, card_ids: List[CardId]) -> None:
|
def remove_notes_by_card(self, card_ids: list[CardId]) -> None:
|
||||||
if hooks.notes_will_be_deleted.count():
|
if hooks.notes_will_be_deleted.count():
|
||||||
nids = self.db.list(
|
nids = self.db.list(
|
||||||
f"select nid from cards where id in {ids2str(card_ids)}"
|
f"select nid from cards where id in {ids2str(card_ids)}"
|
||||||
|
@ -391,7 +391,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
return [CardId(id) for id in self._backend.cards_of_note(note_id)]
|
return [CardId(id) for id in self._backend.cards_of_note(note_id)]
|
||||||
|
|
||||||
def defaults_for_adding(
|
def defaults_for_adding(
|
||||||
self, *, current_review_card: Optional[Card]
|
self, *, current_review_card: Card | None
|
||||||
) -> anki.notes.DefaultsForAdding:
|
) -> anki.notes.DefaultsForAdding:
|
||||||
"""Get starting deck and notetype for add screen.
|
"""Get starting deck and notetype for add screen.
|
||||||
An option in the preferences controls whether this will be based on the current deck
|
An option in the preferences controls whether this will be based on the current deck
|
||||||
|
@ -406,7 +406,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
home_deck_of_current_review_card=home_deck,
|
home_deck_of_current_review_card=home_deck,
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_deck_for_notetype(self, notetype_id: NotetypeId) -> Optional[DeckId]:
|
def default_deck_for_notetype(self, notetype_id: NotetypeId) -> DeckId | None:
|
||||||
"""If 'change deck depending on notetype' is enabled in the preferences,
|
"""If 'change deck depending on notetype' is enabled in the preferences,
|
||||||
return the last deck used with the provided notetype, if any.."""
|
return the last deck used with the provided notetype, if any.."""
|
||||||
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
|
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
|
||||||
|
@ -447,7 +447,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def after_note_updates(
|
def after_note_updates(
|
||||||
self, nids: List[NoteId], mark_modified: bool, generate_cards: bool = True
|
self, nids: list[NoteId], mark_modified: bool, generate_cards: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"If notes modified directly in database, call this afterwards."
|
"If notes modified directly in database, call this afterwards."
|
||||||
self._backend.after_note_updates(
|
self._backend.after_note_updates(
|
||||||
|
@ -460,7 +460,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
def find_cards(
|
def find_cards(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
order: Union[bool, str, BrowserColumns.Column] = False,
|
order: bool | str | BrowserColumns.Column = False,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
) -> Sequence[CardId]:
|
) -> Sequence[CardId]:
|
||||||
"""Return card ids matching the provided search.
|
"""Return card ids matching the provided search.
|
||||||
|
@ -491,7 +491,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
def find_notes(
|
def find_notes(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
order: Union[bool, str, BrowserColumns.Column] = False,
|
order: bool | str | BrowserColumns.Column = False,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
) -> Sequence[NoteId]:
|
) -> Sequence[NoteId]:
|
||||||
"""Return note ids matching the provided search.
|
"""Return note ids matching the provided search.
|
||||||
|
@ -506,7 +506,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
|
|
||||||
def _build_sort_mode(
|
def _build_sort_mode(
|
||||||
self,
|
self,
|
||||||
order: Union[bool, str, BrowserColumns.Column],
|
order: bool | str | BrowserColumns.Column,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
finding_notes: bool,
|
finding_notes: bool,
|
||||||
) -> search_pb2.SortOrder:
|
) -> search_pb2.SortOrder:
|
||||||
|
@ -539,7 +539,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
search: str,
|
search: str,
|
||||||
replacement: str,
|
replacement: str,
|
||||||
regex: bool = False,
|
regex: bool = False,
|
||||||
field_name: Optional[str] = None,
|
field_name: str | None = None,
|
||||||
match_case: bool = False,
|
match_case: bool = False,
|
||||||
) -> OpChangesWithCount:
|
) -> OpChangesWithCount:
|
||||||
"Find and replace fields in a note. Returns changed note count."
|
"Find and replace fields in a note. Returns changed note count."
|
||||||
|
@ -556,14 +556,14 @@ class Collection(DeprecatedNamesMixin):
|
||||||
return self._backend.field_names_for_notes(nids)
|
return self._backend.field_names_for_notes(nids)
|
||||||
|
|
||||||
# returns array of ("dupestr", [nids])
|
# returns array of ("dupestr", [nids])
|
||||||
def find_dupes(self, field_name: str, search: str = "") -> List[Tuple[str, list]]:
|
def find_dupes(self, field_name: str, search: str = "") -> list[tuple[str, list]]:
|
||||||
nids = self.find_notes(
|
nids = self.find_notes(
|
||||||
self.build_search_string(search, SearchNode(field_name=field_name))
|
self.build_search_string(search, SearchNode(field_name=field_name))
|
||||||
)
|
)
|
||||||
# go through notes
|
# go through notes
|
||||||
vals: Dict[str, List[int]] = {}
|
vals: dict[str, list[int]] = {}
|
||||||
dupes = []
|
dupes = []
|
||||||
fields: Dict[int, int] = {}
|
fields: dict[int, int] = {}
|
||||||
|
|
||||||
def ord_for_mid(mid: NotetypeId) -> int:
|
def ord_for_mid(mid: NotetypeId) -> int:
|
||||||
if mid not in fields:
|
if mid not in fields:
|
||||||
|
@ -596,7 +596,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
|
|
||||||
def build_search_string(
|
def build_search_string(
|
||||||
self,
|
self,
|
||||||
*nodes: Union[str, SearchNode],
|
*nodes: str | SearchNode,
|
||||||
joiner: SearchJoiner = "AND",
|
joiner: SearchJoiner = "AND",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Join one or more searches, and return a normalized search string.
|
"""Join one or more searches, and return a normalized search string.
|
||||||
|
@ -612,7 +612,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
|
|
||||||
def group_searches(
|
def group_searches(
|
||||||
self,
|
self,
|
||||||
*nodes: Union[str, SearchNode],
|
*nodes: str | SearchNode,
|
||||||
joiner: SearchJoiner = "AND",
|
joiner: SearchJoiner = "AND",
|
||||||
) -> SearchNode:
|
) -> SearchNode:
|
||||||
"""Join provided search nodes and strings into a single SearchNode.
|
"""Join provided search nodes and strings into a single SearchNode.
|
||||||
|
@ -680,7 +680,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
|
def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
|
||||||
return self._backend.all_browser_columns()
|
return self._backend.all_browser_columns()
|
||||||
|
|
||||||
def get_browser_column(self, key: str) -> Optional[BrowserColumns.Column]:
|
def get_browser_column(self, key: str) -> BrowserColumns.Column | None:
|
||||||
for column in self._backend.all_browser_columns():
|
for column in self._backend.all_browser_columns():
|
||||||
if column.key == key:
|
if column.key == key:
|
||||||
return column
|
return column
|
||||||
|
@ -688,7 +688,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
|
|
||||||
def browser_row_for_id(
|
def browser_row_for_id(
|
||||||
self, id_: int
|
self, id_: int
|
||||||
) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
|
) -> tuple[Generator[tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
|
||||||
row = self._backend.browser_row_for_id(id_)
|
row = self._backend.browser_row_for_id(id_)
|
||||||
return (
|
return (
|
||||||
((cell.text, cell.is_rtl) for cell in row.cells),
|
((cell.text, cell.is_rtl) for cell in row.cells),
|
||||||
|
@ -697,7 +697,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
row.font_size,
|
row.font_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_browser_card_columns(self) -> List[str]:
|
def load_browser_card_columns(self) -> list[str]:
|
||||||
"""Return the stored card column names and ensure the backend columns are set and in sync."""
|
"""Return the stored card column names and ensure the backend columns are set and in sync."""
|
||||||
columns = self.get_config(
|
columns = self.get_config(
|
||||||
BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS
|
BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, BrowserDefaults.CARD_COLUMNS
|
||||||
|
@ -705,11 +705,11 @@ class Collection(DeprecatedNamesMixin):
|
||||||
self._backend.set_active_browser_columns(columns)
|
self._backend.set_active_browser_columns(columns)
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def set_browser_card_columns(self, columns: List[str]) -> None:
|
def set_browser_card_columns(self, columns: list[str]) -> None:
|
||||||
self.set_config(BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, columns)
|
self.set_config(BrowserConfig.ACTIVE_CARD_COLUMNS_KEY, columns)
|
||||||
self._backend.set_active_browser_columns(columns)
|
self._backend.set_active_browser_columns(columns)
|
||||||
|
|
||||||
def load_browser_note_columns(self) -> List[str]:
|
def load_browser_note_columns(self) -> list[str]:
|
||||||
"""Return the stored note column names and ensure the backend columns are set and in sync."""
|
"""Return the stored note column names and ensure the backend columns are set and in sync."""
|
||||||
columns = self.get_config(
|
columns = self.get_config(
|
||||||
BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS
|
BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, BrowserDefaults.NOTE_COLUMNS
|
||||||
|
@ -717,7 +717,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
self._backend.set_active_browser_columns(columns)
|
self._backend.set_active_browser_columns(columns)
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def set_browser_note_columns(self, columns: List[str]) -> None:
|
def set_browser_note_columns(self, columns: list[str]) -> None:
|
||||||
self.set_config(BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, columns)
|
self.set_config(BrowserConfig.ACTIVE_NOTE_COLUMNS_KEY, columns)
|
||||||
self._backend.set_active_browser_columns(columns)
|
self._backend.set_active_browser_columns(columns)
|
||||||
|
|
||||||
|
@ -745,7 +745,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
def remove_config(self, key: str) -> OpChanges:
|
def remove_config(self, key: str) -> OpChanges:
|
||||||
return self.conf.remove(key)
|
return self.conf.remove(key)
|
||||||
|
|
||||||
def all_config(self) -> Dict[str, Any]:
|
def all_config(self) -> dict[str, Any]:
|
||||||
"This is a debugging aid. Prefer .get_config() when you know the key you need."
|
"This is a debugging aid. Prefer .get_config() when you know the key you need."
|
||||||
return from_json_bytes(self._backend.get_all_config())
|
return from_json_bytes(self._backend.get_all_config())
|
||||||
|
|
||||||
|
@ -802,7 +802,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
# Stats
|
# Stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def stats(self) -> "anki.stats.CollectionStats":
|
def stats(self) -> anki.stats.CollectionStats:
|
||||||
from anki.stats import CollectionStats
|
from anki.stats import CollectionStats
|
||||||
|
|
||||||
return CollectionStats(self)
|
return CollectionStats(self)
|
||||||
|
@ -926,7 +926,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_backend_undo_status(self) -> Optional[UndoStatus]:
|
def _check_backend_undo_status(self) -> UndoStatus | None:
|
||||||
"""Return undo status if undo available on backend.
|
"""Return undo status if undo available on backend.
|
||||||
If backend has undo available, clear the Python undo state."""
|
If backend has undo available, clear the Python undo state."""
|
||||||
status = self._backend.get_undo_status()
|
status = self._backend.get_undo_status()
|
||||||
|
@ -956,7 +956,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
self.clear_python_undo()
|
self.clear_python_undo()
|
||||||
return undo
|
return undo
|
||||||
|
|
||||||
def _save_checkpoint(self, name: Optional[str]) -> None:
|
def _save_checkpoint(self, name: str | None) -> None:
|
||||||
"Call via .save(). If name not provided, clear any existing checkpoint."
|
"Call via .save(). If name not provided, clear any existing checkpoint."
|
||||||
self._last_checkpoint_at = time.time()
|
self._last_checkpoint_at = time.time()
|
||||||
if name:
|
if name:
|
||||||
|
@ -1017,7 +1017,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
# DB maintenance
|
# DB maintenance
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def fix_integrity(self) -> Tuple[str, bool]:
|
def fix_integrity(self) -> tuple[str, bool]:
|
||||||
"""Fix possible problems and rebuild caches.
|
"""Fix possible problems and rebuild caches.
|
||||||
|
|
||||||
Returns tuple of (error: str, ok: bool). 'ok' will be true if no
|
Returns tuple of (error: str, ok: bool). 'ok' will be true if no
|
||||||
|
@ -1108,7 +1108,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
self._startTime = time.time()
|
self._startTime = time.time()
|
||||||
self._startReps = self.sched.reps
|
self._startReps = self.sched.reps
|
||||||
|
|
||||||
def timeboxReached(self) -> Union[Literal[False], Tuple[Any, int]]:
|
def timeboxReached(self) -> Literal[False] | tuple[Any, int]:
|
||||||
"Return (elapsedTime, reps) if timebox reached, or False."
|
"Return (elapsedTime, reps) if timebox reached, or False."
|
||||||
if not self.conf["timeLim"]:
|
if not self.conf["timeLim"]:
|
||||||
# timeboxing disabled
|
# timeboxing disabled
|
||||||
|
@ -1126,7 +1126,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
print(args, kwargs)
|
print(args, kwargs)
|
||||||
|
|
||||||
@deprecated(replaced_by=undo_status)
|
@deprecated(replaced_by=undo_status)
|
||||||
def undo_name(self) -> Optional[str]:
|
def undo_name(self) -> str | None:
|
||||||
"Undo menu item name, or None if undo unavailable."
|
"Undo menu item name, or None if undo unavailable."
|
||||||
status = self.undo_status()
|
status = self.undo_status()
|
||||||
return status.undo or None
|
return status.undo or None
|
||||||
|
@ -1146,7 +1146,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
self.remove_notes(ids)
|
self.remove_notes(ids)
|
||||||
|
|
||||||
@deprecated(replaced_by=remove_notes)
|
@deprecated(replaced_by=remove_notes)
|
||||||
def _remNotes(self, ids: List[NoteId]) -> None:
|
def _remNotes(self, ids: list[NoteId]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@deprecated(replaced_by=card_stats)
|
@deprecated(replaced_by=card_stats)
|
||||||
|
@ -1154,21 +1154,21 @@ table.review-log {{ {revlog_style} }}
|
||||||
return self.card_stats(card.id, include_revlog=False)
|
return self.card_stats(card.id, include_revlog=False)
|
||||||
|
|
||||||
@deprecated(replaced_by=after_note_updates)
|
@deprecated(replaced_by=after_note_updates)
|
||||||
def updateFieldCache(self, nids: List[NoteId]) -> None:
|
def updateFieldCache(self, nids: list[NoteId]) -> None:
|
||||||
self.after_note_updates(nids, mark_modified=False, generate_cards=False)
|
self.after_note_updates(nids, mark_modified=False, generate_cards=False)
|
||||||
|
|
||||||
@deprecated(replaced_by=after_note_updates)
|
@deprecated(replaced_by=after_note_updates)
|
||||||
def genCards(self, nids: List[NoteId]) -> List[int]:
|
def genCards(self, nids: list[NoteId]) -> list[int]:
|
||||||
self.after_note_updates(nids, mark_modified=False, generate_cards=True)
|
self.after_note_updates(nids, mark_modified=False, generate_cards=True)
|
||||||
# previously returned empty cards, no longer does
|
# previously returned empty cards, no longer does
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@deprecated(info="no longer used")
|
@deprecated(info="no longer used")
|
||||||
def emptyCids(self) -> List[CardId]:
|
def emptyCids(self) -> list[CardId]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@deprecated(info="handled by backend")
|
@deprecated(info="handled by backend")
|
||||||
def _logRem(self, ids: List[Union[int, NoteId]], type: int) -> None:
|
def _logRem(self, ids: list[int | NoteId], type: int) -> None:
|
||||||
self.db.executemany(
|
self.db.executemany(
|
||||||
"insert into graves values (%d, ?, %d)" % (self.usn(), type),
|
"insert into graves values (%d, ?, %d)" % (self.usn(), type),
|
||||||
([x] for x in ids),
|
([x] for x in ids),
|
||||||
|
@ -1197,7 +1197,7 @@ _Collection = Collection
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _ReviewsUndo:
|
class _ReviewsUndo:
|
||||||
entries: List[LegacyReviewUndo] = field(default_factory=list)
|
entries: list[LegacyReviewUndo] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
|
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, NewType, Optional
|
from typing import Any, NewType
|
||||||
|
|
||||||
# whether new cards should be mixed with reviews, or shown first or last
|
# whether new cards should be mixed with reviews, or shown first or last
|
||||||
NEW_CARDS_DISTRIBUTE = 0
|
NEW_CARDS_DISTRIBUTE = 0
|
||||||
|
@ -94,7 +94,7 @@ REVLOG_RESCHED = 4
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
|
||||||
|
|
||||||
def _tr(col: Optional[anki.collection.Collection]) -> Any:
|
def _tr(col: anki.collection.Collection | None) -> Any:
|
||||||
if col:
|
if col:
|
||||||
return col.tr
|
return col.tr
|
||||||
else:
|
else:
|
||||||
|
@ -107,7 +107,7 @@ def _tr(col: Optional[anki.collection.Collection]) -> Any:
|
||||||
return tr_legacyglobal
|
return tr_legacyglobal
|
||||||
|
|
||||||
|
|
||||||
def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, Any]:
|
def newCardOrderLabels(col: anki.collection.Collection | None) -> dict[int, Any]:
|
||||||
tr = _tr(col)
|
tr = _tr(col)
|
||||||
return {
|
return {
|
||||||
0: tr.scheduling_show_new_cards_in_random_order(),
|
0: tr.scheduling_show_new_cards_in_random_order(),
|
||||||
|
@ -116,8 +116,8 @@ def newCardOrderLabels(col: Optional[anki.collection.Collection]) -> Dict[int, A
|
||||||
|
|
||||||
|
|
||||||
def newCardSchedulingLabels(
|
def newCardSchedulingLabels(
|
||||||
col: Optional[anki.collection.Collection],
|
col: anki.collection.Collection | None,
|
||||||
) -> Dict[int, Any]:
|
) -> dict[int, Any]:
|
||||||
tr = _tr(col)
|
tr = _tr(col)
|
||||||
return {
|
return {
|
||||||
0: tr.scheduling_mix_new_cards_and_reviews(),
|
0: tr.scheduling_mix_new_cards_and_reviews(),
|
||||||
|
|
|
@ -9,12 +9,14 @@ but this class is still used by aqt's profile manager, and a number
|
||||||
of add-ons rely on it.
|
of add-ons rely on it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import time
|
import time
|
||||||
from sqlite3 import Cursor
|
from sqlite3 import Cursor
|
||||||
from sqlite3 import dbapi2 as sqlite
|
from sqlite3 import dbapi2 as sqlite
|
||||||
from typing import Any, List, Type
|
from typing import Any
|
||||||
|
|
||||||
DBError = sqlite.Error
|
DBError = sqlite.Error
|
||||||
|
|
||||||
|
@ -82,7 +84,7 @@ class DB:
|
||||||
return res[0]
|
return res[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def all(self, *a: Any, **kw: Any) -> List:
|
def all(self, *a: Any, **kw: Any) -> list:
|
||||||
return self.execute(*a, **kw).fetchall()
|
return self.execute(*a, **kw).fetchall()
|
||||||
|
|
||||||
def first(self, *a: Any, **kw: Any) -> Any:
|
def first(self, *a: Any, **kw: Any) -> Any:
|
||||||
|
@ -91,7 +93,7 @@ class DB:
|
||||||
c.close()
|
c.close()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def list(self, *a: Any, **kw: Any) -> List:
|
def list(self, *a: Any, **kw: Any) -> list:
|
||||||
return [x[0] for x in self.execute(*a, **kw)]
|
return [x[0] for x in self.execute(*a, **kw)]
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
@ -124,5 +126,5 @@ class DB:
|
||||||
def _textFactory(self, data: bytes) -> str:
|
def _textFactory(self, data: bytes) -> str:
|
||||||
return str(data, errors="ignore")
|
return str(data, errors="ignore")
|
||||||
|
|
||||||
def cursor(self, factory: Type[Cursor] = Cursor) -> Cursor:
|
def cursor(self, factory: type[Cursor] = Cursor) -> Cursor:
|
||||||
return self._db.cursor(factory)
|
return self._db.cursor(factory)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from re import Match
|
from re import Match
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Iterable, Sequence, Union
|
||||||
|
|
||||||
import anki._backend
|
import anki._backend
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class DBProxy:
|
||||||
*args: ValueForDB,
|
*args: ValueForDB,
|
||||||
first_row_only: bool = False,
|
first_row_only: bool = False,
|
||||||
**kwargs: ValueForDB,
|
**kwargs: ValueForDB,
|
||||||
) -> List[Row]:
|
) -> list[Row]:
|
||||||
# mark modified?
|
# mark modified?
|
||||||
s = sql.strip().lower()
|
s = sql.strip().lower()
|
||||||
for stmt in "insert", "update", "delete":
|
for stmt in "insert", "update", "delete":
|
||||||
|
@ -62,15 +62,15 @@ class DBProxy:
|
||||||
# Query shortcuts
|
# Query shortcuts
|
||||||
###################
|
###################
|
||||||
|
|
||||||
def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]:
|
def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> list[Row]:
|
||||||
return self._query(sql, *args, first_row_only=False, **kwargs)
|
return self._query(sql, *args, first_row_only=False, **kwargs)
|
||||||
|
|
||||||
def list(
|
def list(
|
||||||
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB
|
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB
|
||||||
) -> List[ValueFromDB]:
|
) -> list[ValueFromDB]:
|
||||||
return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)]
|
return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)]
|
||||||
|
|
||||||
def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]:
|
def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Row | None:
|
||||||
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
||||||
if rows:
|
if rows:
|
||||||
return rows[0]
|
return rows[0]
|
||||||
|
@ -102,8 +102,8 @@ class DBProxy:
|
||||||
|
|
||||||
# convert kwargs to list format
|
# convert kwargs to list format
|
||||||
def emulate_named_args(
|
def emulate_named_args(
|
||||||
sql: str, args: Tuple, kwargs: Dict[str, Any]
|
sql: str, args: tuple, kwargs: dict[str, Any]
|
||||||
) -> Tuple[str, Sequence[ValueForDB]]:
|
) -> tuple[str, Sequence[ValueForDB]]:
|
||||||
# nothing to do?
|
# nothing to do?
|
||||||
if not kwargs:
|
if not kwargs:
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
|
@ -6,19 +6,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence, no_type_check
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
NewType,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
no_type_check,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import anki
|
import anki
|
||||||
|
@ -40,8 +28,8 @@ DeckConfigsForUpdate = deckconfig_pb2.DeckConfigsForUpdate
|
||||||
UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest
|
UpdateDeckConfigs = deckconfig_pb2.UpdateDeckConfigsRequest
|
||||||
|
|
||||||
# type aliases until we can move away from dicts
|
# type aliases until we can move away from dicts
|
||||||
DeckDict = Dict[str, Any]
|
DeckDict = dict[str, Any]
|
||||||
DeckConfigDict = Dict[str, Any]
|
DeckConfigDict = dict[str, Any]
|
||||||
|
|
||||||
DeckId = NewType("DeckId", int)
|
DeckId = NewType("DeckId", int)
|
||||||
DeckConfigId = NewType("DeckConfigId", int)
|
DeckConfigId = NewType("DeckConfigId", int)
|
||||||
|
@ -96,7 +84,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.decks = DecksDictProxy(col)
|
self.decks = DecksDictProxy(col)
|
||||||
|
|
||||||
def save(self, deck_or_config: Union[DeckDict, DeckConfigDict] = None) -> None:
|
def save(self, deck_or_config: DeckDict | DeckConfigDict = None) -> None:
|
||||||
"Can be called with either a deck or a deck configuration."
|
"Can be called with either a deck or a deck configuration."
|
||||||
if not deck_or_config:
|
if not deck_or_config:
|
||||||
print("col.decks.save() should be passed the changed deck")
|
print("col.decks.save() should be passed the changed deck")
|
||||||
|
@ -131,7 +119,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
name: str,
|
name: str,
|
||||||
create: bool = True,
|
create: bool = True,
|
||||||
type: DeckConfigId = DeckConfigId(0),
|
type: DeckConfigId = DeckConfigId(0),
|
||||||
) -> Optional[DeckId]:
|
) -> DeckId | None:
|
||||||
"Add a deck with NAME. Reuse deck if already exists. Return id as int."
|
"Add a deck with NAME. Reuse deck if already exists. Return id as int."
|
||||||
id = self.id_for_name(name)
|
id = self.id_for_name(name)
|
||||||
if id:
|
if id:
|
||||||
|
@ -155,13 +143,13 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
skip_empty_default=skip_empty_default, include_filtered=include_filtered
|
skip_empty_default=skip_empty_default, include_filtered=include_filtered
|
||||||
)
|
)
|
||||||
|
|
||||||
def id_for_name(self, name: str) -> Optional[DeckId]:
|
def id_for_name(self, name: str) -> DeckId | None:
|
||||||
try:
|
try:
|
||||||
return DeckId(self.col._backend.get_deck_id_by_name(name))
|
return DeckId(self.col._backend.get_deck_id_by_name(name))
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_legacy(self, did: DeckId) -> Optional[DeckDict]:
|
def get_legacy(self, did: DeckId) -> DeckDict | None:
|
||||||
try:
|
try:
|
||||||
return from_json_bytes(self.col._backend.get_deck_legacy(did))
|
return from_json_bytes(self.col._backend.get_deck_legacy(did))
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
|
@ -170,7 +158,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
def have(self, id: DeckId) -> bool:
|
def have(self, id: DeckId) -> bool:
|
||||||
return bool(self.get_legacy(id))
|
return bool(self.get_legacy(id))
|
||||||
|
|
||||||
def get_all_legacy(self) -> List[DeckDict]:
|
def get_all_legacy(self) -> list[DeckDict]:
|
||||||
return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values())
|
return list(from_json_bytes(self.col._backend.get_all_decks_legacy()).values())
|
||||||
|
|
||||||
def new_deck_legacy(self, filtered: bool) -> DeckDict:
|
def new_deck_legacy(self, filtered: bool) -> DeckDict:
|
||||||
|
@ -191,7 +179,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_deck_in_tree(
|
def find_deck_in_tree(
|
||||||
cls, node: DeckTreeNode, deck_id: DeckId
|
cls, node: DeckTreeNode, deck_id: DeckId
|
||||||
) -> Optional[DeckTreeNode]:
|
) -> DeckTreeNode | None:
|
||||||
if node.deck_id == deck_id:
|
if node.deck_id == deck_id:
|
||||||
return node
|
return node
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
|
@ -200,7 +188,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return match
|
return match
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def all(self) -> List[DeckDict]:
|
def all(self) -> list[DeckDict]:
|
||||||
"All decks. Expensive; prefer all_names_and_ids()"
|
"All decks. Expensive; prefer all_names_and_ids()"
|
||||||
return self.get_all_legacy()
|
return self.get_all_legacy()
|
||||||
|
|
||||||
|
@ -226,7 +214,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return len(self.all_names_and_ids())
|
return len(self.all_names_and_ids())
|
||||||
|
|
||||||
def card_count(
|
def card_count(
|
||||||
self, dids: Union[DeckId, Iterable[DeckId]], include_subdecks: bool
|
self, dids: DeckId | Iterable[DeckId], include_subdecks: bool
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if isinstance(dids, int):
|
if isinstance(dids, int):
|
||||||
dids = {dids}
|
dids = {dids}
|
||||||
|
@ -240,7 +228,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
)
|
)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def get(self, did: Union[DeckId, str], default: bool = True) -> Optional[DeckDict]:
|
def get(self, did: DeckId | str, default: bool = True) -> DeckDict | None:
|
||||||
if not did:
|
if not did:
|
||||||
if default:
|
if default:
|
||||||
return self.get_legacy(DEFAULT_DECK_ID)
|
return self.get_legacy(DEFAULT_DECK_ID)
|
||||||
|
@ -255,7 +243,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def by_name(self, name: str) -> Optional[DeckDict]:
|
def by_name(self, name: str) -> DeckDict | None:
|
||||||
"""Get deck with NAME, ignoring case."""
|
"""Get deck with NAME, ignoring case."""
|
||||||
id = self.id_for_name(name)
|
id = self.id_for_name(name)
|
||||||
if id:
|
if id:
|
||||||
|
@ -271,7 +259,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
def update_dict(self, deck: DeckDict) -> OpChanges:
|
def update_dict(self, deck: DeckDict) -> OpChanges:
|
||||||
return self.col._backend.update_deck_legacy(json=to_json_bytes(deck))
|
return self.col._backend.update_deck_legacy(json=to_json_bytes(deck))
|
||||||
|
|
||||||
def rename(self, deck: Union[DeckDict, DeckId], new_name: str) -> OpChanges:
|
def rename(self, deck: DeckDict | DeckId, new_name: str) -> OpChanges:
|
||||||
"Rename deck prefix to NAME if not exists. Updates children."
|
"Rename deck prefix to NAME if not exists. Updates children."
|
||||||
if isinstance(deck, int):
|
if isinstance(deck, int):
|
||||||
deck_id = deck
|
deck_id = deck
|
||||||
|
@ -300,7 +288,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
|
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
|
||||||
return self.col._backend.update_deck_configs(input=input)
|
return self.col._backend.update_deck_configs(input=input)
|
||||||
|
|
||||||
def all_config(self) -> List[DeckConfigDict]:
|
def all_config(self) -> list[DeckConfigDict]:
|
||||||
"A list of all deck config."
|
"A list of all deck config."
|
||||||
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
|
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
|
||||||
|
|
||||||
|
@ -318,7 +306,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
# dynamic decks have embedded conf
|
# dynamic decks have embedded conf
|
||||||
return deck
|
return deck
|
||||||
|
|
||||||
def get_config(self, conf_id: DeckConfigId) -> Optional[DeckConfigDict]:
|
def get_config(self, conf_id: DeckConfigId) -> DeckConfigDict | None:
|
||||||
try:
|
try:
|
||||||
return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id))
|
return from_json_bytes(self.col._backend.get_deck_config_legacy(conf_id))
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
|
@ -331,7 +319,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_config(
|
def add_config(
|
||||||
self, name: str, clone_from: Optional[DeckConfigDict] = None
|
self, name: str, clone_from: DeckConfigDict | None = None
|
||||||
) -> DeckConfigDict:
|
) -> DeckConfigDict:
|
||||||
if clone_from is not None:
|
if clone_from is not None:
|
||||||
conf = copy.deepcopy(clone_from)
|
conf = copy.deepcopy(clone_from)
|
||||||
|
@ -343,7 +331,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def add_config_returning_id(
|
def add_config_returning_id(
|
||||||
self, name: str, clone_from: Optional[DeckConfigDict] = None
|
self, name: str, clone_from: DeckConfigDict | None = None
|
||||||
) -> DeckConfigId:
|
) -> DeckConfigId:
|
||||||
return self.add_config(name, clone_from)["id"]
|
return self.add_config(name, clone_from)["id"]
|
||||||
|
|
||||||
|
@ -363,7 +351,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
deck["conf"] = id
|
deck["conf"] = id
|
||||||
self.save(deck)
|
self.save(deck)
|
||||||
|
|
||||||
def decks_using_config(self, conf: DeckConfigDict) -> List[DeckId]:
|
def decks_using_config(self, conf: DeckConfigDict) -> list[DeckId]:
|
||||||
dids = []
|
dids = []
|
||||||
for deck in self.all():
|
for deck in self.all():
|
||||||
if "conf" in deck and deck["conf"] == conf["id"]:
|
if "conf" in deck and deck["conf"] == conf["id"]:
|
||||||
|
@ -389,13 +377,13 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return deck["name"]
|
return deck["name"]
|
||||||
return self.col.tr.decks_no_deck()
|
return self.col.tr.decks_no_deck()
|
||||||
|
|
||||||
def name_if_exists(self, did: DeckId) -> Optional[str]:
|
def name_if_exists(self, did: DeckId) -> str | None:
|
||||||
deck = self.get(did, default=False)
|
deck = self.get(did, default=False)
|
||||||
if deck:
|
if deck:
|
||||||
return deck["name"]
|
return deck["name"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def cids(self, did: DeckId, children: bool = False) -> List[CardId]:
|
def cids(self, did: DeckId, children: bool = False) -> list[CardId]:
|
||||||
if not children:
|
if not children:
|
||||||
return self.col.db.list("select id from cards where did=?", did)
|
return self.col.db.list("select id from cards where did=?", did)
|
||||||
dids = [did]
|
dids = [did]
|
||||||
|
@ -403,7 +391,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
dids.append(id)
|
dids.append(id)
|
||||||
return self.col.db.list(f"select id from cards where did in {ids2str(dids)}")
|
return self.col.db.list(f"select id from cards where did in {ids2str(dids)}")
|
||||||
|
|
||||||
def for_card_ids(self, cids: List[CardId]) -> List[DeckId]:
|
def for_card_ids(self, cids: list[CardId]) -> list[DeckId]:
|
||||||
return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
|
return self.col.db.list(f"select did from cards where id in {ids2str(cids)}")
|
||||||
|
|
||||||
# Deck selection
|
# Deck selection
|
||||||
|
@ -419,7 +407,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
def current(self) -> DeckDict:
|
def current(self) -> DeckDict:
|
||||||
return self.get(self.selected())
|
return self.get(self.selected())
|
||||||
|
|
||||||
def active(self) -> List[DeckId]:
|
def active(self) -> list[DeckId]:
|
||||||
# some add-ons assume this will always be non-empty
|
# some add-ons assume this will always be non-empty
|
||||||
return self.col.sched.active_decks or [DeckId(1)]
|
return self.col.sched.active_decks or [DeckId(1)]
|
||||||
|
|
||||||
|
@ -435,7 +423,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def path(name: str) -> List[str]:
|
def path(name: str) -> list[str]:
|
||||||
return name.split("::")
|
return name.split("::")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -443,21 +431,21 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return cls.path(name)[-1]
|
return cls.path(name)[-1]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def immediate_parent_path(cls, name: str) -> List[str]:
|
def immediate_parent_path(cls, name: str) -> list[str]:
|
||||||
return cls.path(name)[:-1]
|
return cls.path(name)[:-1]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def immediate_parent(cls, name: str) -> Optional[str]:
|
def immediate_parent(cls, name: str) -> str | None:
|
||||||
parent_path = cls.immediate_parent_path(name)
|
parent_path = cls.immediate_parent_path(name)
|
||||||
if parent_path:
|
if parent_path:
|
||||||
return "::".join(parent_path)
|
return "::".join(parent_path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def key(cls, deck: DeckDict) -> List[str]:
|
def key(cls, deck: DeckDict) -> list[str]:
|
||||||
return cls.path(deck["name"])
|
return cls.path(deck["name"])
|
||||||
|
|
||||||
def children(self, did: DeckId) -> List[Tuple[str, DeckId]]:
|
def children(self, did: DeckId) -> list[tuple[str, DeckId]]:
|
||||||
"All children of did, as (name, id)."
|
"All children of did, as (name, id)."
|
||||||
name = self.get(did)["name"]
|
name = self.get(did)["name"]
|
||||||
actv = []
|
actv = []
|
||||||
|
@ -472,24 +460,24 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
DeckId(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix)
|
DeckId(d.id) for d in self.all_names_and_ids() if d.name.startswith(prefix)
|
||||||
)
|
)
|
||||||
|
|
||||||
def deck_and_child_ids(self, deck_id: DeckId) -> List[DeckId]:
|
def deck_and_child_ids(self, deck_id: DeckId) -> list[DeckId]:
|
||||||
parent_name = self.name(deck_id)
|
parent_name = self.name(deck_id)
|
||||||
out = [deck_id]
|
out = [deck_id]
|
||||||
out.extend(self.child_ids(parent_name))
|
out.extend(self.child_ids(parent_name))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def parents(
|
def parents(
|
||||||
self, did: DeckId, name_map: Optional[Dict[str, DeckDict]] = None
|
self, did: DeckId, name_map: dict[str, DeckDict] | None = None
|
||||||
) -> List[DeckDict]:
|
) -> list[DeckDict]:
|
||||||
"All parents of did."
|
"All parents of did."
|
||||||
# get parent and grandparent names
|
# get parent and grandparent names
|
||||||
parents_names: List[str] = []
|
parents_names: list[str] = []
|
||||||
for part in self.immediate_parent_path(self.get(did)["name"]):
|
for part in self.immediate_parent_path(self.get(did)["name"]):
|
||||||
if not parents_names:
|
if not parents_names:
|
||||||
parents_names.append(part)
|
parents_names.append(part)
|
||||||
else:
|
else:
|
||||||
parents_names.append(f"{parents_names[-1]}::{part}")
|
parents_names.append(f"{parents_names[-1]}::{part}")
|
||||||
parents: List[DeckDict] = []
|
parents: list[DeckDict] = []
|
||||||
# convert to objects
|
# convert to objects
|
||||||
for parent_name in parents_names:
|
for parent_name in parents_names:
|
||||||
if name_map:
|
if name_map:
|
||||||
|
@ -499,13 +487,13 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
parents.append(deck)
|
parents.append(deck)
|
||||||
return parents
|
return parents
|
||||||
|
|
||||||
def parents_by_name(self, name: str) -> List[DeckDict]:
|
def parents_by_name(self, name: str) -> list[DeckDict]:
|
||||||
"All existing parents of name"
|
"All existing parents of name"
|
||||||
if "::" not in name:
|
if "::" not in name:
|
||||||
return []
|
return []
|
||||||
names = self.immediate_parent_path(name)
|
names = self.immediate_parent_path(name)
|
||||||
head = []
|
head = []
|
||||||
parents: List[DeckDict] = []
|
parents: list[DeckDict] = []
|
||||||
|
|
||||||
while names:
|
while names:
|
||||||
head.append(names.pop(0))
|
head.append(names.pop(0))
|
||||||
|
@ -524,7 +512,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
self.select(did)
|
self.select(did)
|
||||||
return did
|
return did
|
||||||
|
|
||||||
def is_filtered(self, did: Union[DeckId, str]) -> bool:
|
def is_filtered(self, did: DeckId | str) -> bool:
|
||||||
return bool(self.get(did)["dyn"])
|
return bool(self.get(did)["dyn"])
|
||||||
|
|
||||||
# Legacy
|
# Legacy
|
||||||
|
@ -546,11 +534,11 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
self.remove([did])
|
self.remove([did])
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
def name_map(self) -> Dict[str, DeckDict]:
|
def name_map(self) -> dict[str, DeckDict]:
|
||||||
return {d["name"]: d for d in self.all()}
|
return {d["name"]: d for d in self.all()}
|
||||||
|
|
||||||
@deprecated(info="use col.set_deck() instead")
|
@deprecated(info="use col.set_deck() instead")
|
||||||
def set_deck(self, cids: List[CardId], did: DeckId) -> None:
|
def set_deck(self, cids: list[CardId], did: DeckId) -> None:
|
||||||
self.col.set_deck(card_ids=cids, deck_id=did)
|
self.col.set_deck(card_ids=cids, deck_id=did)
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
|
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
|
||||||
|
@ -560,11 +548,11 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
def all_ids(self) -> List[str]:
|
def all_ids(self) -> list[str]:
|
||||||
return [str(x.id) for x in self.all_names_and_ids()]
|
return [str(x.id) for x in self.all_names_and_ids()]
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
def all_names(self, dyn: bool = True, force_default: bool = True) -> List[str]:
|
def all_names(self, dyn: bool = True, force_default: bool = True) -> list[str]:
|
||||||
return [
|
return [
|
||||||
x.name
|
x.name
|
||||||
for x in self.all_names_and_ids(
|
for x in self.all_names_and_ids(
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -10,7 +12,7 @@ import shutil
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BufferedWriter
|
from io import BufferedWriter
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Optional, Sequence
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -21,7 +23,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML
|
||||||
|
|
||||||
|
|
||||||
class Exporter:
|
class Exporter:
|
||||||
includeHTML: Union[bool, None] = None
|
includeHTML: bool | None = None
|
||||||
ext: Optional[str] = None
|
ext: Optional[str] = None
|
||||||
includeTags: Optional[bool] = None
|
includeTags: Optional[bool] = None
|
||||||
includeSched: Optional[bool] = None
|
includeSched: Optional[bool] = None
|
||||||
|
@ -31,7 +33,7 @@ class Exporter:
|
||||||
self,
|
self,
|
||||||
col: Collection,
|
col: Collection,
|
||||||
did: Optional[DeckId] = None,
|
did: Optional[DeckId] = None,
|
||||||
cids: Optional[List[CardId]] = None,
|
cids: Optional[list[CardId]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.did = did
|
self.did = did
|
||||||
|
@ -177,7 +179,7 @@ where cards.id in %s)"""
|
||||||
class AnkiExporter(Exporter):
|
class AnkiExporter(Exporter):
|
||||||
|
|
||||||
ext = ".anki2"
|
ext = ".anki2"
|
||||||
includeSched: Union[bool, None] = False
|
includeSched: bool | None = False
|
||||||
includeMedia = True
|
includeMedia = True
|
||||||
|
|
||||||
def __init__(self, col: Collection) -> None:
|
def __init__(self, col: Collection) -> None:
|
||||||
|
@ -187,7 +189,7 @@ class AnkiExporter(Exporter):
|
||||||
def key(col: Collection) -> str:
|
def key(col: Collection) -> str:
|
||||||
return col.tr.exporting_anki_20_deck()
|
return col.tr.exporting_anki_20_deck()
|
||||||
|
|
||||||
def deckIds(self) -> List[DeckId]:
|
def deckIds(self) -> list[DeckId]:
|
||||||
if self.cids:
|
if self.cids:
|
||||||
return self.col.decks.for_card_ids(self.cids)
|
return self.col.decks.for_card_ids(self.cids)
|
||||||
elif self.did:
|
elif self.did:
|
||||||
|
@ -210,7 +212,7 @@ class AnkiExporter(Exporter):
|
||||||
cids = self.cardIds()
|
cids = self.cardIds()
|
||||||
# copy cards, noting used nids
|
# copy cards, noting used nids
|
||||||
nids = {}
|
nids = {}
|
||||||
data: List[Sequence] = []
|
data: list[Sequence] = []
|
||||||
for row in self.src.db.execute(
|
for row in self.src.db.execute(
|
||||||
"select * from cards where id in " + ids2str(cids)
|
"select * from cards where id in " + ids2str(cids)
|
||||||
):
|
):
|
||||||
|
@ -344,7 +346,7 @@ class AnkiPackageExporter(AnkiExporter):
|
||||||
z.writestr("media", json.dumps(media))
|
z.writestr("media", json.dumps(media))
|
||||||
z.close()
|
z.close()
|
||||||
|
|
||||||
def doExport(self, z: ZipFile, path: str) -> Dict[str, str]: # type: ignore
|
def doExport(self, z: ZipFile, path: str) -> dict[str, str]: # type: ignore
|
||||||
# export into the anki2 file
|
# export into the anki2 file
|
||||||
colfile = path.replace(".apkg", ".anki2")
|
colfile = path.replace(".apkg", ".anki2")
|
||||||
AnkiExporter.exportInto(self, colfile)
|
AnkiExporter.exportInto(self, colfile)
|
||||||
|
@ -368,7 +370,7 @@ class AnkiPackageExporter(AnkiExporter):
|
||||||
shutil.rmtree(path.replace(".apkg", ".media"))
|
shutil.rmtree(path.replace(".apkg", ".media"))
|
||||||
return media
|
return media
|
||||||
|
|
||||||
def _exportMedia(self, z: ZipFile, files: List[str], fdir: str) -> Dict[str, str]:
|
def _exportMedia(self, z: ZipFile, files: list[str], fdir: str) -> dict[str, str]:
|
||||||
media = {}
|
media = {}
|
||||||
for c, file in enumerate(files):
|
for c, file in enumerate(files):
|
||||||
cStr = str(c)
|
cStr = str(c)
|
||||||
|
@ -445,13 +447,13 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter):
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def exporters(col: Collection) -> List[Tuple[str, Any]]:
|
def exporters(col: Collection) -> list[tuple[str, Any]]:
|
||||||
def id(obj):
|
def id(obj):
|
||||||
if callable(obj.key):
|
if callable(obj.key):
|
||||||
key_str = obj.key(col)
|
key_str = obj.key(col)
|
||||||
else:
|
else:
|
||||||
key_str = obj.key
|
key_str = obj.key
|
||||||
return ("%s (*%s)" % (key_str, obj.ext), obj)
|
return (f"{key_str} (*{obj.ext})", obj)
|
||||||
|
|
||||||
exps = [
|
exps = [
|
||||||
id(AnkiCollectionPackageExporter),
|
id(AnkiCollectionPackageExporter),
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional, Set
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from anki.hooks import *
|
from anki.hooks import *
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class Finder:
|
class Finder:
|
||||||
def __init__(self, col: Optional[Collection]) -> None:
|
def __init__(self, col: Collection | None) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
|
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ def findReplace(
|
||||||
src: str,
|
src: str,
|
||||||
dst: str,
|
dst: str,
|
||||||
regex: bool = False,
|
regex: bool = False,
|
||||||
field: Optional[str] = None,
|
field: str | None = None,
|
||||||
fold: bool = True,
|
fold: bool = True,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Find and replace fields in a note. Returns changed note count."
|
"Find and replace fields in a note. Returns changed note count."
|
||||||
|
@ -58,7 +58,7 @@ def fieldNamesForNotes(col: Collection, nids: List[NoteId]) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def fieldNames(col: Collection, downcase: bool = True) -> List:
|
def fieldNames(col: Collection, downcase: bool = True) -> List:
|
||||||
fields: Set[str] = set()
|
fields: set[str] = set()
|
||||||
for m in col.models.all():
|
for m in col.models.all():
|
||||||
for f in m["flds"]:
|
for f in m["flds"]:
|
||||||
name = f["name"].lower() if downcase else f["name"]
|
name = f["name"].lower() if downcase else f["name"]
|
||||||
|
|
|
@ -12,8 +12,6 @@ modifying it.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, List
|
|
||||||
|
|
||||||
import decorator
|
import decorator
|
||||||
|
|
||||||
# You can find the definitions in ../tools/genhooks.py
|
# You can find the definitions in ../tools/genhooks.py
|
||||||
|
@ -22,7 +20,7 @@ from anki.hooks_gen import *
|
||||||
# Legacy hook handling
|
# Legacy hook handling
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
_hooks: Dict[str, List[Callable[..., Any]]] = {}
|
_hooks: dict[str, list[Callable[..., Any]]] = {}
|
||||||
|
|
||||||
|
|
||||||
def runHook(hook: str, *args: Any) -> None:
|
def runHook(hook: str, *args: Any) -> None:
|
||||||
|
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
@ -24,9 +24,9 @@ class HttpClient:
|
||||||
verify = True
|
verify = True
|
||||||
timeout = 60
|
timeout = 60
|
||||||
# args are (upload_bytes_in_chunk, download_bytes_in_chunk)
|
# args are (upload_bytes_in_chunk, download_bytes_in_chunk)
|
||||||
progress_hook: Optional[ProgressCallback] = None
|
progress_hook: ProgressCallback | None = None
|
||||||
|
|
||||||
def __init__(self, progress_hook: Optional[ProgressCallback] = None) -> None:
|
def __init__(self, progress_hook: ProgressCallback | None = None) -> None:
|
||||||
self.progress_hook = progress_hook
|
self.progress_hook = progress_hook
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
@ -44,9 +44,7 @@ class HttpClient:
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def post(
|
def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response:
|
||||||
self, url: str, data: bytes, headers: Optional[Dict[str, str]]
|
|
||||||
) -> Response:
|
|
||||||
headers["User-Agent"] = self._agentName()
|
headers["User-Agent"] = self._agentName()
|
||||||
return self.session.post(
|
return self.session.post(
|
||||||
url,
|
url,
|
||||||
|
@ -57,7 +55,7 @@ class HttpClient:
|
||||||
verify=self.verify,
|
verify=self.verify,
|
||||||
) # pytype: disable=wrong-arg-types
|
) # pytype: disable=wrong-arg-types
|
||||||
|
|
||||||
def get(self, url: str, headers: Dict[str, str] = None) -> Response:
|
def get(self, url: str, headers: dict[str, str] = None) -> Response:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["User-Agent"] = self._agentName()
|
headers["User-Agent"] = self._agentName()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import Any, Callable, Sequence, Tuple, Type, Union
|
from typing import Any, Callable, Sequence, Type, Union
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.importing.anki2 import Anki2Importer
|
from anki.importing.anki2 import Anki2Importer
|
||||||
|
@ -14,7 +14,7 @@ from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
|
||||||
from anki.lang import TR
|
from anki.lang import TR
|
||||||
|
|
||||||
|
|
||||||
def importers(col: Collection) -> Sequence[Tuple[str, Type[Importer]]]:
|
def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
|
||||||
return (
|
return (
|
||||||
(col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter),
|
(col.tr.importing_text_separated_by_tabs_or_semicolons(), TextImporter),
|
||||||
(
|
(
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
@ -37,7 +37,7 @@ class Anki2Importer(Importer):
|
||||||
super().__init__(col, file)
|
super().__init__(col, file)
|
||||||
|
|
||||||
# set later, defined here for typechecking
|
# set later, defined here for typechecking
|
||||||
self._decks: Dict[DeckId, DeckId] = {}
|
self._decks: dict[DeckId, DeckId] = {}
|
||||||
self.source_needs_upgrade = False
|
self.source_needs_upgrade = False
|
||||||
|
|
||||||
def run(self, media: None = None, importing_v2: bool = True) -> None:
|
def run(self, media: None = None, importing_v2: bool = True) -> None:
|
||||||
|
@ -80,14 +80,14 @@ class Anki2Importer(Importer):
|
||||||
# Notes
|
# Notes
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _logNoteRow(self, action: str, noteRow: List[str]) -> None:
|
def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
|
||||||
self.log.append(
|
self.log.append(
|
||||||
"[%s] %s" % (action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
|
"[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _importNotes(self) -> None:
|
def _importNotes(self) -> None:
|
||||||
# build guid -> (id,mod,mid) hash & map of existing note ids
|
# build guid -> (id,mod,mid) hash & map of existing note ids
|
||||||
self._notes: Dict[str, Tuple[NoteId, int, NotetypeId]] = {}
|
self._notes: dict[str, tuple[NoteId, int, NotetypeId]] = {}
|
||||||
existing = {}
|
existing = {}
|
||||||
for id, guid, mod, mid in self.dst.db.execute(
|
for id, guid, mod, mid in self.dst.db.execute(
|
||||||
"select id, guid, mod, mid from notes"
|
"select id, guid, mod, mid from notes"
|
||||||
|
@ -96,7 +96,7 @@ class Anki2Importer(Importer):
|
||||||
existing[id] = True
|
existing[id] = True
|
||||||
# we ignore updates to changed schemas. we need to note the ignored
|
# we ignore updates to changed schemas. we need to note the ignored
|
||||||
# guids, so we avoid importing invalid cards
|
# guids, so we avoid importing invalid cards
|
||||||
self._ignoredGuids: Dict[str, bool] = {}
|
self._ignoredGuids: dict[str, bool] = {}
|
||||||
# iterate over source collection
|
# iterate over source collection
|
||||||
add = []
|
add = []
|
||||||
update = []
|
update = []
|
||||||
|
@ -194,7 +194,7 @@ class Anki2Importer(Importer):
|
||||||
|
|
||||||
# determine if note is a duplicate, and adjust mid and/or guid as required
|
# determine if note is a duplicate, and adjust mid and/or guid as required
|
||||||
# returns true if note should be added
|
# returns true if note should be added
|
||||||
def _uniquifyNote(self, note: List[Any]) -> bool:
|
def _uniquifyNote(self, note: list[Any]) -> bool:
|
||||||
origGuid = note[GUID]
|
origGuid = note[GUID]
|
||||||
srcMid = note[MID]
|
srcMid = note[MID]
|
||||||
dstMid = self._mid(srcMid)
|
dstMid = self._mid(srcMid)
|
||||||
|
@ -218,7 +218,7 @@ class Anki2Importer(Importer):
|
||||||
|
|
||||||
def _prepareModels(self) -> None:
|
def _prepareModels(self) -> None:
|
||||||
"Prepare index of schema hashes."
|
"Prepare index of schema hashes."
|
||||||
self._modelMap: Dict[NotetypeId, NotetypeId] = {}
|
self._modelMap: dict[NotetypeId, NotetypeId] = {}
|
||||||
|
|
||||||
def _mid(self, srcMid: NotetypeId) -> Any:
|
def _mid(self, srcMid: NotetypeId) -> Any:
|
||||||
"Return local id for remote MID."
|
"Return local id for remote MID."
|
||||||
|
@ -308,7 +308,7 @@ class Anki2Importer(Importer):
|
||||||
if self.source_needs_upgrade:
|
if self.source_needs_upgrade:
|
||||||
self.src.upgrade_to_v2_scheduler()
|
self.src.upgrade_to_v2_scheduler()
|
||||||
# build map of (guid, ord) -> cid and used id cache
|
# build map of (guid, ord) -> cid and used id cache
|
||||||
self._cards: Dict[Tuple[str, int], CardId] = {}
|
self._cards: dict[tuple[str, int], CardId] = {}
|
||||||
existing = {}
|
existing = {}
|
||||||
for guid, ord, cid in self.dst.db.execute(
|
for guid, ord, cid in self.dst.db.execute(
|
||||||
"select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id"
|
"select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id"
|
||||||
|
|
|
@ -7,14 +7,14 @@ import json
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from anki.importing.anki2 import Anki2Importer
|
from anki.importing.anki2 import Anki2Importer
|
||||||
from anki.utils import tmpfile
|
from anki.utils import tmpfile
|
||||||
|
|
||||||
|
|
||||||
class AnkiPackageImporter(Anki2Importer):
|
class AnkiPackageImporter(Anki2Importer):
|
||||||
nameToNum: Dict[str, str]
|
nameToNum: dict[str, str]
|
||||||
zip: Optional[zipfile.ZipFile]
|
zip: Optional[zipfile.ZipFile]
|
||||||
|
|
||||||
def run(self) -> None: # type: ignore
|
def run(self) -> None: # type: ignore
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import Any, List, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.utils import maxID
|
from anki.utils import maxID
|
||||||
|
@ -18,7 +18,7 @@ class Importer:
|
||||||
|
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
def __init__(self, col: Collection, file: str) -> None:
|
||||||
self.file = file
|
self.file = file
|
||||||
self.log: List[str] = []
|
self.log: list[str] = []
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.total = 0
|
self.total = 0
|
||||||
self.dst = None
|
self.dst = None
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import re
|
import re
|
||||||
from typing import Any, List, Optional, TextIO, Union
|
from typing import Any, Optional, TextIO
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.importing.noteimp import ForeignNote, NoteImporter
|
from anki.importing.noteimp import ForeignNote, NoteImporter
|
||||||
|
@ -19,12 +21,12 @@ class TextImporter(NoteImporter):
|
||||||
self.lines = None
|
self.lines = None
|
||||||
self.fileobj: Optional[TextIO] = None
|
self.fileobj: Optional[TextIO] = None
|
||||||
self.delimiter: Optional[str] = None
|
self.delimiter: Optional[str] = None
|
||||||
self.tagsToAdd: List[str] = []
|
self.tagsToAdd: list[str] = []
|
||||||
self.numFields = 0
|
self.numFields = 0
|
||||||
self.dialect: Optional[Any]
|
self.dialect: Optional[Any]
|
||||||
self.data: Optional[Union[str, List[str]]]
|
self.data: Optional[str | list[str]]
|
||||||
|
|
||||||
def foreignNotes(self) -> List[ForeignNote]:
|
def foreignNotes(self) -> list[ForeignNote]:
|
||||||
self.open()
|
self.open()
|
||||||
# process all lines
|
# process all lines
|
||||||
log = []
|
log = []
|
||||||
|
@ -144,7 +146,7 @@ class TextImporter(NoteImporter):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
zuper.__del__(self) # type: ignore
|
zuper.__del__(self) # type: ignore
|
||||||
|
|
||||||
def noteFromFields(self, fields: List[str]) -> ForeignNote:
|
def noteFromFields(self, fields: list[str]) -> ForeignNote:
|
||||||
note = ForeignNote()
|
note = ForeignNote()
|
||||||
note.fields.extend([x for x in fields])
|
note.fields.extend([x for x in fields])
|
||||||
note.tags.extend(self.tagsToAdd)
|
note.tags.extend(self.tagsToAdd)
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.config import Config
|
from anki.config import Config
|
||||||
|
@ -22,9 +24,9 @@ from anki.utils import (
|
||||||
timestampID,
|
timestampID,
|
||||||
)
|
)
|
||||||
|
|
||||||
TagMappedUpdate = Tuple[int, int, str, str, NoteId, str, str]
|
TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
|
||||||
TagModifiedUpdate = Tuple[int, int, str, str, NoteId, str]
|
TagModifiedUpdate = tuple[int, int, str, str, NoteId, str]
|
||||||
NoTagUpdate = Tuple[int, int, str, NoteId, str]
|
NoTagUpdate = tuple[int, int, str, NoteId, str]
|
||||||
Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate]
|
Updates = Union[TagMappedUpdate, TagModifiedUpdate, NoTagUpdate]
|
||||||
|
|
||||||
# Stores a list of fields, tags and deck
|
# Stores a list of fields, tags and deck
|
||||||
|
@ -35,10 +37,10 @@ class ForeignNote:
|
||||||
"An temporary object storing fields and attributes."
|
"An temporary object storing fields and attributes."
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.fields: List[str] = []
|
self.fields: list[str] = []
|
||||||
self.tags: List[str] = []
|
self.tags: list[str] = []
|
||||||
self.deck = None
|
self.deck = None
|
||||||
self.cards: Dict[int, ForeignCard] = {} # map of ord -> card
|
self.cards: dict[int, ForeignCard] = {} # map of ord -> card
|
||||||
self.fieldsStr = ""
|
self.fieldsStr = ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +77,7 @@ class NoteImporter(Importer):
|
||||||
needDelimiter = False
|
needDelimiter = False
|
||||||
allowHTML = False
|
allowHTML = False
|
||||||
importMode = UPDATE_MODE
|
importMode = UPDATE_MODE
|
||||||
mapping: Optional[List[str]]
|
mapping: Optional[list[str]]
|
||||||
tagModified: Optional[str]
|
tagModified: Optional[str]
|
||||||
|
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
def __init__(self, col: Collection, file: str) -> None:
|
||||||
|
@ -109,11 +111,11 @@ class NoteImporter(Importer):
|
||||||
def mappingOk(self) -> bool:
|
def mappingOk(self) -> bool:
|
||||||
return self.model["flds"][0]["name"] in self.mapping
|
return self.model["flds"][0]["name"] in self.mapping
|
||||||
|
|
||||||
def foreignNotes(self) -> List:
|
def foreignNotes(self) -> list:
|
||||||
"Return a list of foreign notes for importing."
|
"Return a list of foreign notes for importing."
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def importNotes(self, notes: List[ForeignNote]) -> None:
|
def importNotes(self, notes: list[ForeignNote]) -> None:
|
||||||
"Convert each card into a note, apply attributes and add to col."
|
"Convert each card into a note, apply attributes and add to col."
|
||||||
assert self.mappingOk()
|
assert self.mappingOk()
|
||||||
# note whether tags are mapped
|
# note whether tags are mapped
|
||||||
|
@ -122,7 +124,7 @@ class NoteImporter(Importer):
|
||||||
if f == "_tags":
|
if f == "_tags":
|
||||||
self._tagsMapped = True
|
self._tagsMapped = True
|
||||||
# gather checks for duplicate comparison
|
# gather checks for duplicate comparison
|
||||||
csums: Dict[str, List[NoteId]] = {}
|
csums: dict[str, list[NoteId]] = {}
|
||||||
for csum, id in self.col.db.execute(
|
for csum, id in self.col.db.execute(
|
||||||
"select csum, id from notes where mid = ?", self.model["id"]
|
"select csum, id from notes where mid = ?", self.model["id"]
|
||||||
):
|
):
|
||||||
|
@ -130,18 +132,18 @@ class NoteImporter(Importer):
|
||||||
csums[csum].append(id)
|
csums[csum].append(id)
|
||||||
else:
|
else:
|
||||||
csums[csum] = [id]
|
csums[csum] = [id]
|
||||||
firsts: Dict[str, bool] = {}
|
firsts: dict[str, bool] = {}
|
||||||
fld0idx = self.mapping.index(self.model["flds"][0]["name"])
|
fld0idx = self.mapping.index(self.model["flds"][0]["name"])
|
||||||
self._fmap = self.col.models.field_map(self.model)
|
self._fmap = self.col.models.field_map(self.model)
|
||||||
self._nextID = NoteId(timestampID(self.col.db, "notes"))
|
self._nextID = NoteId(timestampID(self.col.db, "notes"))
|
||||||
# loop through the notes
|
# loop through the notes
|
||||||
updates: List[Updates] = []
|
updates: list[Updates] = []
|
||||||
updateLog = []
|
updateLog = []
|
||||||
new = []
|
new = []
|
||||||
self._ids: List[NoteId] = []
|
self._ids: list[NoteId] = []
|
||||||
self._cards: List[Tuple] = []
|
self._cards: list[tuple] = []
|
||||||
dupeCount = 0
|
dupeCount = 0
|
||||||
dupes: List[str] = []
|
dupes: list[str] = []
|
||||||
for n in notes:
|
for n in notes:
|
||||||
for c, field in enumerate(n.fields):
|
for c, field in enumerate(n.fields):
|
||||||
if not self.allowHTML:
|
if not self.allowHTML:
|
||||||
|
@ -232,7 +234,7 @@ class NoteImporter(Importer):
|
||||||
|
|
||||||
def newData(
|
def newData(
|
||||||
self, n: ForeignNote
|
self, n: ForeignNote
|
||||||
) -> Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]:
|
) -> tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]:
|
||||||
id = self._nextID
|
id = self._nextID
|
||||||
self._nextID = NoteId(self._nextID + 1)
|
self._nextID = NoteId(self._nextID + 1)
|
||||||
self._ids.append(id)
|
self._ids.append(id)
|
||||||
|
@ -256,8 +258,8 @@ class NoteImporter(Importer):
|
||||||
|
|
||||||
def addNew(
|
def addNew(
|
||||||
self,
|
self,
|
||||||
rows: List[
|
rows: list[
|
||||||
Tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]
|
tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]
|
||||||
],
|
],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.col.db.executemany(
|
self.col.db.executemany(
|
||||||
|
@ -265,7 +267,7 @@ class NoteImporter(Importer):
|
||||||
)
|
)
|
||||||
|
|
||||||
def updateData(
|
def updateData(
|
||||||
self, n: ForeignNote, id: NoteId, sflds: List[str]
|
self, n: ForeignNote, id: NoteId, sflds: list[str]
|
||||||
) -> Optional[Updates]:
|
) -> Optional[Updates]:
|
||||||
self._ids.append(id)
|
self._ids.append(id)
|
||||||
self.processFields(n, sflds)
|
self.processFields(n, sflds)
|
||||||
|
@ -280,7 +282,7 @@ class NoteImporter(Importer):
|
||||||
else:
|
else:
|
||||||
return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr)
|
return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr)
|
||||||
|
|
||||||
def addUpdates(self, rows: List[Updates]) -> None:
|
def addUpdates(self, rows: list[Updates]) -> None:
|
||||||
changes = self.col.db.scalar("select total_changes()")
|
changes = self.col.db.scalar("select total_changes()")
|
||||||
if self._tagsMapped:
|
if self._tagsMapped:
|
||||||
self.col.db.executemany(
|
self.col.db.executemany(
|
||||||
|
@ -307,7 +309,7 @@ where id = ? and flds != ?""",
|
||||||
self.updateCount = changes2 - changes
|
self.updateCount = changes2 - changes
|
||||||
|
|
||||||
def processFields(
|
def processFields(
|
||||||
self, note: ForeignNote, fields: Optional[List[str]] = None
|
self, note: ForeignNote, fields: Optional[list[str]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = [""] * len(self.model["flds"])
|
fields = [""] * len(self.model["flds"])
|
||||||
|
|
|
@ -9,7 +9,7 @@ import sys
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from string import capwords
|
from string import capwords
|
||||||
from typing import List, Optional, Union
|
from typing import Optional, Union
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
from xml.dom.minidom import Element, Text
|
from xml.dom.minidom import Element, Text
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
|
|
||||||
## DEFAULT IMPORTER METHODS
|
## DEFAULT IMPORTER METHODS
|
||||||
|
|
||||||
def foreignNotes(self) -> List[ForeignNote]:
|
def foreignNotes(self) -> list[ForeignNote]:
|
||||||
|
|
||||||
# Load file and parse it by minidom
|
# Load file and parse it by minidom
|
||||||
self.loadSource(self.file)
|
self.loadSource(self.file)
|
||||||
|
@ -415,7 +415,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
self.logger("-" * 45, level=3)
|
self.logger("-" * 45, level=3)
|
||||||
for key in list(smel.keys()):
|
for key in list(smel.keys()):
|
||||||
self.logger(
|
self.logger(
|
||||||
"\t%s %s" % ((key + ":").ljust(15), smel[key]), level=3
|
"\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger("Element skiped \t- no valid Q and A ...", level=3)
|
self.logger("Element skiped \t- no valid Q and A ...", level=3)
|
||||||
|
|
|
@ -6,7 +6,6 @@ from __future__ import annotations
|
||||||
import locale
|
import locale
|
||||||
import re
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki._backend
|
import anki._backend
|
||||||
|
@ -153,7 +152,7 @@ currentLang = "en"
|
||||||
# not reference this, and should use col.tr instead. The global
|
# not reference this, and should use col.tr instead. The global
|
||||||
# instance exists for legacy reasons, and as a convenience for the
|
# instance exists for legacy reasons, and as a convenience for the
|
||||||
# Qt code.
|
# Qt code.
|
||||||
current_i18n: Optional[anki._backend.RustBackend] = None
|
current_i18n: anki._backend.RustBackend | None = None
|
||||||
tr_legacyglobal = anki._backend.Translations(None)
|
tr_legacyglobal = anki._backend.Translations(None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,7 +173,7 @@ def set_lang(lang: str) -> None:
|
||||||
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
||||||
|
|
||||||
|
|
||||||
def get_def_lang(lang: Optional[str] = None) -> Tuple[int, str]:
|
def get_def_lang(lang: str | None = None) -> tuple[int, str]:
|
||||||
"""Return lang converted to name used on disk and its index, defaulting to system language
|
"""Return lang converted to name used on disk and its index, defaulting to system language
|
||||||
or English if not available."""
|
or English if not available."""
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -7,7 +7,7 @@ import html
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import card_rendering_pb2, hooks
|
from anki import card_rendering_pb2, hooks
|
||||||
|
@ -41,7 +41,7 @@ class ExtractedLatex:
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExtractedLatexOutput:
|
class ExtractedLatexOutput:
|
||||||
html: str
|
html: str
|
||||||
latex: List[ExtractedLatex]
|
latex: list[ExtractedLatex]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_proto(
|
def from_proto(
|
||||||
|
@ -80,7 +80,7 @@ def render_latex_returning_errors(
|
||||||
model: NotetypeDict,
|
model: NotetypeDict,
|
||||||
col: anki.collection.Collection,
|
col: anki.collection.Collection,
|
||||||
expand_clozes: bool = False,
|
expand_clozes: bool = False,
|
||||||
) -> Tuple[str, List[str]]:
|
) -> tuple[str, list[str]]:
|
||||||
"""Returns (text, errors).
|
"""Returns (text, errors).
|
||||||
|
|
||||||
errors will be non-empty if LaTeX failed to render."""
|
errors will be non-empty if LaTeX failed to render."""
|
||||||
|
@ -111,7 +111,7 @@ def _save_latex_image(
|
||||||
header: str,
|
header: str,
|
||||||
footer: str,
|
footer: str,
|
||||||
svg: bool,
|
svg: bool,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
# add header/footer
|
# add header/footer
|
||||||
latex = f"{header}\n{extracted.latex_body}\n{footer}"
|
latex = f"{header}\n{extracted.latex_body}\n{footer}"
|
||||||
# it's only really secure if run in a jail, but these are the most common
|
# it's only really secure if run in a jail, but these are the most common
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pprint
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, List, Optional, Tuple
|
from typing import Any, Callable
|
||||||
|
|
||||||
from anki import media_pb2
|
from anki import media_pb2
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
@ -19,7 +19,7 @@ from anki.template import av_tags_to_native
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
|
|
||||||
|
|
||||||
def media_paths_from_col_path(col_path: str) -> Tuple[str, str]:
|
def media_paths_from_col_path(col_path: str) -> tuple[str, str]:
|
||||||
media_folder = re.sub(r"(?i)\.(anki2)$", ".media", col_path)
|
media_folder = re.sub(r"(?i)\.(anki2)$", ".media", col_path)
|
||||||
media_db = f"{media_folder}.db2"
|
media_db = f"{media_folder}.db2"
|
||||||
return (media_folder, media_db)
|
return (media_folder, media_db)
|
||||||
|
@ -50,7 +50,7 @@ class MediaManager:
|
||||||
|
|
||||||
def __init__(self, col: anki.collection.Collection, server: bool) -> None:
|
def __init__(self, col: anki.collection.Collection, server: bool) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self._dir: Optional[str] = None
|
self._dir: str | None = None
|
||||||
if server:
|
if server:
|
||||||
return
|
return
|
||||||
# media directory
|
# media directory
|
||||||
|
@ -88,7 +88,7 @@ class MediaManager:
|
||||||
# may have been deleted
|
# may have been deleted
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dir(self) -> Optional[str]:
|
def dir(self) -> str | None:
|
||||||
return self._dir
|
return self._dir
|
||||||
|
|
||||||
def force_resync(self) -> None:
|
def force_resync(self) -> None:
|
||||||
|
@ -106,7 +106,7 @@ class MediaManager:
|
||||||
def strip_av_tags(self, text: str) -> str:
|
def strip_av_tags(self, text: str) -> str:
|
||||||
return self.col._backend.strip_av_tags(text)
|
return self.col._backend.strip_av_tags(text)
|
||||||
|
|
||||||
def _extract_filenames(self, text: str) -> List[str]:
|
def _extract_filenames(self, text: str) -> list[str]:
|
||||||
"This only exists do support a legacy function; do not use."
|
"This only exists do support a legacy function; do not use."
|
||||||
out = self.col._backend.extract_av_tags(text=text, question_side=True)
|
out = self.col._backend.extract_av_tags(text=text, question_side=True)
|
||||||
return [
|
return [
|
||||||
|
@ -148,7 +148,7 @@ class MediaManager:
|
||||||
def have(self, fname: str) -> bool:
|
def have(self, fname: str) -> bool:
|
||||||
return os.path.exists(os.path.join(self.dir(), fname))
|
return os.path.exists(os.path.join(self.dir(), fname))
|
||||||
|
|
||||||
def trash_files(self, fnames: List[str]) -> None:
|
def trash_files(self, fnames: list[str]) -> None:
|
||||||
"Move provided files to the trash."
|
"Move provided files to the trash."
|
||||||
self.col._backend.trash_media_files(fnames)
|
self.col._backend.trash_media_files(fnames)
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ class MediaManager:
|
||||||
|
|
||||||
def filesInStr(
|
def filesInStr(
|
||||||
self, mid: NotetypeId, string: str, includeRemote: bool = False
|
self, mid: NotetypeId, string: str, includeRemote: bool = False
|
||||||
) -> List[str]:
|
) -> list[str]:
|
||||||
l = []
|
l = []
|
||||||
model = self.col.models.get(mid)
|
model = self.col.models.get(mid)
|
||||||
# handle latex
|
# handle latex
|
||||||
|
@ -204,8 +204,8 @@ class MediaManager:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def render_all_latex(
|
def render_all_latex(
|
||||||
self, progress_cb: Optional[Callable[[int], bool]] = None
|
self, progress_cb: Callable[[int], bool] | None = None
|
||||||
) -> Optional[Tuple[int, str]]:
|
) -> tuple[int, str] | None:
|
||||||
"""Render any LaTeX that is missing.
|
"""Render any LaTeX that is missing.
|
||||||
|
|
||||||
If a progress callback is provided and it returns false, the operation
|
If a progress callback is provided and it returns false, the operation
|
||||||
|
@ -260,7 +260,7 @@ class MediaManager:
|
||||||
|
|
||||||
addFile = add_file
|
addFile = add_file
|
||||||
|
|
||||||
def writeData(self, opath: str, data: bytes, typeHint: Optional[str] = None) -> str:
|
def writeData(self, opath: str, data: bytes, typeHint: str | None = None) -> str:
|
||||||
fname = os.path.basename(opath)
|
fname = os.path.basename(opath)
|
||||||
if typeHint:
|
if typeHint:
|
||||||
fname = self.add_extension_based_on_mime(fname, typeHint)
|
fname = self.add_extension_based_on_mime(fname, typeHint)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import copy
|
||||||
import pprint
|
import pprint
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union
|
from typing import Any, NewType, Sequence, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki import notetypes_pb2
|
from anki import notetypes_pb2
|
||||||
|
@ -29,10 +29,10 @@ ChangeNotetypeInfo = notetypes_pb2.ChangeNotetypeInfo
|
||||||
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
|
ChangeNotetypeRequest = notetypes_pb2.ChangeNotetypeRequest
|
||||||
|
|
||||||
# legacy types
|
# legacy types
|
||||||
NotetypeDict = Dict[str, Any]
|
NotetypeDict = dict[str, Any]
|
||||||
NoteType = NotetypeDict
|
NoteType = NotetypeDict
|
||||||
FieldDict = Dict[str, Any]
|
FieldDict = dict[str, Any]
|
||||||
TemplateDict = Dict[str, Union[str, int, None]]
|
TemplateDict = dict[str, Union[str, int, None]]
|
||||||
NotetypeId = NewType("NotetypeId", int)
|
NotetypeId = NewType("NotetypeId", int)
|
||||||
sys.modules["anki.models"].NoteType = NotetypeDict # type: ignore
|
sys.modules["anki.models"].NoteType = NotetypeDict # type: ignore
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
# need to cache responses from the backend. Please do not
|
# need to cache responses from the backend. Please do not
|
||||||
# access the cache directly!
|
# access the cache directly!
|
||||||
|
|
||||||
_cache: Dict[NotetypeId, NotetypeDict] = {}
|
_cache: dict[NotetypeId, NotetypeDict] = {}
|
||||||
|
|
||||||
def _update_cache(self, notetype: NotetypeDict) -> None:
|
def _update_cache(self, notetype: NotetypeDict) -> None:
|
||||||
self._cache[notetype["id"]] = notetype
|
self._cache[notetype["id"]] = notetype
|
||||||
|
@ -106,7 +106,7 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
if ntid in self._cache:
|
if ntid in self._cache:
|
||||||
del self._cache[ntid]
|
del self._cache[ntid]
|
||||||
|
|
||||||
def _get_cached(self, ntid: NotetypeId) -> Optional[NotetypeDict]:
|
def _get_cached(self, ntid: NotetypeId) -> NotetypeDict | None:
|
||||||
return self._cache.get(ntid)
|
return self._cache.get(ntid)
|
||||||
|
|
||||||
def _clear_cache(self) -> None:
|
def _clear_cache(self) -> None:
|
||||||
|
@ -142,13 +142,13 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
# Retrieving and creating models
|
# Retrieving and creating models
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def id_for_name(self, name: str) -> Optional[NotetypeId]:
|
def id_for_name(self, name: str) -> NotetypeId | None:
|
||||||
try:
|
try:
|
||||||
return NotetypeId(self.col._backend.get_notetype_id_by_name(name))
|
return NotetypeId(self.col._backend.get_notetype_id_by_name(name))
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, id: NotetypeId) -> Optional[NotetypeDict]:
|
def get(self, id: NotetypeId) -> NotetypeDict | None:
|
||||||
"Get model with ID, or None."
|
"Get model with ID, or None."
|
||||||
# deal with various legacy input types
|
# deal with various legacy input types
|
||||||
if id is None:
|
if id is None:
|
||||||
|
@ -165,11 +165,11 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
return None
|
return None
|
||||||
return notetype
|
return notetype
|
||||||
|
|
||||||
def all(self) -> List[NotetypeDict]:
|
def all(self) -> list[NotetypeDict]:
|
||||||
"Get all models."
|
"Get all models."
|
||||||
return [self.get(NotetypeId(nt.id)) for nt in self.all_names_and_ids()]
|
return [self.get(NotetypeId(nt.id)) for nt in self.all_names_and_ids()]
|
||||||
|
|
||||||
def by_name(self, name: str) -> Optional[NotetypeDict]:
|
def by_name(self, name: str) -> NotetypeDict | None:
|
||||||
"Get model with NAME."
|
"Get model with NAME."
|
||||||
id = self.id_for_name(name)
|
id = self.id_for_name(name)
|
||||||
if id:
|
if id:
|
||||||
|
@ -231,7 +231,7 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
# Tools
|
# Tools
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def nids(self, ntid: NotetypeId) -> List[anki.notes.NoteId]:
|
def nids(self, ntid: NotetypeId) -> list[anki.notes.NoteId]:
|
||||||
"Note ids for M."
|
"Note ids for M."
|
||||||
if isinstance(ntid, dict):
|
if isinstance(ntid, dict):
|
||||||
# legacy callers passed in note type
|
# legacy callers passed in note type
|
||||||
|
@ -261,11 +261,11 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
# Fields
|
# Fields
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def field_map(self, notetype: NotetypeDict) -> Dict[str, Tuple[int, FieldDict]]:
|
def field_map(self, notetype: NotetypeDict) -> dict[str, tuple[int, FieldDict]]:
|
||||||
"Mapping of field name -> (ord, field)."
|
"Mapping of field name -> (ord, field)."
|
||||||
return {f["name"]: (f["ord"], f) for f in notetype["flds"]}
|
return {f["name"]: (f["ord"], f) for f in notetype["flds"]}
|
||||||
|
|
||||||
def field_names(self, notetype: NotetypeDict) -> List[str]:
|
def field_names(self, notetype: NotetypeDict) -> list[str]:
|
||||||
return [f["name"] for f in notetype["flds"]]
|
return [f["name"] for f in notetype["flds"]]
|
||||||
|
|
||||||
def sort_idx(self, notetype: NotetypeDict) -> int:
|
def sort_idx(self, notetype: NotetypeDict) -> int:
|
||||||
|
@ -394,10 +394,10 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
def change( # pylint: disable=invalid-name
|
def change( # pylint: disable=invalid-name
|
||||||
self,
|
self,
|
||||||
notetype: NotetypeDict,
|
notetype: NotetypeDict,
|
||||||
nids: List[anki.notes.NoteId],
|
nids: list[anki.notes.NoteId],
|
||||||
newModel: NotetypeDict,
|
newModel: NotetypeDict,
|
||||||
fmap: Dict[int, Optional[int]],
|
fmap: dict[int, int | None],
|
||||||
cmap: Optional[Dict[int, Optional[int]]],
|
cmap: dict[int, int | None] | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# - maps are ord->ord, and there should not be duplicate targets
|
# - maps are ord->ord, and there should not be duplicate targets
|
||||||
self.col.mod_schema(check=True)
|
self.col.mod_schema(check=True)
|
||||||
|
@ -424,8 +424,8 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _convert_legacy_map(
|
def _convert_legacy_map(
|
||||||
self, old_to_new: Dict[int, Optional[int]], new_count: int
|
self, old_to_new: dict[int, int | None], new_count: int
|
||||||
) -> List[int]:
|
) -> list[int]:
|
||||||
"Convert old->new map to list of old indexes"
|
"Convert old->new map to list of old indexes"
|
||||||
new_to_old = {v: k for k, v in old_to_new.items() if v is not None}
|
new_to_old = {v: k for k, v in old_to_new.items() if v is not None}
|
||||||
out = []
|
out = []
|
||||||
|
@ -458,7 +458,7 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
@deprecated(info="use note.cloze_numbers_in_fields()")
|
@deprecated(info="use note.cloze_numbers_in_fields()")
|
||||||
def _availClozeOrds(
|
def _availClozeOrds(
|
||||||
self, notetype: NotetypeDict, flds: str, allow_empty: bool = True
|
self, notetype: NotetypeDict, flds: str, allow_empty: bool = True
|
||||||
) -> List[int]:
|
) -> list[int]:
|
||||||
import anki.notes_pb2
|
import anki.notes_pb2
|
||||||
|
|
||||||
note = anki.notes_pb2.Note(fields=[flds])
|
note = anki.notes_pb2.Note(fields=[flds])
|
||||||
|
@ -515,11 +515,11 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
self.col.set_config("curModel", m["id"])
|
self.col.set_config("curModel", m["id"])
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
def all_names(self) -> List[str]:
|
def all_names(self) -> list[str]:
|
||||||
return [n.name for n in self.all_names_and_ids()]
|
return [n.name for n in self.all_names_and_ids()]
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
def ids(self) -> List[NotetypeId]:
|
def ids(self) -> list[NotetypeId]:
|
||||||
return [NotetypeId(n.id) for n in self.all_names_and_ids()]
|
return [NotetypeId(n.id) for n in self.all_names_and_ids()]
|
||||||
|
|
||||||
@deprecated(info="no longer required")
|
@deprecated(info="no longer required")
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import Any, List, NewType, Optional, Sequence, Tuple, Union
|
from typing import Any, NewType, Sequence
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki import hooks, notes_pb2
|
from anki import hooks, notes_pb2
|
||||||
|
@ -33,8 +33,8 @@ class Note(DeprecatedNamesMixin):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
col: anki.collection.Collection,
|
col: anki.collection.Collection,
|
||||||
model: Optional[Union[NotetypeDict, NotetypeId]] = None,
|
model: NotetypeDict | NotetypeId | None = None,
|
||||||
id: Optional[NoteId] = None,
|
id: NoteId | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert not (model and id)
|
assert not (model and id)
|
||||||
notetype_id = model["id"] if isinstance(model, dict) else model
|
notetype_id = model["id"] if isinstance(model, dict) else model
|
||||||
|
@ -119,13 +119,13 @@ class Note(DeprecatedNamesMixin):
|
||||||
card._note = self
|
card._note = self
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def cards(self) -> List[anki.cards.Card]:
|
def cards(self) -> list[anki.cards.Card]:
|
||||||
return [self.col.getCard(id) for id in self.card_ids()]
|
return [self.col.getCard(id) for id in self.card_ids()]
|
||||||
|
|
||||||
def card_ids(self) -> Sequence[anki.cards.CardId]:
|
def card_ids(self) -> Sequence[anki.cards.CardId]:
|
||||||
return self.col.card_ids_of_note(self.id)
|
return self.col.card_ids_of_note(self.id)
|
||||||
|
|
||||||
def note_type(self) -> Optional[NotetypeDict]:
|
def note_type(self) -> NotetypeDict | None:
|
||||||
return self.col.models.get(self.mid)
|
return self.col.models.get(self.mid)
|
||||||
|
|
||||||
_note_type = property(note_type)
|
_note_type = property(note_type)
|
||||||
|
@ -136,13 +136,13 @@ class Note(DeprecatedNamesMixin):
|
||||||
# Dict interface
|
# Dict interface
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def keys(self) -> List[str]:
|
def keys(self) -> list[str]:
|
||||||
return list(self._fmap.keys())
|
return list(self._fmap.keys())
|
||||||
|
|
||||||
def values(self) -> List[str]:
|
def values(self) -> list[str]:
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
def items(self) -> List[Tuple[str, str]]:
|
def items(self) -> list[tuple[str, str]]:
|
||||||
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
|
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
|
||||||
|
|
||||||
def _field_index(self, key: str) -> int:
|
def _field_index(self, key: str) -> int:
|
||||||
|
|
|
@ -15,7 +15,7 @@ BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
|
||||||
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
|
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
|
||||||
|
|
||||||
|
|
||||||
from typing import List, Optional, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from anki import config_pb2
|
from anki import config_pb2
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
|
@ -115,7 +115,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
|
def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
|
||||||
return self.col._backend.restore_buried_and_suspended_cards(ids)
|
return self.col._backend.restore_buried_and_suspended_cards(ids)
|
||||||
|
|
||||||
def unbury_cards(self, ids: List[CardId]) -> OpChanges:
|
def unbury_cards(self, ids: list[CardId]) -> OpChanges:
|
||||||
return self.col._backend.restore_buried_and_suspended_cards(ids)
|
return self.col._backend.restore_buried_and_suspended_cards(ids)
|
||||||
|
|
||||||
def unbury_deck(
|
def unbury_deck(
|
||||||
|
@ -162,12 +162,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
self,
|
self,
|
||||||
card_ids: Sequence[CardId],
|
card_ids: Sequence[CardId],
|
||||||
days: str,
|
days: str,
|
||||||
config_key: Optional[Config.String.V] = None,
|
config_key: Config.String.V | None = None,
|
||||||
) -> OpChanges:
|
) -> OpChanges:
|
||||||
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
||||||
`days` can be of the form '5' or '5..7'
|
`days` can be of the form '5' or '5..7'
|
||||||
If `config_key` is provided, provided days will be remembered in config."""
|
If `config_key` is provided, provided days will be remembered in config."""
|
||||||
key: Optional[config_pb2.OptionalStringConfigKey]
|
key: config_pb2.OptionalStringConfigKey | None
|
||||||
if config_key is not None:
|
if config_key is not None:
|
||||||
key = config_pb2.OptionalStringConfigKey(key=config_key)
|
key = config_pb2.OptionalStringConfigKey(key=config_key)
|
||||||
else:
|
else:
|
||||||
|
@ -179,7 +179,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
config_key=key, # type: ignore
|
config_key=key, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
def resetCards(self, ids: List[CardId]) -> None:
|
def resetCards(self, ids: list[CardId]) -> None:
|
||||||
"Completely reset cards for export."
|
"Completely reset cards for export."
|
||||||
sids = ids2str(ids)
|
sids = ids2str(ids)
|
||||||
assert self.col.db
|
assert self.col.db
|
||||||
|
@ -229,7 +229,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
self.orderCards(did)
|
self.orderCards(did)
|
||||||
|
|
||||||
# for post-import
|
# for post-import
|
||||||
def maybeRandomizeDeck(self, did: Optional[DeckId] = None) -> None:
|
def maybeRandomizeDeck(self, did: DeckId | None = None) -> None:
|
||||||
if not did:
|
if not did:
|
||||||
did = self.col.decks.selected()
|
did = self.col.decks.selected()
|
||||||
conf = self.col.decks.config_dict_for_deck_id(did)
|
conf = self.col.decks.config_dict_for_deck_id(did)
|
||||||
|
@ -240,7 +240,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
# legacy
|
# legacy
|
||||||
def sortCards(
|
def sortCards(
|
||||||
self,
|
self,
|
||||||
cids: List[CardId],
|
cids: list[CardId],
|
||||||
start: int = 1,
|
start: int = 1,
|
||||||
step: int = 1,
|
step: int = 1,
|
||||||
shuffle: bool = False,
|
shuffle: bool = False,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
from typing import List, Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN
|
from anki.consts import CARD_TYPE_RELEARNING, QUEUE_TYPE_DAY_LEARN_RELEARN
|
||||||
|
@ -17,7 +17,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
|
||||||
"Legacy aliases and helpers. These will go away in the future."
|
"Legacy aliases and helpers. These will go away in the future."
|
||||||
|
|
||||||
def reschedCards(
|
def reschedCards(
|
||||||
self, card_ids: List[CardId], min_interval: int, max_interval: int
|
self, card_ids: list[CardId], min_interval: int, max_interval: int
|
||||||
) -> None:
|
) -> None:
|
||||||
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
||||||
self.col.usn(),
|
self.col.usn(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def remFromDyn(self, cids: List[CardId]) -> None:
|
def remFromDyn(self, cids: list[CardId]) -> None:
|
||||||
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
|
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
|
||||||
|
|
||||||
# used by v2 scheduler and some add-ons
|
# used by v2 scheduler and some add-ons
|
||||||
|
@ -104,7 +104,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
||||||
elif type == "time":
|
elif type == "time":
|
||||||
self.update_stats(did, milliseconds_delta=cnt)
|
self.update_stats(did, milliseconds_delta=cnt)
|
||||||
|
|
||||||
def deckDueTree(self) -> List:
|
def deckDueTree(self) -> list:
|
||||||
"List of (base name, did, rev, lrn, new, children)"
|
"List of (base name, did, rev, lrn, new, children)"
|
||||||
print(
|
print(
|
||||||
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
|
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
|
||||||
|
@ -116,7 +116,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
||||||
def _cardConf(self, card: Card) -> DeckConfigDict:
|
def _cardConf(self, card: Card) -> DeckConfigDict:
|
||||||
return self.col.decks.config_dict_for_deck_id(card.did)
|
return self.col.decks.config_dict_for_deck_id(card.did)
|
||||||
|
|
||||||
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
|
def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
|
||||||
return (ivl, ivl)
|
return (ivl, ivl)
|
||||||
|
|
||||||
# simple aliases
|
# simple aliases
|
||||||
|
|
|
@ -8,7 +8,6 @@ from __future__ import annotations
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from heapq import *
|
from heapq import *
|
||||||
from typing import Any, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -93,7 +92,7 @@ class Scheduler(V2):
|
||||||
card.usn = self.col.usn()
|
card.usn = self.col.usn()
|
||||||
card.flush()
|
card.flush()
|
||||||
|
|
||||||
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
|
def counts(self, card: Card | None = None) -> tuple[int, int, int]:
|
||||||
counts = [self.newCount, self.lrnCount, self.revCount]
|
counts = [self.newCount, self.lrnCount, self.revCount]
|
||||||
if card:
|
if card:
|
||||||
idx = self.countIdx(card)
|
idx = self.countIdx(card)
|
||||||
|
@ -127,7 +126,7 @@ class Scheduler(V2):
|
||||||
# Getting the next card
|
# Getting the next card
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _getCard(self) -> Optional[Card]:
|
def _getCard(self) -> Card | None:
|
||||||
"Return the next due card id, or None."
|
"Return the next due card id, or None."
|
||||||
# learning card due?
|
# learning card due?
|
||||||
c = self._getLrnCard()
|
c = self._getLrnCard()
|
||||||
|
@ -179,12 +178,12 @@ and due <= ? limit %d"""
|
||||||
|
|
||||||
def _resetLrn(self) -> None:
|
def _resetLrn(self) -> None:
|
||||||
self._resetLrnCount()
|
self._resetLrnCount()
|
||||||
self._lrnQueue: List[Any] = []
|
self._lrnQueue: list[Any] = []
|
||||||
self._lrnDayQueue: List[Any] = []
|
self._lrnDayQueue: list[Any] = []
|
||||||
self._lrnDids = self.col.decks.active()[:]
|
self._lrnDids = self.col.decks.active()[:]
|
||||||
|
|
||||||
# sub-day learning
|
# sub-day learning
|
||||||
def _fillLrn(self) -> Union[bool, List[Any]]:
|
def _fillLrn(self) -> bool | list[Any]:
|
||||||
if not self.lrnCount:
|
if not self.lrnCount:
|
||||||
return False
|
return False
|
||||||
if self._lrnQueue:
|
if self._lrnQueue:
|
||||||
|
@ -202,7 +201,7 @@ limit %d"""
|
||||||
self._lrnQueue.sort()
|
self._lrnQueue.sort()
|
||||||
return self._lrnQueue
|
return self._lrnQueue
|
||||||
|
|
||||||
def _getLrnCard(self, collapse: bool = False) -> Optional[Card]:
|
def _getLrnCard(self, collapse: bool = False) -> Card | None:
|
||||||
if self._fillLrn():
|
if self._fillLrn():
|
||||||
cutoff = time.time()
|
cutoff = time.time()
|
||||||
if collapse:
|
if collapse:
|
||||||
|
@ -374,7 +373,7 @@ limit %d"""
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
log()
|
log()
|
||||||
|
|
||||||
def removeLrn(self, ids: Optional[List[int]] = None) -> None:
|
def removeLrn(self, ids: list[int] | None = None) -> None:
|
||||||
"Remove cards from the learning queues."
|
"Remove cards from the learning queues."
|
||||||
if ids:
|
if ids:
|
||||||
extra = f" and id in {ids2str(ids)}"
|
extra = f" and id in {ids2str(ids)}"
|
||||||
|
@ -429,7 +428,7 @@ and due <= ? limit ?)""",
|
||||||
return self._deckNewLimit(did, self._deckRevLimitSingle)
|
return self._deckNewLimit(did, self._deckRevLimitSingle)
|
||||||
|
|
||||||
def _resetRev(self) -> None:
|
def _resetRev(self) -> None:
|
||||||
self._revQueue: List[Any] = []
|
self._revQueue: list[Any] = []
|
||||||
self._revDids = self.col.decks.active()[:]
|
self._revDids = self.col.decks.active()[:]
|
||||||
|
|
||||||
def _fillRev(self, recursing: bool = False) -> bool:
|
def _fillRev(self, recursing: bool = False) -> bool:
|
||||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import annotations
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from heapq import *
|
from heapq import *
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
from anki import hooks, scheduler_pb2
|
from anki import hooks, scheduler_pb2
|
||||||
|
@ -23,7 +23,7 @@ CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
|
||||||
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
|
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
|
||||||
|
|
||||||
# legacy type alias
|
# legacy type alias
|
||||||
QueueConfig = Dict[str, Any]
|
QueueConfig = dict[str, Any]
|
||||||
|
|
||||||
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
||||||
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
||||||
|
@ -49,11 +49,11 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
self.reps = 0
|
self.reps = 0
|
||||||
self._haveQueues = False
|
self._haveQueues = False
|
||||||
self._lrnCutoff = 0
|
self._lrnCutoff = 0
|
||||||
self._active_decks: List[DeckId] = []
|
self._active_decks: list[DeckId] = []
|
||||||
self._current_deck_id = DeckId(1)
|
self._current_deck_id = DeckId(1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_decks(self) -> List[DeckId]:
|
def active_decks(self) -> list[DeckId]:
|
||||||
"Caller must make sure to make a copy."
|
"Caller must make sure to make a copy."
|
||||||
return self._active_decks
|
return self._active_decks
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
self.revCount = node.review_count
|
self.revCount = node.review_count
|
||||||
self._immediate_learn_count = node.learn_count
|
self._immediate_learn_count = node.learn_count
|
||||||
|
|
||||||
def getCard(self) -> Optional[Card]:
|
def getCard(self) -> Card | None:
|
||||||
"""Pop the next card from the queue. None if finished."""
|
"""Pop the next card from the queue. None if finished."""
|
||||||
self._checkDay()
|
self._checkDay()
|
||||||
if not self._haveQueues:
|
if not self._haveQueues:
|
||||||
|
@ -109,7 +109,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
return card
|
return card
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _getCard(self) -> Optional[Card]:
|
def _getCard(self) -> Card | None:
|
||||||
"""Return the next due card, or None."""
|
"""Return the next due card, or None."""
|
||||||
# learning card due?
|
# learning card due?
|
||||||
c = self._getLrnCard()
|
c = self._getLrnCard()
|
||||||
|
@ -153,7 +153,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
|
|
||||||
def _resetNew(self) -> None:
|
def _resetNew(self) -> None:
|
||||||
self._newDids = self.col.decks.active()[:]
|
self._newDids = self.col.decks.active()[:]
|
||||||
self._newQueue: List[CardId] = []
|
self._newQueue: list[CardId] = []
|
||||||
self._updateNewCardRatio()
|
self._updateNewCardRatio()
|
||||||
|
|
||||||
def _fillNew(self, recursing: bool = False) -> bool:
|
def _fillNew(self, recursing: bool = False) -> bool:
|
||||||
|
@ -188,7 +188,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
self._resetNew()
|
self._resetNew()
|
||||||
return self._fillNew(recursing=True)
|
return self._fillNew(recursing=True)
|
||||||
|
|
||||||
def _getNewCard(self) -> Optional[Card]:
|
def _getNewCard(self) -> Card | None:
|
||||||
if self._fillNew():
|
if self._fillNew():
|
||||||
self.newCount -= 1
|
self.newCount -= 1
|
||||||
return self.col.getCard(self._newQueue.pop())
|
return self.col.getCard(self._newQueue.pop())
|
||||||
|
@ -204,7 +204,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
return
|
return
|
||||||
self.newCardModulus = 0
|
self.newCardModulus = 0
|
||||||
|
|
||||||
def _timeForNewCard(self) -> Optional[bool]:
|
def _timeForNewCard(self) -> bool | None:
|
||||||
"True if it's time to display a new card when distributing."
|
"True if it's time to display a new card when distributing."
|
||||||
if not self.newCount:
|
if not self.newCount:
|
||||||
return False
|
return False
|
||||||
|
@ -219,7 +219,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _deckNewLimit(
|
def _deckNewLimit(
|
||||||
self, did: DeckId, fn: Optional[Callable[[DeckDict], int]] = None
|
self, did: DeckId, fn: Callable[[DeckDict], int] | None = None
|
||||||
) -> int:
|
) -> int:
|
||||||
if not fn:
|
if not fn:
|
||||||
fn = self._deckNewLimitSingle
|
fn = self._deckNewLimitSingle
|
||||||
|
@ -310,12 +310,12 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
|
||||||
def _resetLrn(self) -> None:
|
def _resetLrn(self) -> None:
|
||||||
self._updateLrnCutoff(force=True)
|
self._updateLrnCutoff(force=True)
|
||||||
self._resetLrnCount()
|
self._resetLrnCount()
|
||||||
self._lrnQueue: List[Tuple[int, CardId]] = []
|
self._lrnQueue: list[tuple[int, CardId]] = []
|
||||||
self._lrnDayQueue: List[CardId] = []
|
self._lrnDayQueue: list[CardId] = []
|
||||||
self._lrnDids = self.col.decks.active()[:]
|
self._lrnDids = self.col.decks.active()[:]
|
||||||
|
|
||||||
# sub-day learning
|
# sub-day learning
|
||||||
def _fillLrn(self) -> Union[bool, List[Any]]:
|
def _fillLrn(self) -> bool | list[Any]:
|
||||||
if not self.lrnCount:
|
if not self.lrnCount:
|
||||||
return False
|
return False
|
||||||
if self._lrnQueue:
|
if self._lrnQueue:
|
||||||
|
@ -329,12 +329,12 @@ limit %d"""
|
||||||
% (self._deckLimit(), self.reportLimit),
|
% (self._deckLimit(), self.reportLimit),
|
||||||
cutoff,
|
cutoff,
|
||||||
)
|
)
|
||||||
self._lrnQueue = [cast(Tuple[int, CardId], tuple(e)) for e in self._lrnQueue]
|
self._lrnQueue = [cast(tuple[int, CardId], tuple(e)) for e in self._lrnQueue]
|
||||||
# as it arrives sorted by did first, we need to sort it
|
# as it arrives sorted by did first, we need to sort it
|
||||||
self._lrnQueue.sort()
|
self._lrnQueue.sort()
|
||||||
return self._lrnQueue
|
return self._lrnQueue
|
||||||
|
|
||||||
def _getLrnCard(self, collapse: bool = False) -> Optional[Card]:
|
def _getLrnCard(self, collapse: bool = False) -> Card | None:
|
||||||
self._maybeResetLrn(force=collapse and self.lrnCount == 0)
|
self._maybeResetLrn(force=collapse and self.lrnCount == 0)
|
||||||
if self._fillLrn():
|
if self._fillLrn():
|
||||||
cutoff = time.time()
|
cutoff = time.time()
|
||||||
|
@ -348,7 +348,7 @@ limit %d"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# daily learning
|
# daily learning
|
||||||
def _fillLrnDay(self) -> Optional[bool]:
|
def _fillLrnDay(self) -> bool | None:
|
||||||
if not self.lrnCount:
|
if not self.lrnCount:
|
||||||
return False
|
return False
|
||||||
if self._lrnDayQueue:
|
if self._lrnDayQueue:
|
||||||
|
@ -378,7 +378,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
|
||||||
# shouldn't reach here
|
# shouldn't reach here
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _getLrnDayCard(self) -> Optional[Card]:
|
def _getLrnDayCard(self) -> Card | None:
|
||||||
if self._fillLrnDay():
|
if self._fillLrnDay():
|
||||||
self.lrnCount -= 1
|
self.lrnCount -= 1
|
||||||
return self.col.getCard(self._lrnDayQueue.pop())
|
return self.col.getCard(self._lrnDayQueue.pop())
|
||||||
|
@ -391,7 +391,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
|
||||||
d = self.col.decks.get(self.col.decks.selected(), default=False)
|
d = self.col.decks.get(self.col.decks.selected(), default=False)
|
||||||
return self._deckRevLimitSingle(d)
|
return self._deckRevLimitSingle(d)
|
||||||
|
|
||||||
def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int:
|
def _deckRevLimitSingle(self, d: dict[str, Any]) -> int:
|
||||||
# invalid deck selected?
|
# invalid deck selected?
|
||||||
if not d:
|
if not d:
|
||||||
return 0
|
return 0
|
||||||
|
@ -405,7 +405,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
|
||||||
return hooks.scheduler_review_limit_for_single_deck(lim, d)
|
return hooks.scheduler_review_limit_for_single_deck(lim, d)
|
||||||
|
|
||||||
def _resetRev(self) -> None:
|
def _resetRev(self) -> None:
|
||||||
self._revQueue: List[CardId] = []
|
self._revQueue: list[CardId] = []
|
||||||
|
|
||||||
def _fillRev(self, recursing: bool = False) -> bool:
|
def _fillRev(self, recursing: bool = False) -> bool:
|
||||||
"True if a review card can be fetched."
|
"True if a review card can be fetched."
|
||||||
|
@ -439,7 +439,7 @@ limit ?"""
|
||||||
self._resetRev()
|
self._resetRev()
|
||||||
return self._fillRev(recursing=True)
|
return self._fillRev(recursing=True)
|
||||||
|
|
||||||
def _getRevCard(self) -> Optional[Card]:
|
def _getRevCard(self) -> Card | None:
|
||||||
if self._fillRev():
|
if self._fillRev():
|
||||||
self.revCount -= 1
|
self.revCount -= 1
|
||||||
return self.col.getCard(self._revQueue.pop())
|
return self.col.getCard(self._revQueue.pop())
|
||||||
|
@ -601,7 +601,7 @@ limit ?"""
|
||||||
self._rescheduleLrnCard(card, conf, delay=delay)
|
self._rescheduleLrnCard(card, conf, delay=delay)
|
||||||
|
|
||||||
def _rescheduleLrnCard(
|
def _rescheduleLrnCard(
|
||||||
self, card: Card, conf: QueueConfig, delay: Optional[int] = None
|
self, card: Card, conf: QueueConfig, delay: int | None = None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
# normal delay for the current step?
|
# normal delay for the current step?
|
||||||
if delay is None:
|
if delay is None:
|
||||||
|
@ -690,9 +690,9 @@ limit ?"""
|
||||||
|
|
||||||
def _leftToday(
|
def _leftToday(
|
||||||
self,
|
self,
|
||||||
delays: List[int],
|
delays: list[int],
|
||||||
left: int,
|
left: int,
|
||||||
now: Optional[int] = None,
|
now: int | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"The number of steps that can be completed by the day cutoff."
|
"The number of steps that can be completed by the day cutoff."
|
||||||
if not now:
|
if not now:
|
||||||
|
@ -927,7 +927,7 @@ limit ?"""
|
||||||
min, max = self._fuzzIvlRange(ivl)
|
min, max = self._fuzzIvlRange(ivl)
|
||||||
return random.randint(min, max)
|
return random.randint(min, max)
|
||||||
|
|
||||||
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
|
def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
|
||||||
if ivl < 2:
|
if ivl < 2:
|
||||||
return (1, 1)
|
return (1, 1)
|
||||||
elif ivl == 2:
|
elif ivl == 2:
|
||||||
|
@ -1080,7 +1080,7 @@ limit ?"""
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _burySiblings(self, card: Card) -> None:
|
def _burySiblings(self, card: Card) -> None:
|
||||||
toBury: List[CardId] = []
|
toBury: list[CardId] = []
|
||||||
nconf = self._newConf(card)
|
nconf = self._newConf(card)
|
||||||
buryNew = nconf.get("bury", True)
|
buryNew = nconf.get("bury", True)
|
||||||
rconf = self._revConf(card)
|
rconf = self._revConf(card)
|
||||||
|
@ -1115,7 +1115,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
||||||
# Review-related UI helpers
|
# Review-related UI helpers
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
|
def counts(self, card: Card | None = None) -> tuple[int, int, int]:
|
||||||
counts = [self.newCount, self.lrnCount, self.revCount]
|
counts = [self.newCount, self.lrnCount, self.revCount]
|
||||||
if card:
|
if card:
|
||||||
idx = self.countIdx(card)
|
idx = self.countIdx(card)
|
||||||
|
|
|
@ -12,7 +12,7 @@ as '2' internally.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Literal, Sequence, Tuple
|
from typing import Literal, Optional, Sequence
|
||||||
|
|
||||||
from anki import scheduler_pb2
|
from anki import scheduler_pb2
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
@ -113,7 +113,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
"Don't use this, it is a stop-gap until this code is refactored."
|
"Don't use this, it is a stop-gap until this code is refactored."
|
||||||
return not self.get_queued_cards().cards
|
return not self.get_queued_cards().cards
|
||||||
|
|
||||||
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
|
def counts(self, card: Optional[Card] = None) -> tuple[int, int, int]:
|
||||||
info = self.get_queued_cards()
|
info = self.get_queued_cards()
|
||||||
return (info.new_count, info.learning_count, info.review_count)
|
return (info.new_count, info.learning_count, info.review_count)
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
|
|
||||||
# called by col.decks.active(), which add-ons are using
|
# called by col.decks.active(), which add-ons are using
|
||||||
@property
|
@property
|
||||||
def active_decks(self) -> List[DeckId]:
|
def active_decks(self) -> list[DeckId]:
|
||||||
try:
|
try:
|
||||||
return self.col.db.list("select id from active_decks")
|
return self.col.db.list("select id from active_decks")
|
||||||
except DBError:
|
except DBError:
|
||||||
|
|
|
@ -11,7 +11,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -23,10 +23,10 @@ class TTSTag:
|
||||||
|
|
||||||
field_text: str
|
field_text: str
|
||||||
lang: str
|
lang: str
|
||||||
voices: List[str]
|
voices: list[str]
|
||||||
speed: float
|
speed: float
|
||||||
# each arg should be in the form 'foo=bar'
|
# each arg should be in the form 'foo=bar'
|
||||||
other_args: List[str]
|
other_args: list[str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import annotations
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Sequence
|
||||||
|
|
||||||
import anki.cards
|
import anki.cards
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
@ -37,12 +37,12 @@ class CardStats:
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
|
|
||||||
def addLine(self, k: str, v: Union[int, str]) -> None:
|
def addLine(self, k: str, v: int | str) -> None:
|
||||||
self.txt += self.makeLine(k, v)
|
self.txt += self.makeLine(k, v)
|
||||||
|
|
||||||
def makeLine(self, k: str, v: Union[str, int]) -> str:
|
def makeLine(self, k: str, v: str | int) -> str:
|
||||||
txt = "<tr><td align=left style='padding-right: 3px;'>"
|
txt = "<tr><td align=left style='padding-right: 3px;'>"
|
||||||
txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v)
|
txt += f"<b>{k}</b></td><td>{v}</td></tr>"
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def date(self, tm: float) -> str:
|
def date(self, tm: float) -> str:
|
||||||
|
@ -183,7 +183,7 @@ from revlog where id > ? """
|
||||||
# Due and cumulative due
|
# Due and cumulative due
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def get_start_end_chunk(self, by: str = "review") -> Tuple[int, Optional[int], int]:
|
def get_start_end_chunk(self, by: str = "review") -> tuple[int, int | None, int]:
|
||||||
start = 0
|
start = 0
|
||||||
if self.type == PERIOD_MONTH:
|
if self.type == PERIOD_MONTH:
|
||||||
end, chunk = 31, 1
|
end, chunk = 31, 1
|
||||||
|
@ -245,7 +245,7 @@ from revlog where id > ? """
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def _dueInfo(self, tot: int, num: int) -> str:
|
def _dueInfo(self, tot: int, num: int) -> str:
|
||||||
i: List[str] = []
|
i: list[str] = []
|
||||||
self._line(
|
self._line(
|
||||||
i,
|
i,
|
||||||
"Total",
|
"Total",
|
||||||
|
@ -264,7 +264,7 @@ and due = ?"""
|
||||||
return self._lineTbl(i)
|
return self._lineTbl(i)
|
||||||
|
|
||||||
def _due(
|
def _due(
|
||||||
self, start: Optional[int] = None, end: Optional[int] = None, chunk: int = 1
|
self, start: int | None = None, end: int | None = None, chunk: int = 1
|
||||||
) -> Any:
|
) -> Any:
|
||||||
lim = ""
|
lim = ""
|
||||||
if start is not None:
|
if start is not None:
|
||||||
|
@ -293,7 +293,7 @@ group by day order by day"""
|
||||||
data = self._added(days, chunk)
|
data = self._added(days, chunk)
|
||||||
if not data:
|
if not data:
|
||||||
return ""
|
return ""
|
||||||
conf: Dict[str, Any] = dict(
|
conf: dict[str, Any] = dict(
|
||||||
xaxis=dict(tickDecimals=0, max=0.5),
|
xaxis=dict(tickDecimals=0, max=0.5),
|
||||||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||||
)
|
)
|
||||||
|
@ -311,12 +311,12 @@ group by day order by day"""
|
||||||
txt = self._title("Added", "The number of new cards you have added.")
|
txt = self._title("Added", "The number of new cards you have added.")
|
||||||
txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards")
|
txt += plot("intro", repdata, ylabel="Cards", ylabel2="Cumulative Cards")
|
||||||
# total and per day average
|
# total and per day average
|
||||||
tot = sum([i[1] for i in data])
|
tot = sum(i[1] for i in data)
|
||||||
period = self._periodDays()
|
period = self._periodDays()
|
||||||
if not period:
|
if not period:
|
||||||
# base off date of earliest added card
|
# base off date of earliest added card
|
||||||
period = self._deckAge("add")
|
period = self._deckAge("add")
|
||||||
i: List[str] = []
|
i: list[str] = []
|
||||||
self._line(i, "Total", "%d cards" % tot)
|
self._line(i, "Total", "%d cards" % tot)
|
||||||
self._line(i, "Average", self._avgDay(tot, period, "cards"))
|
self._line(i, "Average", self._avgDay(tot, period, "cards"))
|
||||||
txt += self._lineTbl(i)
|
txt += self._lineTbl(i)
|
||||||
|
@ -328,7 +328,7 @@ group by day order by day"""
|
||||||
data = self._done(days, chunk)
|
data = self._done(days, chunk)
|
||||||
if not data:
|
if not data:
|
||||||
return ""
|
return ""
|
||||||
conf: Dict[str, Any] = dict(
|
conf: dict[str, Any] = dict(
|
||||||
xaxis=dict(tickDecimals=0, max=0.5),
|
xaxis=dict(tickDecimals=0, max=0.5),
|
||||||
yaxes=[dict(min=0), dict(position="right", min=0)],
|
yaxes=[dict(min=0), dict(position="right", min=0)],
|
||||||
)
|
)
|
||||||
|
@ -384,20 +384,20 @@ group by day order by day"""
|
||||||
|
|
||||||
def _ansInfo(
|
def _ansInfo(
|
||||||
self,
|
self,
|
||||||
totd: List[Tuple[int, float]],
|
totd: list[tuple[int, float]],
|
||||||
studied: int,
|
studied: int,
|
||||||
first: int,
|
first: int,
|
||||||
unit: str,
|
unit: str,
|
||||||
convHours: bool = False,
|
convHours: bool = False,
|
||||||
total: Optional[int] = None,
|
total: int | None = None,
|
||||||
) -> Tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
assert totd
|
assert totd
|
||||||
tot = totd[-1][1]
|
tot = totd[-1][1]
|
||||||
period = self._periodDays()
|
period = self._periodDays()
|
||||||
if not period:
|
if not period:
|
||||||
# base off earliest repetition date
|
# base off earliest repetition date
|
||||||
period = self._deckAge("review")
|
period = self._deckAge("review")
|
||||||
i: List[str] = []
|
i: list[str] = []
|
||||||
self._line(
|
self._line(
|
||||||
i,
|
i,
|
||||||
"Days studied",
|
"Days studied",
|
||||||
|
@ -432,12 +432,12 @@ group by day order by day"""
|
||||||
|
|
||||||
def _splitRepData(
|
def _splitRepData(
|
||||||
self,
|
self,
|
||||||
data: List[Tuple[Any, ...]],
|
data: list[tuple[Any, ...]],
|
||||||
spec: Sequence[Tuple[int, str, str]],
|
spec: Sequence[tuple[int, str, str]],
|
||||||
) -> Tuple[List[Dict[str, Any]], List[Tuple[Any, Any]]]:
|
) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
|
||||||
sep: Dict[int, Any] = {}
|
sep: dict[int, Any] = {}
|
||||||
totcnt = {}
|
totcnt = {}
|
||||||
totd: Dict[int, Any] = {}
|
totd: dict[int, Any] = {}
|
||||||
alltot = []
|
alltot = []
|
||||||
allcnt: float = 0
|
allcnt: float = 0
|
||||||
for (n, col, lab) in spec:
|
for (n, col, lab) in spec:
|
||||||
|
@ -471,7 +471,7 @@ group by day order by day"""
|
||||||
)
|
)
|
||||||
return (ret, alltot)
|
return (ret, alltot)
|
||||||
|
|
||||||
def _added(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
|
def _added(self, num: int | None = 7, chunk: int = 1) -> Any:
|
||||||
lims = []
|
lims = []
|
||||||
if num is not None:
|
if num is not None:
|
||||||
lims.append(
|
lims.append(
|
||||||
|
@ -498,7 +498,7 @@ group by day order by day"""
|
||||||
chunk,
|
chunk,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _done(self, num: Optional[int] = 7, chunk: int = 1) -> Any:
|
def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
|
||||||
lims = []
|
lims = []
|
||||||
if num is not None:
|
if num is not None:
|
||||||
lims.append(
|
lims.append(
|
||||||
|
@ -605,12 +605,12 @@ group by day order by day)"""
|
||||||
yaxes=[dict(), dict(position="right", max=105)],
|
yaxes=[dict(), dict(position="right", max=105)],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
i: List[str] = []
|
i: list[str] = []
|
||||||
self._line(i, "Average interval", self.col.format_timespan(avg * 86400))
|
self._line(i, "Average interval", self.col.format_timespan(avg * 86400))
|
||||||
self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400))
|
self._line(i, "Longest interval", self.col.format_timespan(max_ * 86400))
|
||||||
return txt + self._lineTbl(i)
|
return txt + self._lineTbl(i)
|
||||||
|
|
||||||
def _ivls(self) -> Tuple[List[Any], int]:
|
def _ivls(self) -> tuple[list[Any], int]:
|
||||||
start, end, chunk = self.get_start_end_chunk()
|
start, end, chunk = self.get_start_end_chunk()
|
||||||
lim = "and grp <= %d" % end if end else ""
|
lim = "and grp <= %d" % end if end else ""
|
||||||
data = [
|
data = [
|
||||||
|
@ -643,7 +643,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
||||||
# 3 + 4 + 4 + spaces on sides and middle = 15
|
# 3 + 4 + 4 + spaces on sides and middle = 15
|
||||||
# yng starts at 1+3+1 = 5
|
# yng starts at 1+3+1 = 5
|
||||||
# mtr starts at 5+4+1 = 10
|
# mtr starts at 5+4+1 = 10
|
||||||
d: Dict[str, List] = {"lrn": [], "yng": [], "mtr": []}
|
d: dict[str, list] = {"lrn": [], "yng": [], "mtr": []}
|
||||||
types = ("lrn", "yng", "mtr")
|
types = ("lrn", "yng", "mtr")
|
||||||
eases = self._eases()
|
eases = self._eases()
|
||||||
for (type, ease, cnt) in eases:
|
for (type, ease, cnt) in eases:
|
||||||
|
@ -685,7 +685,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
||||||
txt += self._easeInfo(eases)
|
txt += self._easeInfo(eases)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def _easeInfo(self, eases: List[Tuple[int, int, int]]) -> str:
|
def _easeInfo(self, eases: list[tuple[int, int, int]]) -> str:
|
||||||
types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
|
types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
|
||||||
for (type, ease, cnt) in eases:
|
for (type, ease, cnt) in eases:
|
||||||
if ease == 1:
|
if ease == 1:
|
||||||
|
@ -752,7 +752,7 @@ order by thetype, ease"""
|
||||||
shifted = []
|
shifted = []
|
||||||
counts = []
|
counts = []
|
||||||
mcount = 0
|
mcount = 0
|
||||||
trend: List[Tuple[int, int]] = []
|
trend: list[tuple[int, int]] = []
|
||||||
peak = 0
|
peak = 0
|
||||||
for d in data:
|
for d in data:
|
||||||
hour = (d[0] - 4) % 24
|
hour = (d[0] - 4) % 24
|
||||||
|
@ -852,9 +852,9 @@ group by hour having count() > 30 order by hour"""
|
||||||
("Suspended+Buried", colSusp),
|
("Suspended+Buried", colSusp),
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col))
|
d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col))
|
||||||
# text data
|
# text data
|
||||||
i: List[str] = []
|
i: list[str] = []
|
||||||
(c, f) = self.col.db.first(
|
(c, f) = self.col.db.first(
|
||||||
"""
|
"""
|
||||||
select count(id), count(distinct nid) from cards
|
select count(id), count(distinct nid) from cards
|
||||||
|
@ -880,9 +880,7 @@ when you answer "good" on a review."""
|
||||||
)
|
)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def _line(
|
def _line(self, i: list[str], a: str, b: int | str, bold: bool = True) -> None:
|
||||||
self, i: List[str], a: str, b: Union[int, str], bold: bool = True
|
|
||||||
) -> None:
|
|
||||||
# T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
|
# T: Symbols separating first and second column in a statistics table. Eg in "Total: 3 reviews".
|
||||||
colon = ":"
|
colon = ":"
|
||||||
if bold:
|
if bold:
|
||||||
|
@ -896,7 +894,7 @@ when you answer "good" on a review."""
|
||||||
% (a, colon, b)
|
% (a, colon, b)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _lineTbl(self, i: List[str]) -> str:
|
def _lineTbl(self, i: list[str]) -> str:
|
||||||
return "<table width=400>" + "".join(i) + "</table>"
|
return "<table width=400>" + "".join(i) + "</table>"
|
||||||
|
|
||||||
def _factors(self) -> Any:
|
def _factors(self) -> Any:
|
||||||
|
@ -945,7 +943,7 @@ from cards where did in %s"""
|
||||||
self,
|
self,
|
||||||
id: str,
|
id: str,
|
||||||
data: Any,
|
data: Any,
|
||||||
conf: Optional[Any] = None,
|
conf: Any | None = None,
|
||||||
type: str = "bars",
|
type: str = "bars",
|
||||||
xunit: int = 1,
|
xunit: int = 1,
|
||||||
ylabel: str = "Cards",
|
ylabel: str = "Cards",
|
||||||
|
@ -1069,7 +1067,7 @@ $(function () {
|
||||||
)
|
)
|
||||||
|
|
||||||
def _title(self, title: str, subtitle: str = "") -> str:
|
def _title(self, title: str, subtitle: str = "") -> str:
|
||||||
return "<h1>%s</h1>%s" % (title, subtitle)
|
return f"<h1>{title}</h1>{subtitle}"
|
||||||
|
|
||||||
def _deckAge(self, by: str) -> int:
|
def _deckAge(self, by: str) -> int:
|
||||||
lim = self._revlogLimit()
|
lim = self._revlogLimit()
|
||||||
|
@ -1089,7 +1087,7 @@ $(function () {
|
||||||
period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400)))
|
period = max(1, int(1 + ((self.col.sched.dayCutoff - (t / 1000)) / 86400)))
|
||||||
return period
|
return period
|
||||||
|
|
||||||
def _periodDays(self) -> Optional[int]:
|
def _periodDays(self) -> int | None:
|
||||||
start, end, chunk = self.get_start_end_chunk()
|
start, end, chunk = self.get_start_end_chunk()
|
||||||
if end is None:
|
if end is None:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, List, Tuple
|
from typing import Any, Callable
|
||||||
|
|
||||||
import anki.collection
|
import anki.collection
|
||||||
import anki.models
|
import anki.models
|
||||||
|
@ -15,7 +15,7 @@ StockNotetypeKind = notetypes_pb2.StockNotetype.Kind
|
||||||
|
|
||||||
# add-on authors can add ("note type name", function)
|
# add-on authors can add ("note type name", function)
|
||||||
# to this list to have it shown in the add/clone note type screen
|
# to this list to have it shown in the add/clone note type screen
|
||||||
models: List[Tuple] = []
|
models: list[tuple] = []
|
||||||
|
|
||||||
|
|
||||||
def _get_stock_notetype(
|
def _get_stock_notetype(
|
||||||
|
@ -26,9 +26,9 @@ def _get_stock_notetype(
|
||||||
|
|
||||||
def get_stock_notetypes(
|
def get_stock_notetypes(
|
||||||
col: anki.collection.Collection,
|
col: anki.collection.Collection,
|
||||||
) -> List[Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]:
|
) -> list[tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]]:
|
||||||
out: List[
|
out: list[
|
||||||
Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
|
tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
|
||||||
] = []
|
] = []
|
||||||
# add standard
|
# add standard
|
||||||
for kind in [
|
for kind in [
|
||||||
|
|
|
@ -116,7 +116,7 @@ def after_full_sync() -> None:
|
||||||
|
|
||||||
def get_method(
|
def get_method(
|
||||||
method_str: str,
|
method_str: str,
|
||||||
) -> Optional[SyncServerMethodRequest.Method.V]: # pylint: disable=no-member
|
) -> SyncServerMethodRequest.Method.V | None: # pylint: disable=no-member
|
||||||
s = method_str
|
s = method_str
|
||||||
if s == "hostKey":
|
if s == "hostKey":
|
||||||
return Method.HOST_KEY
|
return Method.HOST_KEY
|
||||||
|
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
import re
|
||||||
from typing import Collection, List, Match, Optional, Sequence
|
from typing import Collection, Match, Sequence
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
@ -33,7 +33,7 @@ class TagManager:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
|
|
||||||
# legacy add-on code expects a List return type
|
# legacy add-on code expects a List return type
|
||||||
def all(self) -> List[str]:
|
def all(self) -> list[str]:
|
||||||
return list(self.col._backend.all_tags())
|
return list(self.col._backend.all_tags())
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -50,7 +50,7 @@ class TagManager:
|
||||||
def clear_unused_tags(self) -> OpChangesWithCount:
|
def clear_unused_tags(self) -> OpChangesWithCount:
|
||||||
return self.col._backend.clear_unused_tags()
|
return self.col._backend.clear_unused_tags()
|
||||||
|
|
||||||
def byDeck(self, did: DeckId, children: bool = False) -> List[str]:
|
def byDeck(self, did: DeckId, children: bool = False) -> list[str]:
|
||||||
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
||||||
if not children:
|
if not children:
|
||||||
query = f"{basequery} AND c.did=?"
|
query = f"{basequery} AND c.did=?"
|
||||||
|
@ -123,11 +123,11 @@ class TagManager:
|
||||||
# String-based utilities
|
# String-based utilities
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def split(self, tags: str) -> List[str]:
|
def split(self, tags: str) -> list[str]:
|
||||||
"Parse a string and return a list of tags."
|
"Parse a string and return a list of tags."
|
||||||
return [t for t in tags.replace("\u3000", " ").split(" ") if t]
|
return [t for t in tags.replace("\u3000", " ").split(" ") if t]
|
||||||
|
|
||||||
def join(self, tags: List[str]) -> str:
|
def join(self, tags: list[str]) -> str:
|
||||||
"Join tags into a single string, with leading and trailing spaces."
|
"Join tags into a single string, with leading and trailing spaces."
|
||||||
if not tags:
|
if not tags:
|
||||||
return ""
|
return ""
|
||||||
|
@ -164,30 +164,30 @@ class TagManager:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# this is now a no-op - the tags are canonified when the note is saved
|
# this is now a no-op - the tags are canonified when the note is saved
|
||||||
def canonify(self, tagList: List[str]) -> List[str]:
|
def canonify(self, tagList: list[str]) -> list[str]:
|
||||||
return tagList
|
return tagList
|
||||||
|
|
||||||
def inList(self, tag: str, tags: List[str]) -> bool:
|
def inList(self, tag: str, tags: list[str]) -> bool:
|
||||||
"True if TAG is in TAGS. Ignore case."
|
"True if TAG is in TAGS. Ignore case."
|
||||||
return tag.lower() in [t.lower() for t in tags]
|
return tag.lower() in [t.lower() for t in tags]
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def registerNotes(self, nids: Optional[List[int]] = None) -> None:
|
def registerNotes(self, nids: list[int] | None = None) -> None:
|
||||||
self.clear_unused_tags()
|
self.clear_unused_tags()
|
||||||
|
|
||||||
def register(
|
def register(
|
||||||
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
|
self, tags: Collection[str], usn: int | None = None, clear: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
print("tags.register() is deprecated and no longer works")
|
print("tags.register() is deprecated and no longer works")
|
||||||
|
|
||||||
def bulkAdd(self, ids: List[NoteId], tags: str, add: bool = True) -> None:
|
def bulkAdd(self, ids: list[NoteId], tags: str, add: bool = True) -> None:
|
||||||
"Add tags in bulk. TAGS is space-separated."
|
"Add tags in bulk. TAGS is space-separated."
|
||||||
if add:
|
if add:
|
||||||
self.bulk_add(ids, tags)
|
self.bulk_add(ids, tags)
|
||||||
else:
|
else:
|
||||||
self.bulk_remove(ids, tags)
|
self.bulk_remove(ids, tags)
|
||||||
|
|
||||||
def bulkRem(self, ids: List[NoteId], tags: str) -> None:
|
def bulkRem(self, ids: list[NoteId], tags: str) -> None:
|
||||||
self.bulkAdd(ids, tags, False)
|
self.bulkAdd(ids, tags, False)
|
||||||
|
|
|
@ -29,7 +29,7 @@ template_legacy.py file, using the legacy addHook() system.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Sequence, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import card_rendering_pb2, hooks
|
from anki import card_rendering_pb2, hooks
|
||||||
|
@ -50,10 +50,10 @@ CARD_BLANK_HELP = (
|
||||||
class TemplateReplacement:
|
class TemplateReplacement:
|
||||||
field_name: str
|
field_name: str
|
||||||
current_text: str
|
current_text: str
|
||||||
filters: List[str]
|
filters: list[str]
|
||||||
|
|
||||||
|
|
||||||
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
TemplateReplacementList = list[Union[str, TemplateReplacement]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -105,7 +105,7 @@ def av_tag_to_native(tag: card_rendering_pb2.AVTag) -> AVTag:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> List[AVTag]:
|
def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> list[AVTag]:
|
||||||
return list(map(av_tag_to_native, tags))
|
return list(map(av_tag_to_native, tags))
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class TemplateRenderContext:
|
||||||
note: Note,
|
note: Note,
|
||||||
card: Card,
|
card: Card,
|
||||||
notetype: NotetypeDict,
|
notetype: NotetypeDict,
|
||||||
template: Dict,
|
template: dict,
|
||||||
fill_empty: bool,
|
fill_empty: bool,
|
||||||
) -> TemplateRenderContext:
|
) -> TemplateRenderContext:
|
||||||
return TemplateRenderContext(
|
return TemplateRenderContext(
|
||||||
|
@ -144,7 +144,7 @@ class TemplateRenderContext:
|
||||||
note: Note,
|
note: Note,
|
||||||
browser: bool = False,
|
browser: bool = False,
|
||||||
notetype: NotetypeDict = None,
|
notetype: NotetypeDict = None,
|
||||||
template: Optional[Dict] = None,
|
template: dict | None = None,
|
||||||
fill_empty: bool = False,
|
fill_empty: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._col = col.weakref()
|
self._col = col.weakref()
|
||||||
|
@ -153,7 +153,7 @@ class TemplateRenderContext:
|
||||||
self._browser = browser
|
self._browser = browser
|
||||||
self._template = template
|
self._template = template
|
||||||
self._fill_empty = fill_empty
|
self._fill_empty = fill_empty
|
||||||
self._fields: Optional[Dict] = None
|
self._fields: dict | None = None
|
||||||
self._latex_svg = False
|
self._latex_svg = False
|
||||||
if not notetype:
|
if not notetype:
|
||||||
self._note_type = note.note_type()
|
self._note_type = note.note_type()
|
||||||
|
@ -162,12 +162,12 @@ class TemplateRenderContext:
|
||||||
|
|
||||||
# if you need to store extra state to share amongst rendering
|
# if you need to store extra state to share amongst rendering
|
||||||
# hooks, you can insert it into this dictionary
|
# hooks, you can insert it into this dictionary
|
||||||
self.extra_state: Dict[str, Any] = {}
|
self.extra_state: dict[str, Any] = {}
|
||||||
|
|
||||||
def col(self) -> anki.collection.Collection:
|
def col(self) -> anki.collection.Collection:
|
||||||
return self._col
|
return self._col
|
||||||
|
|
||||||
def fields(self) -> Dict[str, str]:
|
def fields(self) -> dict[str, str]:
|
||||||
print(".fields() is obsolete, use .note() or .card()")
|
print(".fields() is obsolete, use .note() or .card()")
|
||||||
if not self._fields:
|
if not self._fields:
|
||||||
# fields from note
|
# fields from note
|
||||||
|
@ -269,8 +269,8 @@ class TemplateRenderOutput:
|
||||||
"Stores the rendered templates and extracted AV tags."
|
"Stores the rendered templates and extracted AV tags."
|
||||||
question_text: str
|
question_text: str
|
||||||
answer_text: str
|
answer_text: str
|
||||||
question_av_tags: List[AVTag]
|
question_av_tags: list[AVTag]
|
||||||
answer_av_tags: List[AVTag]
|
answer_av_tags: list[AVTag]
|
||||||
css: str = ""
|
css: str = ""
|
||||||
|
|
||||||
def question_and_style(self) -> str:
|
def question_and_style(self) -> str:
|
||||||
|
@ -281,7 +281,7 @@ class TemplateRenderOutput:
|
||||||
|
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
|
def templates_for_card(card: Card, browser: bool) -> tuple[str, str]:
|
||||||
template = card.template()
|
template = card.template()
|
||||||
if browser:
|
if browser:
|
||||||
q, a = template.get("bqfmt"), template.get("bafmt")
|
q, a = template.get("bqfmt"), template.get("bafmt")
|
||||||
|
@ -295,7 +295,7 @@ def templates_for_card(card: Card, browser: bool) -> Tuple[str, str]:
|
||||||
def apply_custom_filters(
|
def apply_custom_filters(
|
||||||
rendered: TemplateReplacementList,
|
rendered: TemplateReplacementList,
|
||||||
ctx: TemplateRenderContext,
|
ctx: TemplateRenderContext,
|
||||||
front_side: Optional[str],
|
front_side: str | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"Complete rendering by applying any pending custom filters."
|
"Complete rendering by applying any pending custom filters."
|
||||||
# template already fully rendered?
|
# template already fully rendered?
|
||||||
|
|
|
@ -17,11 +17,11 @@ import time
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from typing import Any, Iterable, Iterator, List, Optional, Union
|
from typing import Any, Iterable, Iterator
|
||||||
|
|
||||||
from anki.dbproxy import DBProxy
|
from anki.dbproxy import DBProxy
|
||||||
|
|
||||||
_tmpdir: Optional[str]
|
_tmpdir: str | None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint: disable=c-extension-no-member
|
# pylint: disable=c-extension-no-member
|
||||||
|
@ -89,7 +89,7 @@ def htmlToTextLine(s: str) -> str:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def ids2str(ids: Iterable[Union[int, str]]) -> str:
|
def ids2str(ids: Iterable[int | str]) -> str:
|
||||||
"""Given a list of integers, return a string '(int1,int2,...)'."""
|
"""Given a list of integers, return a string '(int1,int2,...)'."""
|
||||||
return f"({','.join(str(i) for i in ids)})"
|
return f"({','.join(str(i) for i in ids)})"
|
||||||
|
|
||||||
|
@ -140,11 +140,11 @@ def guid64() -> str:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def joinFields(list: List[str]) -> str:
|
def joinFields(list: list[str]) -> str:
|
||||||
return "\x1f".join(list)
|
return "\x1f".join(list)
|
||||||
|
|
||||||
|
|
||||||
def splitFields(string: str) -> List[str]:
|
def splitFields(string: str) -> list[str]:
|
||||||
return string.split("\x1f")
|
return string.split("\x1f")
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ def splitFields(string: str) -> List[str]:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def checksum(data: Union[bytes, str]) -> str:
|
def checksum(data: bytes | str) -> str:
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode("utf-8")
|
data = data.encode("utf-8")
|
||||||
return sha1(data).hexdigest()
|
return sha1(data).hexdigest()
|
||||||
|
@ -218,7 +218,7 @@ def noBundledLibs() -> Iterator[None]:
|
||||||
os.environ["LD_LIBRARY_PATH"] = oldlpath
|
os.environ["LD_LIBRARY_PATH"] = oldlpath
|
||||||
|
|
||||||
|
|
||||||
def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int:
|
def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
|
||||||
"Execute a command. If WAIT, return exit code."
|
"Execute a command. If WAIT, return exit code."
|
||||||
# ensure we don't open a separate window for forking process on windows
|
# ensure we don't open a separate window for forking process on windows
|
||||||
if isWin:
|
if isWin:
|
||||||
|
@ -262,7 +262,7 @@ devMode = os.getenv("ANKIDEV", "")
|
||||||
invalidFilenameChars = ':*?"<>|'
|
invalidFilenameChars = ':*?"<>|'
|
||||||
|
|
||||||
|
|
||||||
def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
|
def invalidFilename(str: str, dirsep: bool = True) -> str | None:
|
||||||
for c in invalidFilenameChars:
|
for c in invalidFilenameChars:
|
||||||
if c in str:
|
if c in str:
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -268,7 +268,9 @@ def test_chained_mods():
|
||||||
a1 = "<b>sentence</b>"
|
a1 = "<b>sentence</b>"
|
||||||
q2 = '<span style="color:red">en chaine</span>'
|
q2 = '<span style="color:red">en chaine</span>'
|
||||||
a2 = "<i>chained</i>"
|
a2 = "<i>chained</i>"
|
||||||
note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
|
note[
|
||||||
|
"Text"
|
||||||
|
] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format(
|
||||||
q1,
|
q1,
|
||||||
a1,
|
a1,
|
||||||
q2,
|
q2,
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Tuple
|
from typing import Dict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -433,7 +433,7 @@ def test_reviews():
|
||||||
assert "leech" in c.note().tags
|
assert "leech" in c.note().tags
|
||||||
|
|
||||||
|
|
||||||
def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]:
|
def review_limits_setup() -> tuple[anki.collection.Collection, Dict]:
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
|
|
||||||
parent = col.decks.get(col.decks.id("parent"))
|
parent = col.decks.get(col.decks.id("parent"))
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Hook:
|
||||||
name: str
|
name: str
|
||||||
# string of the typed arguments passed to the callback, eg
|
# string of the typed arguments passed to the callback, eg
|
||||||
# ["kind: str", "val: int"]
|
# ["kind: str", "val: int"]
|
||||||
args: List[str] = None
|
args: list[str] = None
|
||||||
# string of the return type. if set, hook is a filter.
|
# string of the return type. if set, hook is a filter.
|
||||||
return_type: Optional[str] = None
|
return_type: Optional[str] = None
|
||||||
# if add-ons may be relying on the legacy hook name, add it here
|
# if add-ons may be relying on the legacy hook name, add it here
|
||||||
|
@ -41,7 +41,7 @@ class Hook:
|
||||||
types_str = ", ".join(types)
|
types_str = ", ".join(types)
|
||||||
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
||||||
|
|
||||||
def arg_names(self) -> List[str]:
|
def arg_names(self) -> list[str]:
|
||||||
names = []
|
names = []
|
||||||
for arg in self.args or []:
|
for arg in self.args or []:
|
||||||
if not arg:
|
if not arg:
|
||||||
|
@ -64,7 +64,7 @@ class Hook:
|
||||||
|
|
||||||
def list_code(self) -> str:
|
def list_code(self) -> str:
|
||||||
return f"""\
|
return f"""\
|
||||||
_hooks: List[{self.callable()}] = []
|
_hooks: list[{self.callable()}] = []
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def code(self) -> str:
|
def code(self) -> str:
|
||||||
|
@ -153,7 +153,7 @@ class {self.classname()}:
|
||||||
return f"{out}\n\n"
|
return f"{out}\n\n"
|
||||||
|
|
||||||
|
|
||||||
def write_file(path: str, hooks: List[Hook], prefix: str, suffix: str):
|
def write_file(path: str, hooks: list[Hook], prefix: str, suffix: str):
|
||||||
hooks.sort(key=attrgetter("name"))
|
hooks.sort(key=attrgetter("name"))
|
||||||
code = f"{prefix}\n"
|
code = f"{prefix}\n"
|
||||||
for hook in hooks:
|
for hook in hooks:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import builtins
|
import builtins
|
||||||
import cProfile
|
import cProfile
|
||||||
|
@ -10,7 +12,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
|
from typing import Any, Callable, Optional, cast
|
||||||
|
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki._backend import RustBackend
|
from anki._backend import RustBackend
|
||||||
|
@ -90,7 +92,7 @@ from aqt import stats, about, preferences, mediasync # isort:skip
|
||||||
|
|
||||||
class DialogManager:
|
class DialogManager:
|
||||||
|
|
||||||
_dialogs: Dict[str, list] = {
|
_dialogs: dict[str, list] = {
|
||||||
"AddCards": [addcards.AddCards, None],
|
"AddCards": [addcards.AddCards, None],
|
||||||
"AddonsDialog": [addons.AddonsDialog, None],
|
"AddonsDialog": [addons.AddonsDialog, None],
|
||||||
"Browser": [browser.Browser, None],
|
"Browser": [browser.Browser, None],
|
||||||
|
@ -267,7 +269,7 @@ class AnkiApp(QApplication):
|
||||||
KEY = f"anki{checksum(getpass.getuser())}"
|
KEY = f"anki{checksum(getpass.getuser())}"
|
||||||
TMOUT = 30000
|
TMOUT = 30000
|
||||||
|
|
||||||
def __init__(self, argv: List[str]) -> None:
|
def __init__(self, argv: list[str]) -> None:
|
||||||
QApplication.__init__(self, argv)
|
QApplication.__init__(self, argv)
|
||||||
self._argv = argv
|
self._argv = argv
|
||||||
|
|
||||||
|
@ -328,7 +330,7 @@ class AnkiApp(QApplication):
|
||||||
return QApplication.event(self, evt)
|
return QApplication.event(self, evt)
|
||||||
|
|
||||||
|
|
||||||
def parseArgs(argv: List[str]) -> Tuple[argparse.Namespace, List[str]]:
|
def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
||||||
"Returns (opts, args)."
|
"Returns (opts, args)."
|
||||||
# py2app fails to strip this in some instances, then anki dies
|
# py2app fails to strip this in some instances, then anki dies
|
||||||
# as there's no such profile
|
# as there's no such profile
|
||||||
|
@ -444,7 +446,7 @@ def run() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run(argv: Optional[List[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
|
def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
|
||||||
"""Start AnkiQt application or reuse an existing instance if one exists.
|
"""Start AnkiQt application or reuse an existing instance if one exists.
|
||||||
|
|
||||||
If the function is invoked with exec=False, the AnkiQt will not enter
|
If the function is invoked with exec=False, the AnkiQt will not enter
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -50,7 +50,7 @@ class AddCards(QDialog):
|
||||||
self.setupEditor()
|
self.setupEditor()
|
||||||
self.setupButtons()
|
self.setupButtons()
|
||||||
self._load_new_note()
|
self._load_new_note()
|
||||||
self.history: List[NoteId] = []
|
self.history: list[NoteId] = []
|
||||||
self._last_added_note: Optional[Note] = None
|
self._last_added_note: Optional[Note] = None
|
||||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||||
restoreGeom(self, "add")
|
restoreGeom(self, "add")
|
||||||
|
|
150
qt/aqt/addons.py
150
qt/aqt/addons.py
|
@ -11,7 +11,7 @@ from collections import defaultdict
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import IO, Any, Callable, Iterable, Union
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class AbortAddonImport(Exception):
|
||||||
@dataclass
|
@dataclass
|
||||||
class InstallOk:
|
class InstallOk:
|
||||||
name: str
|
name: str
|
||||||
conflicts: List[str]
|
conflicts: list[str]
|
||||||
compatible: bool
|
compatible: bool
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,13 +75,13 @@ class DownloadOk:
|
||||||
@dataclass
|
@dataclass
|
||||||
class DownloadError:
|
class DownloadError:
|
||||||
# set if result was not 200
|
# set if result was not 200
|
||||||
status_code: Optional[int] = None
|
status_code: int | None = None
|
||||||
# set if an exception occurred
|
# set if an exception occurred
|
||||||
exception: Optional[Exception] = None
|
exception: Exception | None = None
|
||||||
|
|
||||||
|
|
||||||
# first arg is add-on id
|
# first arg is add-on id
|
||||||
DownloadLogEntry = Tuple[int, Union[DownloadError, InstallError, InstallOk]]
|
DownloadLogEntry = tuple[int, Union[DownloadError, InstallError, InstallOk]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -101,21 +101,21 @@ current_point_version = anki.utils.pointVersion()
|
||||||
@dataclass
|
@dataclass
|
||||||
class AddonMeta:
|
class AddonMeta:
|
||||||
dir_name: str
|
dir_name: str
|
||||||
provided_name: Optional[str]
|
provided_name: str | None
|
||||||
enabled: bool
|
enabled: bool
|
||||||
installed_at: int
|
installed_at: int
|
||||||
conflicts: List[str]
|
conflicts: list[str]
|
||||||
min_point_version: int
|
min_point_version: int
|
||||||
max_point_version: int
|
max_point_version: int
|
||||||
branch_index: int
|
branch_index: int
|
||||||
human_version: Optional[str]
|
human_version: str | None
|
||||||
update_enabled: bool
|
update_enabled: bool
|
||||||
homepage: Optional[str]
|
homepage: str | None
|
||||||
|
|
||||||
def human_name(self) -> str:
|
def human_name(self) -> str:
|
||||||
return self.provided_name or self.dir_name
|
return self.provided_name or self.dir_name
|
||||||
|
|
||||||
def ankiweb_id(self) -> Optional[int]:
|
def ankiweb_id(self) -> int | None:
|
||||||
m = ANKIWEB_ID_RE.match(self.dir_name)
|
m = ANKIWEB_ID_RE.match(self.dir_name)
|
||||||
if m:
|
if m:
|
||||||
return int(m.group(0))
|
return int(m.group(0))
|
||||||
|
@ -134,13 +134,13 @@ class AddonMeta:
|
||||||
def is_latest(self, server_update_time: int) -> bool:
|
def is_latest(self, server_update_time: int) -> bool:
|
||||||
return self.installed_at >= server_update_time
|
return self.installed_at >= server_update_time
|
||||||
|
|
||||||
def page(self) -> Optional[str]:
|
def page(self) -> str | None:
|
||||||
if self.ankiweb_id():
|
if self.ankiweb_id():
|
||||||
return f"{aqt.appShared}info/{self.dir_name}"
|
return f"{aqt.appShared}info/{self.dir_name}"
|
||||||
return self.homepage
|
return self.homepage
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json_meta(dir_name: str, json_meta: Dict[str, Any]) -> AddonMeta:
|
def from_json_meta(dir_name: str, json_meta: dict[str, Any]) -> AddonMeta:
|
||||||
return AddonMeta(
|
return AddonMeta(
|
||||||
dir_name=dir_name,
|
dir_name=dir_name,
|
||||||
provided_name=json_meta.get("name"),
|
provided_name=json_meta.get("name"),
|
||||||
|
@ -207,7 +207,7 @@ class AddonManager:
|
||||||
sys.path.insert(0, self.addonsFolder())
|
sys.path.insert(0, self.addonsFolder())
|
||||||
|
|
||||||
# in new code, you may want all_addon_meta() instead
|
# in new code, you may want all_addon_meta() instead
|
||||||
def allAddons(self) -> List[str]:
|
def allAddons(self) -> list[str]:
|
||||||
l = []
|
l = []
|
||||||
for d in os.listdir(self.addonsFolder()):
|
for d in os.listdir(self.addonsFolder()):
|
||||||
path = self.addonsFolder(d)
|
path = self.addonsFolder(d)
|
||||||
|
@ -222,7 +222,7 @@ class AddonManager:
|
||||||
def all_addon_meta(self) -> Iterable[AddonMeta]:
|
def all_addon_meta(self) -> Iterable[AddonMeta]:
|
||||||
return map(self.addon_meta, self.allAddons())
|
return map(self.addon_meta, self.allAddons())
|
||||||
|
|
||||||
def addonsFolder(self, dir: Optional[str] = None) -> str:
|
def addonsFolder(self, dir: str | None = None) -> str:
|
||||||
root = self.mw.pm.addonFolder()
|
root = self.mw.pm.addonFolder()
|
||||||
if dir is None:
|
if dir is None:
|
||||||
return root
|
return root
|
||||||
|
@ -280,7 +280,7 @@ class AddonManager:
|
||||||
return os.path.join(self.addonsFolder(dir), "meta.json")
|
return os.path.join(self.addonsFolder(dir), "meta.json")
|
||||||
|
|
||||||
# in new code, use self.addon_meta() instead
|
# in new code, use self.addon_meta() instead
|
||||||
def addonMeta(self, dir: str) -> Dict[str, Any]:
|
def addonMeta(self, dir: str) -> dict[str, Any]:
|
||||||
path = self._addonMetaPath(dir)
|
path = self._addonMetaPath(dir)
|
||||||
try:
|
try:
|
||||||
with open(path, encoding="utf8") as f:
|
with open(path, encoding="utf8") as f:
|
||||||
|
@ -293,12 +293,12 @@ class AddonManager:
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
# in new code, use write_addon_meta() instead
|
# in new code, use write_addon_meta() instead
|
||||||
def writeAddonMeta(self, dir: str, meta: Dict[str, Any]) -> None:
|
def writeAddonMeta(self, dir: str, meta: dict[str, Any]) -> None:
|
||||||
path = self._addonMetaPath(dir)
|
path = self._addonMetaPath(dir)
|
||||||
with open(path, "w", encoding="utf8") as f:
|
with open(path, "w", encoding="utf8") as f:
|
||||||
json.dump(meta, f)
|
json.dump(meta, f)
|
||||||
|
|
||||||
def toggleEnabled(self, dir: str, enable: Optional[bool] = None) -> None:
|
def toggleEnabled(self, dir: str, enable: bool | None = None) -> None:
|
||||||
addon = self.addon_meta(dir)
|
addon = self.addon_meta(dir)
|
||||||
should_enable = enable if enable is not None else not addon.enabled
|
should_enable = enable if enable is not None else not addon.enabled
|
||||||
if should_enable is True:
|
if should_enable is True:
|
||||||
|
@ -316,7 +316,7 @@ class AddonManager:
|
||||||
addon.enabled = should_enable
|
addon.enabled = should_enable
|
||||||
self.write_addon_meta(addon)
|
self.write_addon_meta(addon)
|
||||||
|
|
||||||
def ankiweb_addons(self) -> List[int]:
|
def ankiweb_addons(self) -> list[int]:
|
||||||
ids = []
|
ids = []
|
||||||
for meta in self.all_addon_meta():
|
for meta in self.all_addon_meta():
|
||||||
if meta.ankiweb_id() is not None:
|
if meta.ankiweb_id() is not None:
|
||||||
|
@ -332,7 +332,7 @@ class AddonManager:
|
||||||
def addonName(self, dir: str) -> str:
|
def addonName(self, dir: str) -> str:
|
||||||
return self.addon_meta(dir).human_name()
|
return self.addon_meta(dir).human_name()
|
||||||
|
|
||||||
def addonConflicts(self, dir: str) -> List[str]:
|
def addonConflicts(self, dir: str) -> list[str]:
|
||||||
return self.addon_meta(dir).conflicts
|
return self.addon_meta(dir).conflicts
|
||||||
|
|
||||||
def annotatedName(self, dir: str) -> str:
|
def annotatedName(self, dir: str) -> str:
|
||||||
|
@ -345,8 +345,8 @@ class AddonManager:
|
||||||
# Conflict resolution
|
# Conflict resolution
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def allAddonConflicts(self) -> Dict[str, List[str]]:
|
def allAddonConflicts(self) -> dict[str, list[str]]:
|
||||||
all_conflicts: Dict[str, List[str]] = defaultdict(list)
|
all_conflicts: dict[str, list[str]] = defaultdict(list)
|
||||||
for addon in self.all_addon_meta():
|
for addon in self.all_addon_meta():
|
||||||
if not addon.enabled:
|
if not addon.enabled:
|
||||||
continue
|
continue
|
||||||
|
@ -354,7 +354,7 @@ class AddonManager:
|
||||||
all_conflicts[other_dir].append(addon.dir_name)
|
all_conflicts[other_dir].append(addon.dir_name)
|
||||||
return all_conflicts
|
return all_conflicts
|
||||||
|
|
||||||
def _disableConflicting(self, dir: str, conflicts: List[str] = None) -> List[str]:
|
def _disableConflicting(self, dir: str, conflicts: list[str] = None) -> list[str]:
|
||||||
conflicts = conflicts or self.addonConflicts(dir)
|
conflicts = conflicts or self.addonConflicts(dir)
|
||||||
|
|
||||||
installed = self.allAddons()
|
installed = self.allAddons()
|
||||||
|
@ -371,7 +371,7 @@ class AddonManager:
|
||||||
# Installing and deleting add-ons
|
# Installing and deleting add-ons
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def readManifestFile(self, zfile: ZipFile) -> Dict[Any, Any]:
|
def readManifestFile(self, zfile: ZipFile) -> dict[Any, Any]:
|
||||||
try:
|
try:
|
||||||
with zfile.open("manifest.json") as f:
|
with zfile.open("manifest.json") as f:
|
||||||
data = json.loads(f.read())
|
data = json.loads(f.read())
|
||||||
|
@ -385,8 +385,8 @@ class AddonManager:
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
def install(
|
def install(
|
||||||
self, file: Union[IO, str], manifest: Dict[str, Any] = None
|
self, file: IO | str, manifest: dict[str, Any] = None
|
||||||
) -> Union[InstallOk, InstallError]:
|
) -> InstallOk | InstallError:
|
||||||
"""Install add-on from path or file-like object. Metadata is read
|
"""Install add-on from path or file-like object. Metadata is read
|
||||||
from the manifest file, with keys overriden by supplying a 'manifest'
|
from the manifest file, with keys overriden by supplying a 'manifest'
|
||||||
dictionary"""
|
dictionary"""
|
||||||
|
@ -463,8 +463,8 @@ class AddonManager:
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def processPackages(
|
def processPackages(
|
||||||
self, paths: List[str], parent: QWidget = None
|
self, paths: list[str], parent: QWidget = None
|
||||||
) -> Tuple[List[str], List[str]]:
|
) -> tuple[list[str], list[str]]:
|
||||||
|
|
||||||
log = []
|
log = []
|
||||||
errs = []
|
errs = []
|
||||||
|
@ -493,7 +493,7 @@ class AddonManager:
|
||||||
|
|
||||||
def _installationErrorReport(
|
def _installationErrorReport(
|
||||||
self, result: InstallError, base: str, mode: str = "download"
|
self, result: InstallError, base: str, mode: str = "download"
|
||||||
) -> List[str]:
|
) -> list[str]:
|
||||||
|
|
||||||
messages = {
|
messages = {
|
||||||
"zip": tr.addons_corrupt_addon_file(),
|
"zip": tr.addons_corrupt_addon_file(),
|
||||||
|
@ -511,7 +511,7 @@ class AddonManager:
|
||||||
|
|
||||||
def _installationSuccessReport(
|
def _installationSuccessReport(
|
||||||
self, result: InstallOk, base: str, mode: str = "download"
|
self, result: InstallOk, base: str, mode: str = "download"
|
||||||
) -> List[str]:
|
) -> list[str]:
|
||||||
|
|
||||||
name = result.name or base
|
name = result.name or base
|
||||||
if mode == "download":
|
if mode == "download":
|
||||||
|
@ -536,8 +536,8 @@ class AddonManager:
|
||||||
# Updating
|
# Updating
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def extract_update_info(self, items: List[Dict]) -> List[UpdateInfo]:
|
def extract_update_info(self, items: list[dict]) -> list[UpdateInfo]:
|
||||||
def extract_one(item: Dict) -> UpdateInfo:
|
def extract_one(item: dict) -> UpdateInfo:
|
||||||
id = item["id"]
|
id = item["id"]
|
||||||
meta = self.addon_meta(str(id))
|
meta = self.addon_meta(str(id))
|
||||||
branch_idx = meta.branch_index
|
branch_idx = meta.branch_index
|
||||||
|
@ -545,7 +545,7 @@ class AddonManager:
|
||||||
|
|
||||||
return list(map(extract_one, items))
|
return list(map(extract_one, items))
|
||||||
|
|
||||||
def update_supported_versions(self, items: List[UpdateInfo]) -> None:
|
def update_supported_versions(self, items: list[UpdateInfo]) -> None:
|
||||||
for item in items:
|
for item in items:
|
||||||
self.update_supported_version(item)
|
self.update_supported_version(item)
|
||||||
|
|
||||||
|
@ -581,7 +581,7 @@ class AddonManager:
|
||||||
if updated:
|
if updated:
|
||||||
self.write_addon_meta(addon)
|
self.write_addon_meta(addon)
|
||||||
|
|
||||||
def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]:
|
def updates_required(self, items: list[UpdateInfo]) -> list[UpdateInfo]:
|
||||||
"""Return ids of add-ons requiring an update."""
|
"""Return ids of add-ons requiring an update."""
|
||||||
need_update = []
|
need_update = []
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -600,10 +600,10 @@ class AddonManager:
|
||||||
# Add-on Config
|
# Add-on Config
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_configButtonActions: Dict[str, Callable[[], Optional[bool]]] = {}
|
_configButtonActions: dict[str, Callable[[], bool | None]] = {}
|
||||||
_configUpdatedActions: Dict[str, Callable[[Any], None]] = {}
|
_configUpdatedActions: dict[str, Callable[[Any], None]] = {}
|
||||||
|
|
||||||
def addonConfigDefaults(self, dir: str) -> Optional[Dict[str, Any]]:
|
def addonConfigDefaults(self, dir: str) -> dict[str, Any] | None:
|
||||||
path = os.path.join(self.addonsFolder(dir), "config.json")
|
path = os.path.join(self.addonsFolder(dir), "config.json")
|
||||||
try:
|
try:
|
||||||
with open(path, encoding="utf8") as f:
|
with open(path, encoding="utf8") as f:
|
||||||
|
@ -622,7 +622,7 @@ class AddonManager:
|
||||||
def addonFromModule(self, module: str) -> str:
|
def addonFromModule(self, module: str) -> str:
|
||||||
return module.split(".")[0]
|
return module.split(".")[0]
|
||||||
|
|
||||||
def configAction(self, addon: str) -> Callable[[], Optional[bool]]:
|
def configAction(self, addon: str) -> Callable[[], bool | None]:
|
||||||
return self._configButtonActions.get(addon)
|
return self._configButtonActions.get(addon)
|
||||||
|
|
||||||
def configUpdatedAction(self, addon: str) -> Callable[[Any], None]:
|
def configUpdatedAction(self, addon: str) -> Callable[[Any], None]:
|
||||||
|
@ -649,7 +649,7 @@ class AddonManager:
|
||||||
# Add-on Config API
|
# Add-on Config API
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def getConfig(self, module: str) -> Optional[Dict[str, Any]]:
|
def getConfig(self, module: str) -> dict[str, Any] | None:
|
||||||
addon = self.addonFromModule(module)
|
addon = self.addonFromModule(module)
|
||||||
# get default config
|
# get default config
|
||||||
config = self.addonConfigDefaults(addon)
|
config = self.addonConfigDefaults(addon)
|
||||||
|
@ -661,7 +661,7 @@ class AddonManager:
|
||||||
config.update(userConf)
|
config.update(userConf)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def setConfigAction(self, module: str, fn: Callable[[], Optional[bool]]) -> None:
|
def setConfigAction(self, module: str, fn: Callable[[], bool | None]) -> None:
|
||||||
addon = self.addonFromModule(module)
|
addon = self.addonFromModule(module)
|
||||||
self._configButtonActions[addon] = fn
|
self._configButtonActions[addon] = fn
|
||||||
|
|
||||||
|
@ -700,7 +700,7 @@ class AddonManager:
|
||||||
# Web Exports
|
# Web Exports
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_webExports: Dict[str, str] = {}
|
_webExports: dict[str, str] = {}
|
||||||
|
|
||||||
def setWebExports(self, module: str, pattern: str) -> None:
|
def setWebExports(self, module: str, pattern: str) -> None:
|
||||||
addon = self.addonFromModule(module)
|
addon = self.addonFromModule(module)
|
||||||
|
@ -825,18 +825,18 @@ class AddonsDialog(QDialog):
|
||||||
gui_hooks.addons_dialog_did_change_selected_addon(self, addon)
|
gui_hooks.addons_dialog_did_change_selected_addon(self, addon)
|
||||||
return
|
return
|
||||||
|
|
||||||
def selectedAddons(self) -> List[str]:
|
def selectedAddons(self) -> list[str]:
|
||||||
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
|
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
|
||||||
return [self.addons[idx].dir_name for idx in idxs]
|
return [self.addons[idx].dir_name for idx in idxs]
|
||||||
|
|
||||||
def onlyOneSelected(self) -> Optional[str]:
|
def onlyOneSelected(self) -> str | None:
|
||||||
dirs = self.selectedAddons()
|
dirs = self.selectedAddons()
|
||||||
if len(dirs) != 1:
|
if len(dirs) != 1:
|
||||||
showInfo(tr.addons_please_select_a_single_addon_first())
|
showInfo(tr.addons_please_select_a_single_addon_first())
|
||||||
return None
|
return None
|
||||||
return dirs[0]
|
return dirs[0]
|
||||||
|
|
||||||
def selected_addon_meta(self) -> Optional[AddonMeta]:
|
def selected_addon_meta(self) -> AddonMeta | None:
|
||||||
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
|
idxs = [x.row() for x in self.form.addonList.selectedIndexes()]
|
||||||
if len(idxs) != 1:
|
if len(idxs) != 1:
|
||||||
showInfo(tr.addons_please_select_a_single_addon_first())
|
showInfo(tr.addons_please_select_a_single_addon_first())
|
||||||
|
@ -887,14 +887,14 @@ class AddonsDialog(QDialog):
|
||||||
if obj.ids:
|
if obj.ids:
|
||||||
download_addons(self, self.mgr, obj.ids, self.after_downloading)
|
download_addons(self, self.mgr, obj.ids, self.after_downloading)
|
||||||
|
|
||||||
def after_downloading(self, log: List[DownloadLogEntry]) -> None:
|
def after_downloading(self, log: list[DownloadLogEntry]) -> None:
|
||||||
self.redrawAddons()
|
self.redrawAddons()
|
||||||
if log:
|
if log:
|
||||||
show_log_to_user(self, log)
|
show_log_to_user(self, log)
|
||||||
else:
|
else:
|
||||||
tooltip(tr.addons_no_updates_available())
|
tooltip(tr.addons_no_updates_available())
|
||||||
|
|
||||||
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
|
def onInstallFiles(self, paths: list[str] | None = None) -> bool | None:
|
||||||
if not paths:
|
if not paths:
|
||||||
key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})"
|
key = f"{tr.addons_packaged_anki_addon()} (*{self.mgr.ext})"
|
||||||
paths_ = getFile(
|
paths_ = getFile(
|
||||||
|
@ -943,7 +943,7 @@ class GetAddons(QDialog):
|
||||||
self.addonsDlg = dlg
|
self.addonsDlg = dlg
|
||||||
self.mgr = dlg.mgr
|
self.mgr = dlg.mgr
|
||||||
self.mw = self.mgr.mw
|
self.mw = self.mgr.mw
|
||||||
self.ids: List[int] = []
|
self.ids: list[int] = []
|
||||||
self.form = aqt.forms.getaddons.Ui_Dialog()
|
self.form = aqt.forms.getaddons.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
b = self.form.buttonBox.addButton(
|
b = self.form.buttonBox.addButton(
|
||||||
|
@ -974,7 +974,7 @@ class GetAddons(QDialog):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def download_addon(client: HttpClient, id: int) -> Union[DownloadOk, DownloadError]:
|
def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
|
||||||
"Fetch a single add-on from AnkiWeb."
|
"Fetch a single add-on from AnkiWeb."
|
||||||
try:
|
try:
|
||||||
resp = client.get(
|
resp = client.get(
|
||||||
|
@ -1025,7 +1025,7 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
|
|
||||||
def download_log_to_html(log: List[DownloadLogEntry]) -> str:
|
def download_log_to_html(log: list[DownloadLogEntry]) -> str:
|
||||||
return "<br>".join(map(describe_log_entry, log))
|
return "<br>".join(map(describe_log_entry, log))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1053,7 +1053,7 @@ def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def download_encountered_problem(log: List[DownloadLogEntry]) -> bool:
|
def download_encountered_problem(log: list[DownloadLogEntry]) -> bool:
|
||||||
return any(not isinstance(e[1], InstallOk) for e in log)
|
return any(not isinstance(e[1], InstallOk) for e in log)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1099,10 +1099,10 @@ class DownloaderInstaller(QObject):
|
||||||
self.client.progress_hook = bg_thread_progress
|
self.client.progress_hook = bg_thread_progress
|
||||||
|
|
||||||
def download(
|
def download(
|
||||||
self, ids: List[int], on_done: Callable[[List[DownloadLogEntry]], None]
|
self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None]
|
||||||
) -> None:
|
) -> None:
|
||||||
self.ids = ids
|
self.ids = ids
|
||||||
self.log: List[DownloadLogEntry] = []
|
self.log: list[DownloadLogEntry] = []
|
||||||
|
|
||||||
self.dl_bytes = 0
|
self.dl_bytes = 0
|
||||||
self.last_tooltip = 0
|
self.last_tooltip = 0
|
||||||
|
@ -1135,7 +1135,7 @@ class DownloaderInstaller(QObject):
|
||||||
self.mgr.mw.progress.timer(50, lambda: self.on_done(self.log), False)
|
self.mgr.mw.progress.timer(50, lambda: self.on_done(self.log), False)
|
||||||
|
|
||||||
|
|
||||||
def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
|
def show_log_to_user(parent: QWidget, log: list[DownloadLogEntry]) -> None:
|
||||||
have_problem = download_encountered_problem(log)
|
have_problem = download_encountered_problem(log)
|
||||||
|
|
||||||
if have_problem:
|
if have_problem:
|
||||||
|
@ -1153,9 +1153,9 @@ def show_log_to_user(parent: QWidget, log: List[DownloadLogEntry]) -> None:
|
||||||
def download_addons(
|
def download_addons(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
ids: List[int],
|
ids: list[int],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||||
client: Optional[HttpClient] = None,
|
client: HttpClient | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if client is None:
|
if client is None:
|
||||||
client = HttpClient()
|
client = HttpClient()
|
||||||
|
@ -1174,7 +1174,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
||||||
self,
|
self,
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
updated_addons: List[UpdateInfo],
|
updated_addons: list[UpdateInfo],
|
||||||
) -> None:
|
) -> None:
|
||||||
QListWidget.__init__(self, parent)
|
QListWidget.__init__(self, parent)
|
||||||
self.mgr = mgr
|
self.mgr = mgr
|
||||||
|
@ -1266,7 +1266,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
||||||
return
|
return
|
||||||
self.check_item(self.header_item, Qt.Checked)
|
self.check_item(self.header_item, Qt.Checked)
|
||||||
|
|
||||||
def get_selected_addon_ids(self) -> List[int]:
|
def get_selected_addon_ids(self) -> list[int]:
|
||||||
addon_ids = []
|
addon_ids = []
|
||||||
for i in range(1, self.count()):
|
for i in range(1, self.count()):
|
||||||
item = self.item(i)
|
item = self.item(i)
|
||||||
|
@ -1286,7 +1286,7 @@ class ChooseAddonsToUpdateList(QListWidget):
|
||||||
|
|
||||||
class ChooseAddonsToUpdateDialog(QDialog):
|
class ChooseAddonsToUpdateDialog(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo]
|
self, parent: QWidget, mgr: AddonManager, updated_addons: list[UpdateInfo]
|
||||||
) -> None:
|
) -> None:
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setWindowTitle(tr.addons_choose_update_window_title())
|
self.setWindowTitle(tr.addons_choose_update_window_title())
|
||||||
|
@ -1312,7 +1312,7 @@ class ChooseAddonsToUpdateDialog(QDialog):
|
||||||
layout.addWidget(button_box)
|
layout.addWidget(button_box)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def ask(self) -> List[int]:
|
def ask(self) -> list[int]:
|
||||||
"Returns a list of selected addons' ids"
|
"Returns a list of selected addons' ids"
|
||||||
ret = self.exec_()
|
ret = self.exec_()
|
||||||
saveGeom(self, "addonsChooseUpdate")
|
saveGeom(self, "addonsChooseUpdate")
|
||||||
|
@ -1323,9 +1323,9 @@ class ChooseAddonsToUpdateDialog(QDialog):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
def fetch_update_info(client: HttpClient, ids: list[int]) -> list[dict]:
|
||||||
"""Fetch update info from AnkiWeb in one or more batches."""
|
"""Fetch update info from AnkiWeb in one or more batches."""
|
||||||
all_info: List[Dict] = []
|
all_info: list[dict] = []
|
||||||
|
|
||||||
while ids:
|
while ids:
|
||||||
# get another chunk
|
# get another chunk
|
||||||
|
@ -1340,7 +1340,7 @@ def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]:
|
||||||
|
|
||||||
def _fetch_update_info_batch(
|
def _fetch_update_info_batch(
|
||||||
client: HttpClient, chunk: Iterable[str]
|
client: HttpClient, chunk: Iterable[str]
|
||||||
) -> Iterable[Dict]:
|
) -> Iterable[dict]:
|
||||||
"""Get update info from AnkiWeb.
|
"""Get update info from AnkiWeb.
|
||||||
|
|
||||||
Chunk must not contain more than 25 ids."""
|
Chunk must not contain more than 25 ids."""
|
||||||
|
@ -1354,21 +1354,21 @@ def _fetch_update_info_batch(
|
||||||
def check_and_prompt_for_updates(
|
def check_and_prompt_for_updates(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||||
requested_by_user: bool = True,
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
def on_updates_received(client: HttpClient, items: List[Dict]) -> None:
|
def on_updates_received(client: HttpClient, items: list[dict]) -> None:
|
||||||
handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
|
handle_update_info(parent, mgr, client, items, on_done, requested_by_user)
|
||||||
|
|
||||||
check_for_updates(mgr, on_updates_received)
|
check_for_updates(mgr, on_updates_received)
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates(
|
def check_for_updates(
|
||||||
mgr: AddonManager, on_done: Callable[[HttpClient, List[Dict]], None]
|
mgr: AddonManager, on_done: Callable[[HttpClient, list[dict]], None]
|
||||||
) -> None:
|
) -> None:
|
||||||
client = HttpClient()
|
client = HttpClient()
|
||||||
|
|
||||||
def check() -> List[Dict]:
|
def check() -> list[dict]:
|
||||||
return fetch_update_info(client, mgr.ankiweb_addons())
|
return fetch_update_info(client, mgr.ankiweb_addons())
|
||||||
|
|
||||||
def update_info_received(future: Future) -> None:
|
def update_info_received(future: Future) -> None:
|
||||||
|
@ -1395,7 +1395,7 @@ def check_for_updates(
|
||||||
|
|
||||||
|
|
||||||
def extract_update_info(
|
def extract_update_info(
|
||||||
current_point_version: int, current_branch_idx: int, info_json: Dict
|
current_point_version: int, current_branch_idx: int, info_json: dict
|
||||||
) -> UpdateInfo:
|
) -> UpdateInfo:
|
||||||
"Process branches to determine the updated mod time and min/max versions."
|
"Process branches to determine the updated mod time and min/max versions."
|
||||||
branches = info_json["branches"]
|
branches = info_json["branches"]
|
||||||
|
@ -1425,8 +1425,8 @@ def handle_update_info(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
items: List[Dict],
|
items: list[dict],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||||
requested_by_user: bool = True,
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
update_info = mgr.extract_update_info(items)
|
update_info = mgr.extract_update_info(items)
|
||||||
|
@ -1445,8 +1445,8 @@ def prompt_to_update(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
mgr: AddonManager,
|
mgr: AddonManager,
|
||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
updated_addons: List[UpdateInfo],
|
updated_addons: list[UpdateInfo],
|
||||||
on_done: Callable[[List[DownloadLogEntry]], None],
|
on_done: Callable[[list[DownloadLogEntry]], None],
|
||||||
requested_by_user: bool = True,
|
requested_by_user: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not requested_by_user:
|
if not requested_by_user:
|
||||||
|
@ -1468,7 +1468,7 @@ def prompt_to_update(
|
||||||
|
|
||||||
|
|
||||||
class ConfigEditor(QDialog):
|
class ConfigEditor(QDialog):
|
||||||
def __init__(self, dlg: AddonsDialog, addon: str, conf: Dict) -> None:
|
def __init__(self, dlg: AddonsDialog, addon: str, conf: dict) -> None:
|
||||||
super().__init__(dlg)
|
super().__init__(dlg)
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
|
@ -1509,7 +1509,7 @@ class ConfigEditor(QDialog):
|
||||||
else:
|
else:
|
||||||
self.form.scrollArea.setVisible(False)
|
self.form.scrollArea.setVisible(False)
|
||||||
|
|
||||||
def updateText(self, conf: Dict[str, Any]) -> None:
|
def updateText(self, conf: dict[str, Any]) -> None:
|
||||||
text = json.dumps(
|
text = json.dumps(
|
||||||
conf,
|
conf,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
|
@ -1584,8 +1584,8 @@ class ConfigEditor(QDialog):
|
||||||
|
|
||||||
def installAddonPackages(
|
def installAddonPackages(
|
||||||
addonsManager: AddonManager,
|
addonsManager: AddonManager,
|
||||||
paths: List[str],
|
paths: list[str],
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
warn: bool = False,
|
warn: bool = False,
|
||||||
strictly_modal: bool = False,
|
strictly_modal: bool = False,
|
||||||
advise_restart: bool = False,
|
advise_restart: bool = False,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Sequence
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -89,14 +89,14 @@ class MockModel:
|
||||||
class Browser(QMainWindow):
|
class Browser(QMainWindow):
|
||||||
mw: AnkiQt
|
mw: AnkiQt
|
||||||
col: Collection
|
col: Collection
|
||||||
editor: Optional[Editor]
|
editor: Editor | None
|
||||||
table: Table
|
table: Table
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
card: Optional[Card] = None,
|
card: Card | None = None,
|
||||||
search: Optional[Tuple[Union[str, SearchNode]]] = None,
|
search: tuple[str | SearchNode] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
card -- try to select the provided card after executing "search" or
|
card -- try to select the provided card after executing "search" or
|
||||||
|
@ -108,8 +108,8 @@ class Browser(QMainWindow):
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.col = self.mw.col
|
self.col = self.mw.col
|
||||||
self.lastFilter = ""
|
self.lastFilter = ""
|
||||||
self.focusTo: Optional[int] = None
|
self.focusTo: int | None = None
|
||||||
self._previewer: Optional[Previewer] = None
|
self._previewer: Previewer | None = None
|
||||||
self._closeEventHasCleanedUp = False
|
self._closeEventHasCleanedUp = False
|
||||||
self.form = aqt.forms.browser.Ui_Dialog()
|
self.form = aqt.forms.browser.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
|
@ -119,8 +119,8 @@ class Browser(QMainWindow):
|
||||||
restoreSplitter(self.form.splitter, "editor3")
|
restoreSplitter(self.form.splitter, "editor3")
|
||||||
self.form.splitter.setChildrenCollapsible(False)
|
self.form.splitter.setChildrenCollapsible(False)
|
||||||
# set if exactly 1 row is selected; used by the previewer
|
# set if exactly 1 row is selected; used by the previewer
|
||||||
self.card: Optional[Card] = None
|
self.card: Card | None = None
|
||||||
self.current_card: Optional[Card] = None
|
self.current_card: Card | None = None
|
||||||
self.setup_table()
|
self.setup_table()
|
||||||
self.setupMenus()
|
self.setupMenus()
|
||||||
self.setupHooks()
|
self.setupHooks()
|
||||||
|
@ -134,7 +134,7 @@ class Browser(QMainWindow):
|
||||||
self.setupSearch(card, search)
|
self.setupSearch(card, search)
|
||||||
|
|
||||||
def on_operation_did_execute(
|
def on_operation_did_execute(
|
||||||
self, changes: OpChanges, handler: Optional[object]
|
self, changes: OpChanges, handler: object | None
|
||||||
) -> None:
|
) -> None:
|
||||||
focused = current_window() == self
|
focused = current_window() == self
|
||||||
self.table.op_executed(changes, handler, focused)
|
self.table.op_executed(changes, handler, focused)
|
||||||
|
@ -161,7 +161,7 @@ class Browser(QMainWindow):
|
||||||
if changes.note_text or changes.card:
|
if changes.note_text or changes.card:
|
||||||
self._renderPreview()
|
self._renderPreview()
|
||||||
|
|
||||||
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
def on_focus_change(self, new: QWidget | None, old: QWidget | None) -> None:
|
||||||
if current_window() == self:
|
if current_window() == self:
|
||||||
self.setUpdatesEnabled(True)
|
self.setUpdatesEnabled(True)
|
||||||
self.table.redraw_cells()
|
self.table.redraw_cells()
|
||||||
|
@ -263,8 +263,8 @@ class Browser(QMainWindow):
|
||||||
def reopen(
|
def reopen(
|
||||||
self,
|
self,
|
||||||
_mw: AnkiQt,
|
_mw: AnkiQt,
|
||||||
card: Optional[Card] = None,
|
card: Card | None = None,
|
||||||
search: Optional[Tuple[Union[str, SearchNode]]] = None,
|
search: tuple[str | SearchNode] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if search is not None:
|
if search is not None:
|
||||||
self.search_for_terms(*search)
|
self.search_for_terms(*search)
|
||||||
|
@ -281,8 +281,8 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
def setupSearch(
|
def setupSearch(
|
||||||
self,
|
self,
|
||||||
card: Optional[Card] = None,
|
card: Card | None = None,
|
||||||
search: Optional[Tuple[Union[str, SearchNode]]] = None,
|
search: tuple[str | SearchNode] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
||||||
self.form.searchEdit.setCompleter(None)
|
self.form.searchEdit.setCompleter(None)
|
||||||
|
@ -310,7 +310,7 @@ class Browser(QMainWindow):
|
||||||
self.search_for(normed)
|
self.search_for(normed)
|
||||||
self.update_history()
|
self.update_history()
|
||||||
|
|
||||||
def search_for(self, search: str, prompt: Optional[str] = None) -> None:
|
def search_for(self, search: str, prompt: str | None = None) -> None:
|
||||||
"""Keep track of search string so that we reuse identical search when
|
"""Keep track of search string so that we reuse identical search when
|
||||||
refreshing, rather than whatever is currently in the search field.
|
refreshing, rather than whatever is currently in the search field.
|
||||||
Optionally set the search bar to a different text than the actual search.
|
Optionally set the search bar to a different text than the actual search.
|
||||||
|
@ -354,12 +354,12 @@ class Browser(QMainWindow):
|
||||||
without_unicode_isolation(tr_title(total=cur, selected=selected))
|
without_unicode_isolation(tr_title(total=cur, selected=selected))
|
||||||
)
|
)
|
||||||
|
|
||||||
def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None:
|
def search_for_terms(self, *search_terms: str | SearchNode) -> None:
|
||||||
search = self.col.build_search_string(*search_terms)
|
search = self.col.build_search_string(*search_terms)
|
||||||
self.form.searchEdit.setEditText(search)
|
self.form.searchEdit.setEditText(search)
|
||||||
self.onSearchActivated()
|
self.onSearchActivated()
|
||||||
|
|
||||||
def _default_search(self, card: Optional[Card] = None) -> None:
|
def _default_search(self, card: Card | None = None) -> None:
|
||||||
default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT)
|
default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT)
|
||||||
if default.strip():
|
if default.strip():
|
||||||
search = default
|
search = default
|
||||||
|
@ -674,7 +674,7 @@ class Browser(QMainWindow):
|
||||||
@ensure_editor_saved
|
@ensure_editor_saved
|
||||||
def add_tags_to_selected_notes(
|
def add_tags_to_selected_notes(
|
||||||
self,
|
self,
|
||||||
tags: Optional[str] = None,
|
tags: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"Shows prompt if tags not provided."
|
"Shows prompt if tags not provided."
|
||||||
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())):
|
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())):
|
||||||
|
@ -686,7 +686,7 @@ class Browser(QMainWindow):
|
||||||
@no_arg_trigger
|
@no_arg_trigger
|
||||||
@skip_if_selection_is_empty
|
@skip_if_selection_is_empty
|
||||||
@ensure_editor_saved
|
@ensure_editor_saved
|
||||||
def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None:
|
def remove_tags_from_selected_notes(self, tags: str | None = None) -> None:
|
||||||
"Shows prompt if tags not provided."
|
"Shows prompt if tags not provided."
|
||||||
if not (
|
if not (
|
||||||
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete())
|
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete())
|
||||||
|
@ -697,7 +697,7 @@ class Browser(QMainWindow):
|
||||||
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
|
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
|
||||||
).run_in_background(initiator=self)
|
).run_in_background(initiator=self)
|
||||||
|
|
||||||
def _prompt_for_tags(self, prompt: str) -> Optional[str]:
|
def _prompt_for_tags(self, prompt: str) -> str | None:
|
||||||
(tags, ok) = getTag(self, self.col, prompt)
|
(tags, ok) = getTag(self, self.col, prompt)
|
||||||
if not ok:
|
if not ok:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Optional, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
|
@ -39,7 +39,7 @@ class FindAndReplaceDialog(QDialog):
|
||||||
*,
|
*,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
note_ids: Sequence[NoteId],
|
note_ids: Sequence[NoteId],
|
||||||
field: Optional[str] = None,
|
field: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
If 'field' is passed, only this is added to the field selector.
|
If 'field' is passed, only this is added to the field selector.
|
||||||
|
@ -48,7 +48,7 @@ class FindAndReplaceDialog(QDialog):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.note_ids = note_ids
|
self.note_ids = note_ids
|
||||||
self.field_names: List[str] = []
|
self.field_names: list[str] = []
|
||||||
self._field = field
|
self._field = field
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.find
|
import anki.find
|
||||||
|
@ -45,7 +45,7 @@ class FindDuplicatesDialog(QDialog):
|
||||||
)
|
)
|
||||||
form.fields.addItems(fields)
|
form.fields.addItems(fields)
|
||||||
restore_combo_index_for_session(form.fields, fields, "findDupesFields")
|
restore_combo_index_for_session(form.fields, fields, "findDupesFields")
|
||||||
self._dupesButton: Optional[QPushButton] = None
|
self._dupesButton: QPushButton | None = None
|
||||||
|
|
||||||
# links
|
# links
|
||||||
form.webView.set_title("find duplicates")
|
form.webView.set_title("find duplicates")
|
||||||
|
@ -75,7 +75,7 @@ class FindDuplicatesDialog(QDialog):
|
||||||
qconnect(search.clicked, on_click)
|
qconnect(search.clicked, on_click)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def show_duplicates_report(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
|
def show_duplicates_report(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
|
||||||
if not self._dupesButton:
|
if not self._dupesButton:
|
||||||
self._dupesButton = b = self.form.buttonBox.addButton(
|
self._dupesButton = b = self.form.buttonBox.addButton(
|
||||||
tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole
|
tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole
|
||||||
|
@ -104,7 +104,7 @@ class FindDuplicatesDialog(QDialog):
|
||||||
text += "</ol>"
|
text += "</ol>"
|
||||||
self.form.webView.stdHtml(text, context=self)
|
self.form.webView.stdHtml(text, context=self)
|
||||||
|
|
||||||
def _tag_duplicates(self, dupes: List[Tuple[str, List[NoteId]]]) -> None:
|
def _tag_duplicates(self, dupes: list[tuple[str, list[NoteId]]]) -> None:
|
||||||
if not dupes:
|
if not dupes:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Optional, Tuple, Union
|
from typing import Any, Callable
|
||||||
|
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
@ -33,14 +33,14 @@ from aqt.theme import theme_manager
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
|
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
LastStateAndMod = Tuple[str, int, int]
|
LastStateAndMod = tuple[str, int, int]
|
||||||
|
|
||||||
|
|
||||||
class Previewer(QDialog):
|
class Previewer(QDialog):
|
||||||
_last_state: Optional[LastStateAndMod] = None
|
_last_state: LastStateAndMod | None = None
|
||||||
_card_changed = False
|
_card_changed = False
|
||||||
_last_render: Union[int, float] = 0
|
_last_render: int | float = 0
|
||||||
_timer: Optional[QTimer] = None
|
_timer: QTimer | None = None
|
||||||
_show_both_sides = False
|
_show_both_sides = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -57,7 +57,7 @@ class Previewer(QDialog):
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
self.setWindowIcon(icon)
|
self.setWindowIcon(icon)
|
||||||
|
|
||||||
def card(self) -> Optional[Card]:
|
def card(self) -> Card | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def card_changed(self) -> bool:
|
def card_changed(self) -> bool:
|
||||||
|
@ -143,7 +143,7 @@ class Previewer(QDialog):
|
||||||
if cmd.startswith("play:"):
|
if cmd.startswith("play:"):
|
||||||
play_clicked_audio(cmd, self.card())
|
play_clicked_audio(cmd, self.card())
|
||||||
|
|
||||||
def _update_flag_and_mark_icons(self, card: Optional[Card]) -> None:
|
def _update_flag_and_mark_icons(self, card: Card | None) -> None:
|
||||||
if card:
|
if card:
|
||||||
flag = card.user_flag()
|
flag = card.user_flag()
|
||||||
marked = card.note(reload=True).has_tag(MARKED_TAG)
|
marked = card.note(reload=True).has_tag(MARKED_TAG)
|
||||||
|
@ -247,7 +247,7 @@ class Previewer(QDialog):
|
||||||
self._state = "question"
|
self._state = "question"
|
||||||
self.render_card()
|
self.render_card()
|
||||||
|
|
||||||
def _state_and_mod(self) -> Tuple[str, int, int]:
|
def _state_and_mod(self) -> tuple[str, int, int]:
|
||||||
c = self.card()
|
c = self.card()
|
||||||
n = c.note()
|
n = c.note()
|
||||||
n.load()
|
n.load()
|
||||||
|
@ -258,7 +258,7 @@ class Previewer(QDialog):
|
||||||
|
|
||||||
|
|
||||||
class MultiCardPreviewer(Previewer):
|
class MultiCardPreviewer(Previewer):
|
||||||
def card(self) -> Optional[Card]:
|
def card(self) -> Card | None:
|
||||||
# need to state explicitly it's not implement to avoid W0223
|
# need to state explicitly it's not implement to avoid W0223
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -321,14 +321,14 @@ class MultiCardPreviewer(Previewer):
|
||||||
|
|
||||||
class BrowserPreviewer(MultiCardPreviewer):
|
class BrowserPreviewer(MultiCardPreviewer):
|
||||||
_last_card_id = 0
|
_last_card_id = 0
|
||||||
_parent: Optional[aqt.browser.Browser]
|
_parent: aqt.browser.Browser | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None]
|
self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None]
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent=parent, mw=mw, on_close=on_close)
|
super().__init__(parent=parent, mw=mw, on_close=on_close)
|
||||||
|
|
||||||
def card(self) -> Optional[Card]:
|
def card(self) -> Card | None:
|
||||||
if self._parent.singleCard:
|
if self._parent.singleCard:
|
||||||
return self._parent.card
|
return self._parent.card
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Callable, Iterable, List, Optional, Union
|
from typing import Callable, Iterable
|
||||||
|
|
||||||
from anki.collection import SearchNode
|
from anki.collection import SearchNode
|
||||||
from aqt.theme import ColoredIcon
|
from aqt.theme import ColoredIcon
|
||||||
|
@ -59,8 +59,8 @@ class SidebarItem:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
icon: Union[str, ColoredIcon],
|
icon: str | ColoredIcon,
|
||||||
search_node: Optional[SearchNode] = None,
|
search_node: SearchNode | None = None,
|
||||||
on_expanded: Callable[[bool], None] = None,
|
on_expanded: Callable[[bool], None] = None,
|
||||||
expanded: bool = False,
|
expanded: bool = False,
|
||||||
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
item_type: SidebarItemType = SidebarItemType.CUSTOM,
|
||||||
|
@ -75,24 +75,24 @@ class SidebarItem:
|
||||||
self.id = id
|
self.id = id
|
||||||
self.search_node = search_node
|
self.search_node = search_node
|
||||||
self.on_expanded = on_expanded
|
self.on_expanded = on_expanded
|
||||||
self.children: List["SidebarItem"] = []
|
self.children: list[SidebarItem] = []
|
||||||
self.tooltip: Optional[str] = None
|
self.tooltip: str | None = None
|
||||||
self._parent_item: Optional["SidebarItem"] = None
|
self._parent_item: SidebarItem | None = None
|
||||||
self._expanded = expanded
|
self._expanded = expanded
|
||||||
self._row_in_parent: Optional[int] = None
|
self._row_in_parent: int | None = None
|
||||||
self._search_matches_self = False
|
self._search_matches_self = False
|
||||||
self._search_matches_child = False
|
self._search_matches_child = False
|
||||||
|
|
||||||
def add_child(self, cb: "SidebarItem") -> None:
|
def add_child(self, cb: SidebarItem) -> None:
|
||||||
self.children.append(cb)
|
self.children.append(cb)
|
||||||
cb._parent_item = self
|
cb._parent_item = self
|
||||||
|
|
||||||
def add_simple(
|
def add_simple(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
icon: Union[str, ColoredIcon],
|
icon: str | ColoredIcon,
|
||||||
type: SidebarItemType,
|
type: SidebarItemType,
|
||||||
search_node: Optional[SearchNode],
|
search_node: SearchNode | None,
|
||||||
) -> SidebarItem:
|
) -> SidebarItem:
|
||||||
"Add child sidebar item, and return it."
|
"Add child sidebar item, and return it."
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Callable, Tuple
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -18,7 +17,7 @@ class SidebarTool(Enum):
|
||||||
|
|
||||||
|
|
||||||
class SidebarToolbar(QToolBar):
|
class SidebarToolbar(QToolBar):
|
||||||
_tools: Tuple[Tuple[SidebarTool, str, Callable[[], str]], ...] = (
|
_tools: tuple[tuple[SidebarTool, str, Callable[[], str]], ...] = (
|
||||||
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
|
(SidebarTool.SEARCH, ":/icons/magnifying_glass.svg", tr.actions_search),
|
||||||
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
|
(SidebarTool.SELECT, ":/icons/select.svg", tr.actions_select),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Dict, Iterable, List, Optional, Tuple, cast
|
from typing import Iterable, cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import (
|
from anki.collection import (
|
||||||
|
@ -75,8 +75,8 @@ class SidebarTreeView(QTreeView):
|
||||||
self.browser = browser
|
self.browser = browser
|
||||||
self.mw = browser.mw
|
self.mw = browser.mw
|
||||||
self.col = self.mw.col
|
self.col = self.mw.col
|
||||||
self.current_search: Optional[str] = None
|
self.current_search: str | None = None
|
||||||
self.valid_drop_types: Tuple[SidebarItemType, ...] = ()
|
self.valid_drop_types: tuple[SidebarItemType, ...] = ()
|
||||||
self._refresh_needed = False
|
self._refresh_needed = False
|
||||||
|
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
@ -140,7 +140,7 @@ class SidebarTreeView(QTreeView):
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
if changes.browser_sidebar and not handler is self:
|
if changes.browser_sidebar and not handler is self:
|
||||||
self._refresh_needed = True
|
self._refresh_needed = True
|
||||||
|
@ -198,9 +198,9 @@ class SidebarTreeView(QTreeView):
|
||||||
def find_item(
|
def find_item(
|
||||||
self,
|
self,
|
||||||
is_target: Callable[[SidebarItem], bool],
|
is_target: Callable[[SidebarItem], bool],
|
||||||
parent: Optional[SidebarItem] = None,
|
parent: SidebarItem | None = None,
|
||||||
) -> Optional[SidebarItem]:
|
) -> SidebarItem | None:
|
||||||
def find_item_rec(parent: SidebarItem) -> Optional[SidebarItem]:
|
def find_item_rec(parent: SidebarItem) -> SidebarItem | None:
|
||||||
if is_target(parent):
|
if is_target(parent):
|
||||||
return parent
|
return parent
|
||||||
for child in parent.children:
|
for child in parent.children:
|
||||||
|
@ -226,7 +226,7 @@ class SidebarTreeView(QTreeView):
|
||||||
def _expand_where_necessary(
|
def _expand_where_necessary(
|
||||||
self,
|
self,
|
||||||
model: SidebarModel,
|
model: SidebarModel,
|
||||||
parent: Optional[QModelIndex] = None,
|
parent: QModelIndex | None = None,
|
||||||
searching: bool = False,
|
searching: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
scroll_to_first_match = searching
|
scroll_to_first_match = searching
|
||||||
|
@ -348,7 +348,7 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
self.valid_drop_types = tuple(valid_drop_types)
|
self.valid_drop_types = tuple(valid_drop_types)
|
||||||
|
|
||||||
def handle_drag_drop(self, sources: List[SidebarItem], target: SidebarItem) -> bool:
|
def handle_drag_drop(self, sources: list[SidebarItem], target: SidebarItem) -> bool:
|
||||||
if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
|
if target.item_type in (SidebarItemType.DECK, SidebarItemType.DECK_ROOT):
|
||||||
return self._handle_drag_drop_decks(sources, target)
|
return self._handle_drag_drop_decks(sources, target)
|
||||||
if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT):
|
if target.item_type in (SidebarItemType.TAG, SidebarItemType.TAG_ROOT):
|
||||||
|
@ -361,7 +361,7 @@ class SidebarTreeView(QTreeView):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _handle_drag_drop_decks(
|
def _handle_drag_drop_decks(
|
||||||
self, sources: List[SidebarItem], target: SidebarItem
|
self, sources: list[SidebarItem], target: SidebarItem
|
||||||
) -> bool:
|
) -> bool:
|
||||||
deck_ids = [
|
deck_ids = [
|
||||||
DeckId(source.id)
|
DeckId(source.id)
|
||||||
|
@ -380,7 +380,7 @@ class SidebarTreeView(QTreeView):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_drag_drop_tags(
|
def _handle_drag_drop_tags(
|
||||||
self, sources: List[SidebarItem], target: SidebarItem
|
self, sources: list[SidebarItem], target: SidebarItem
|
||||||
) -> bool:
|
) -> bool:
|
||||||
tags = [
|
tags = [
|
||||||
source.full_name
|
source.full_name
|
||||||
|
@ -402,7 +402,7 @@ class SidebarTreeView(QTreeView):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _handle_drag_drop_saved_search(
|
def _handle_drag_drop_saved_search(
|
||||||
self, sources: List[SidebarItem], _target: SidebarItem
|
self, sources: list[SidebarItem], _target: SidebarItem
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if len(sources) != 1 or sources[0].search_node is None:
|
if len(sources) != 1 or sources[0].search_node is None:
|
||||||
return False
|
return False
|
||||||
|
@ -464,7 +464,7 @@ class SidebarTreeView(QTreeView):
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
def _root_tree(self) -> SidebarItem:
|
def _root_tree(self) -> SidebarItem:
|
||||||
root: Optional[SidebarItem] = None
|
root: SidebarItem | None = None
|
||||||
|
|
||||||
for stage in SidebarStage:
|
for stage in SidebarStage:
|
||||||
if stage == SidebarStage.ROOT:
|
if stage == SidebarStage.ROOT:
|
||||||
|
@ -504,7 +504,7 @@ class SidebarTreeView(QTreeView):
|
||||||
name: str,
|
name: str,
|
||||||
icon: Union[str, ColoredIcon],
|
icon: Union[str, ColoredIcon],
|
||||||
collapse_key: Config.Bool.V,
|
collapse_key: Config.Bool.V,
|
||||||
type: Optional[SidebarItemType] = None,
|
type: SidebarItemType | None = None,
|
||||||
) -> SidebarItem:
|
) -> SidebarItem:
|
||||||
def update(expanded: bool) -> None:
|
def update(expanded: bool) -> None:
|
||||||
CollectionOp(
|
CollectionOp(
|
||||||
|
@ -1112,13 +1112,13 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
_saved_searches_key = "savedFilters"
|
_saved_searches_key = "savedFilters"
|
||||||
|
|
||||||
def _get_saved_searches(self) -> Dict[str, str]:
|
def _get_saved_searches(self) -> dict[str, str]:
|
||||||
return self.col.get_config(self._saved_searches_key, {})
|
return self.col.get_config(self._saved_searches_key, {})
|
||||||
|
|
||||||
def _set_saved_searches(self, searches: Dict[str, str]) -> None:
|
def _set_saved_searches(self, searches: dict[str, str]) -> None:
|
||||||
self.col.set_config(self._saved_searches_key, searches)
|
self.col.set_config(self._saved_searches_key, searches)
|
||||||
|
|
||||||
def _get_current_search(self) -> Optional[str]:
|
def _get_current_search(self) -> str | None:
|
||||||
try:
|
try:
|
||||||
return self.col.build_search_string(self.browser.current_search())
|
return self.col.build_search_string(self.browser.current_search())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -1198,24 +1198,24 @@ class SidebarTreeView(QTreeView):
|
||||||
# Helpers
|
# Helpers
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
def _selected_items(self) -> List[SidebarItem]:
|
def _selected_items(self) -> list[SidebarItem]:
|
||||||
return [self.model().item_for_index(idx) for idx in self.selectedIndexes()]
|
return [self.model().item_for_index(idx) for idx in self.selectedIndexes()]
|
||||||
|
|
||||||
def _selected_decks(self) -> List[DeckId]:
|
def _selected_decks(self) -> list[DeckId]:
|
||||||
return [
|
return [
|
||||||
DeckId(item.id)
|
DeckId(item.id)
|
||||||
for item in self._selected_items()
|
for item in self._selected_items()
|
||||||
if item.item_type == SidebarItemType.DECK
|
if item.item_type == SidebarItemType.DECK
|
||||||
]
|
]
|
||||||
|
|
||||||
def _selected_saved_searches(self) -> List[str]:
|
def _selected_saved_searches(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
item.name
|
item.name
|
||||||
for item in self._selected_items()
|
for item in self._selected_items()
|
||||||
if item.item_type == SidebarItemType.SAVED_SEARCH
|
if item.item_type == SidebarItemType.SAVED_SEARCH
|
||||||
]
|
]
|
||||||
|
|
||||||
def _selected_tags(self) -> List[str]:
|
def _selected_tags(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
item.full_name
|
item.full_name
|
||||||
for item in self._selected_items()
|
for item in self._selected_items()
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Generator, Optional, Sequence, Tuple, Union
|
from typing import TYPE_CHECKING, Generator, Sequence, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
|
@ -24,10 +23,10 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]]
|
||||||
class SearchContext:
|
class SearchContext:
|
||||||
search: str
|
search: str
|
||||||
browser: aqt.browser.Browser
|
browser: aqt.browser.Browser
|
||||||
order: Union[bool, str, Column] = True
|
order: bool | str | Column = True
|
||||||
reverse: bool = False
|
reverse: bool = False
|
||||||
# if set, provided ids will be used instead of the regular search
|
# if set, provided ids will be used instead of the regular search
|
||||||
ids: Optional[Sequence[ItemId]] = None
|
ids: Sequence[ItemId] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -41,14 +40,14 @@ class CellRow:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cells: Generator[Tuple[str, bool], None, None],
|
cells: Generator[tuple[str, bool], None, None],
|
||||||
color: BrowserRow.Color.V,
|
color: BrowserRow.Color.V,
|
||||||
font_name: str,
|
font_name: str,
|
||||||
font_size: int,
|
font_size: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.refreshed_at: float = time.time()
|
self.refreshed_at: float = time.time()
|
||||||
self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
|
self.cells: tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
|
||||||
self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color)
|
self.color: tuple[str, str] | None = backend_color_to_aqt_color(color)
|
||||||
self.font_name: str = font_name or "arial"
|
self.font_name: str = font_name or "arial"
|
||||||
self.font_size: int = font_size if font_size > 0 else 12
|
self.font_size: int = font_size if font_size > 0 else 12
|
||||||
|
|
||||||
|
@ -75,7 +74,7 @@ class CellRow:
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]:
|
def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> tuple[str, str] | None:
|
||||||
if color == BrowserRow.COLOR_MARKED:
|
if color == BrowserRow.COLOR_MARKED:
|
||||||
return colors.MARKED_BG
|
return colors.MARKED_BG
|
||||||
if color == BrowserRow.COLOR_SUSPENDED:
|
if color == BrowserRow.COLOR_SUSPENDED:
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast
|
from typing import Any, Callable, Sequence, cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
|
@ -41,13 +40,13 @@ class DataModel(QAbstractTableModel):
|
||||||
) -> None:
|
) -> None:
|
||||||
QAbstractTableModel.__init__(self)
|
QAbstractTableModel.__init__(self)
|
||||||
self.col: Collection = col
|
self.col: Collection = col
|
||||||
self.columns: Dict[str, Column] = dict(
|
self.columns: dict[str, Column] = {
|
||||||
((c.key, c) for c in self.col.all_browser_columns())
|
c.key: c for c in self.col.all_browser_columns()
|
||||||
)
|
}
|
||||||
gui_hooks.browser_did_fetch_columns(self.columns)
|
gui_hooks.browser_did_fetch_columns(self.columns)
|
||||||
self._state: ItemState = state
|
self._state: ItemState = state
|
||||||
self._items: Sequence[ItemId] = []
|
self._items: Sequence[ItemId] = []
|
||||||
self._rows: Dict[int, CellRow] = {}
|
self._rows: dict[int, CellRow] = {}
|
||||||
self._block_updates = False
|
self._block_updates = False
|
||||||
self._stale_cutoff = 0.0
|
self._stale_cutoff = 0.0
|
||||||
self._on_row_state_will_change = row_state_will_change_callback
|
self._on_row_state_will_change = row_state_will_change_callback
|
||||||
|
@ -77,7 +76,7 @@ class DataModel(QAbstractTableModel):
|
||||||
return self._fetch_row_and_update_cache(index, item, None)
|
return self._fetch_row_and_update_cache(index, item, None)
|
||||||
|
|
||||||
def _fetch_row_and_update_cache(
|
def _fetch_row_and_update_cache(
|
||||||
self, index: QModelIndex, item: ItemId, old_row: Optional[CellRow]
|
self, index: QModelIndex, item: ItemId, old_row: CellRow | None
|
||||||
) -> CellRow:
|
) -> CellRow:
|
||||||
"""Fetch a row from the backend, add it to the cache and return it.
|
"""Fetch a row from the backend, add it to the cache and return it.
|
||||||
Then fire callbacks if the row is being deleted or restored.
|
Then fire callbacks if the row is being deleted or restored.
|
||||||
|
@ -119,7 +118,7 @@ class DataModel(QAbstractTableModel):
|
||||||
)
|
)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def get_cached_row(self, index: QModelIndex) -> Optional[CellRow]:
|
def get_cached_row(self, index: QModelIndex) -> CellRow | None:
|
||||||
"""Get row if it is cached, regardless of staleness."""
|
"""Get row if it is cached, regardless of staleness."""
|
||||||
return self._rows.get(self.get_item(index))
|
return self._rows.get(self.get_item(index))
|
||||||
|
|
||||||
|
@ -175,41 +174,41 @@ class DataModel(QAbstractTableModel):
|
||||||
def get_item(self, index: QModelIndex) -> ItemId:
|
def get_item(self, index: QModelIndex) -> ItemId:
|
||||||
return self._items[index.row()]
|
return self._items[index.row()]
|
||||||
|
|
||||||
def get_items(self, indices: List[QModelIndex]) -> Sequence[ItemId]:
|
def get_items(self, indices: list[QModelIndex]) -> Sequence[ItemId]:
|
||||||
return [self.get_item(index) for index in indices]
|
return [self.get_item(index) for index in indices]
|
||||||
|
|
||||||
def get_card_ids(self, indices: List[QModelIndex]) -> Sequence[CardId]:
|
def get_card_ids(self, indices: list[QModelIndex]) -> Sequence[CardId]:
|
||||||
return self._state.get_card_ids(self.get_items(indices))
|
return self._state.get_card_ids(self.get_items(indices))
|
||||||
|
|
||||||
def get_note_ids(self, indices: List[QModelIndex]) -> Sequence[NoteId]:
|
def get_note_ids(self, indices: list[QModelIndex]) -> Sequence[NoteId]:
|
||||||
return self._state.get_note_ids(self.get_items(indices))
|
return self._state.get_note_ids(self.get_items(indices))
|
||||||
|
|
||||||
def get_note_id(self, index: QModelIndex) -> Optional[NoteId]:
|
def get_note_id(self, index: QModelIndex) -> NoteId | None:
|
||||||
if nid_list := self._state.get_note_ids([self.get_item(index)]):
|
if nid_list := self._state.get_note_ids([self.get_item(index)]):
|
||||||
return nid_list[0]
|
return nid_list[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get row numbers from items
|
# Get row numbers from items
|
||||||
|
|
||||||
def get_item_row(self, item: ItemId) -> Optional[int]:
|
def get_item_row(self, item: ItemId) -> int | None:
|
||||||
for row, i in enumerate(self._items):
|
for row, i in enumerate(self._items):
|
||||||
if i == item:
|
if i == item:
|
||||||
return row
|
return row
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_item_rows(self, items: Sequence[ItemId]) -> List[int]:
|
def get_item_rows(self, items: Sequence[ItemId]) -> list[int]:
|
||||||
rows = []
|
rows = []
|
||||||
for row, i in enumerate(self._items):
|
for row, i in enumerate(self._items):
|
||||||
if i in items:
|
if i in items:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def get_card_row(self, card_id: CardId) -> Optional[int]:
|
def get_card_row(self, card_id: CardId) -> int | None:
|
||||||
return self.get_item_row(self._state.get_item_from_card_id(card_id))
|
return self.get_item_row(self._state.get_item_from_card_id(card_id))
|
||||||
|
|
||||||
# Get objects (cards or notes)
|
# Get objects (cards or notes)
|
||||||
|
|
||||||
def get_card(self, index: QModelIndex) -> Optional[Card]:
|
def get_card(self, index: QModelIndex) -> Card | None:
|
||||||
"""Try to return the indicated, possibly deleted card."""
|
"""Try to return the indicated, possibly deleted card."""
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
|
@ -218,7 +217,7 @@ class DataModel(QAbstractTableModel):
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_note(self, index: QModelIndex) -> Optional[Note]:
|
def get_note(self, index: QModelIndex) -> Note | None:
|
||||||
"""Try to return the indicated, possibly deleted note."""
|
"""Try to return the indicated, possibly deleted note."""
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
|
@ -280,7 +279,7 @@ class DataModel(QAbstractTableModel):
|
||||||
self.columns[key] = addon_column_fillin(key)
|
self.columns[key] = addon_column_fillin(key)
|
||||||
return self.columns[key]
|
return self.columns[key]
|
||||||
|
|
||||||
def active_column_index(self, column: str) -> Optional[int]:
|
def active_column_index(self, column: str) -> int | None:
|
||||||
return (
|
return (
|
||||||
self._state.active_columns.index(column)
|
self._state.active_columns.index(column)
|
||||||
if column in self._state.active_columns
|
if column in self._state.active_columns
|
||||||
|
@ -317,7 +316,7 @@ class DataModel(QAbstractTableModel):
|
||||||
qfont.setPixelSize(row.font_size)
|
qfont.setPixelSize(row.font_size)
|
||||||
return qfont
|
return qfont
|
||||||
elif role == Qt.TextAlignmentRole:
|
elif role == Qt.TextAlignmentRole:
|
||||||
align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter
|
align: Qt.AlignmentFlag | int = Qt.AlignVCenter
|
||||||
if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
|
if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
|
||||||
align |= Qt.AlignHCenter
|
align |= Qt.AlignHCenter
|
||||||
return align
|
return align
|
||||||
|
@ -329,7 +328,7 @@ class DataModel(QAbstractTableModel):
|
||||||
|
|
||||||
def headerData(
|
def headerData(
|
||||||
self, section: int, orientation: Qt.Orientation, role: int = 0
|
self, section: int, orientation: Qt.Orientation, role: int = 0
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||||
return self._state.column_label(self.column_at_section(section))
|
return self._state.column_label(self.column_at_section(section))
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from typing import List, Sequence, Union, cast
|
from typing import Sequence, cast
|
||||||
|
|
||||||
from anki.browser import BrowserConfig
|
from anki.browser import BrowserConfig
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
|
@ -18,7 +17,7 @@ class ItemState(ABC):
|
||||||
GEOMETRY_KEY_PREFIX: str
|
GEOMETRY_KEY_PREFIX: str
|
||||||
SORT_COLUMN_KEY: str
|
SORT_COLUMN_KEY: str
|
||||||
SORT_BACKWARDS_KEY: str
|
SORT_BACKWARDS_KEY: str
|
||||||
_active_columns: List[str]
|
_active_columns: list[str]
|
||||||
|
|
||||||
def __init__(self, col: Collection) -> None:
|
def __init__(self, col: Collection) -> None:
|
||||||
self.col = col
|
self.col = col
|
||||||
|
@ -57,7 +56,7 @@ class ItemState(ABC):
|
||||||
# abstractproperty is deprecated but used due to mypy limitations
|
# abstractproperty is deprecated but used due to mypy limitations
|
||||||
# (https://github.com/python/mypy/issues/1362)
|
# (https://github.com/python/mypy/issues/1362)
|
||||||
@abstractproperty # pylint: disable=deprecated-decorator
|
@abstractproperty # pylint: disable=deprecated-decorator
|
||||||
def active_columns(self) -> List[str]:
|
def active_columns(self) -> list[str]:
|
||||||
"""Return the saved or default columns for the state."""
|
"""Return the saved or default columns for the state."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -96,7 +95,7 @@ class ItemState(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def find_items(
|
def find_items(
|
||||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
self, search: str, order: bool | str | Column, reverse: bool
|
||||||
) -> Sequence[ItemId]:
|
) -> Sequence[ItemId]:
|
||||||
"""Return the item ids fitting the given search and order."""
|
"""Return the item ids fitting the given search and order."""
|
||||||
|
|
||||||
|
@ -133,7 +132,7 @@ class CardState(ItemState):
|
||||||
self._active_columns = self.col.load_browser_card_columns()
|
self._active_columns = self.col.load_browser_card_columns()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_columns(self) -> List[str]:
|
def active_columns(self) -> list[str]:
|
||||||
return self._active_columns
|
return self._active_columns
|
||||||
|
|
||||||
def toggle_active_column(self, column: str) -> None:
|
def toggle_active_column(self, column: str) -> None:
|
||||||
|
@ -150,7 +149,7 @@ class CardState(ItemState):
|
||||||
return self.get_card(item).note()
|
return self.get_card(item).note()
|
||||||
|
|
||||||
def find_items(
|
def find_items(
|
||||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
self, search: str, order: bool | str | Column, reverse: bool
|
||||||
) -> Sequence[ItemId]:
|
) -> Sequence[ItemId]:
|
||||||
return self.col.find_cards(search, order, reverse)
|
return self.col.find_cards(search, order, reverse)
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ class NoteState(ItemState):
|
||||||
self._active_columns = self.col.load_browser_note_columns()
|
self._active_columns = self.col.load_browser_note_columns()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_columns(self) -> List[str]:
|
def active_columns(self) -> list[str]:
|
||||||
return self._active_columns
|
return self._active_columns
|
||||||
|
|
||||||
def toggle_active_column(self, column: str) -> None:
|
def toggle_active_column(self, column: str) -> None:
|
||||||
|
@ -197,7 +196,7 @@ class NoteState(ItemState):
|
||||||
return self.col.get_note(NoteId(item))
|
return self.col.get_note(NoteId(item))
|
||||||
|
|
||||||
def find_items(
|
def find_items(
|
||||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
self, search: str, order: bool | str | Column, reverse: bool
|
||||||
) -> Sequence[ItemId]:
|
) -> Sequence[ItemId]:
|
||||||
return self.col.find_notes(search, order, reverse)
|
return self.col.find_notes(search, order, reverse)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, List, Optional, Sequence, Tuple, cast
|
from typing import Any, Callable, Sequence, cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -45,12 +44,12 @@ class Table:
|
||||||
self._on_row_state_will_change,
|
self._on_row_state_will_change,
|
||||||
self._on_row_state_changed,
|
self._on_row_state_changed,
|
||||||
)
|
)
|
||||||
self._view: Optional[QTableView] = None
|
self._view: QTableView | None = None
|
||||||
# cached for performance
|
# cached for performance
|
||||||
self._len_selection = 0
|
self._len_selection = 0
|
||||||
self._selected_rows: Optional[List[QModelIndex]] = None
|
self._selected_rows: list[QModelIndex] | None = None
|
||||||
# temporarily set for selection preservation
|
# temporarily set for selection preservation
|
||||||
self._current_item: Optional[ItemId] = None
|
self._current_item: ItemId | None = None
|
||||||
self._selected_items: Sequence[ItemId] = []
|
self._selected_items: Sequence[ItemId] = []
|
||||||
|
|
||||||
def set_view(self, view: QTableView) -> None:
|
def set_view(self, view: QTableView) -> None:
|
||||||
|
@ -89,13 +88,13 @@ class Table:
|
||||||
|
|
||||||
# Get objects
|
# Get objects
|
||||||
|
|
||||||
def get_current_card(self) -> Optional[Card]:
|
def get_current_card(self) -> Card | None:
|
||||||
return self._model.get_card(self._current())
|
return self._model.get_card(self._current())
|
||||||
|
|
||||||
def get_current_note(self) -> Optional[Note]:
|
def get_current_note(self) -> Note | None:
|
||||||
return self._model.get_note(self._current())
|
return self._model.get_note(self._current())
|
||||||
|
|
||||||
def get_single_selected_card(self) -> Optional[Card]:
|
def get_single_selected_card(self) -> Card | None:
|
||||||
"""If there is only one row selected return its card, else None.
|
"""If there is only one row selected return its card, else None.
|
||||||
This may be a different one than the current card."""
|
This may be a different one than the current card."""
|
||||||
if self.len_selection() != 1:
|
if self.len_selection() != 1:
|
||||||
|
@ -171,7 +170,7 @@ class Table:
|
||||||
self._model.redraw_cells()
|
self._model.redraw_cells()
|
||||||
|
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
if changes.browser_table:
|
if changes.browser_table:
|
||||||
self._model.mark_cache_stale()
|
self._model.mark_cache_stale()
|
||||||
|
@ -260,7 +259,7 @@ class Table:
|
||||||
def _current(self) -> QModelIndex:
|
def _current(self) -> QModelIndex:
|
||||||
return self._view.selectionModel().currentIndex()
|
return self._view.selectionModel().currentIndex()
|
||||||
|
|
||||||
def _selected(self) -> List[QModelIndex]:
|
def _selected(self) -> list[QModelIndex]:
|
||||||
if self._selected_rows is None:
|
if self._selected_rows is None:
|
||||||
self._selected_rows = self._view.selectionModel().selectedRows()
|
self._selected_rows = self._view.selectionModel().selectedRows()
|
||||||
return self._selected_rows
|
return self._selected_rows
|
||||||
|
@ -280,7 +279,7 @@ class Table:
|
||||||
self._len_selection = 0
|
self._len_selection = 0
|
||||||
self._selected_rows = None
|
self._selected_rows = None
|
||||||
|
|
||||||
def _select_rows(self, rows: List[int]) -> None:
|
def _select_rows(self, rows: list[int]) -> None:
|
||||||
selection = QItemSelection()
|
selection = QItemSelection()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
selection.select(
|
selection.select(
|
||||||
|
@ -532,9 +531,7 @@ class Table:
|
||||||
self._selected_items = []
|
self._selected_items = []
|
||||||
self._current_item = None
|
self._current_item = None
|
||||||
|
|
||||||
def _qualify_selected_rows(
|
def _qualify_selected_rows(self, rows: list[int], current: int | None) -> list[int]:
|
||||||
self, rows: List[int], current: Optional[int]
|
|
||||||
) -> List[int]:
|
|
||||||
"""Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current."""
|
"""Return between 1 and SELECTION_LIMIT rows, as far as possible from rows or current."""
|
||||||
if rows:
|
if rows:
|
||||||
if len(rows) < self.SELECTION_LIMIT:
|
if len(rows) < self.SELECTION_LIMIT:
|
||||||
|
@ -544,7 +541,7 @@ class Table:
|
||||||
return rows[0:1]
|
return rows[0:1]
|
||||||
return [current if current else 0]
|
return [current if current else 0]
|
||||||
|
|
||||||
def _intersected_selection(self) -> Tuple[List[int], Optional[int]]:
|
def _intersected_selection(self) -> tuple[list[int], int | None]:
|
||||||
"""Return all rows of items that were in the saved selection and the row of the saved
|
"""Return all rows of items that were in the saved selection and the row of the saved
|
||||||
current element if present.
|
current element if present.
|
||||||
"""
|
"""
|
||||||
|
@ -554,7 +551,7 @@ class Table:
|
||||||
)
|
)
|
||||||
return selected_rows, current_row
|
return selected_rows, current_row
|
||||||
|
|
||||||
def _toggled_selection(self) -> Tuple[List[int], Optional[int]]:
|
def _toggled_selection(self) -> tuple[list[int], int | None]:
|
||||||
"""Convert the items of the saved selection and current element to the new state and
|
"""Convert the items of the saved selection and current element to the new state and
|
||||||
return their rows.
|
return their rows.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, Dict, List, Match, Optional
|
from typing import Any, Match, Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
|
@ -135,7 +135,7 @@ class CardLayout(QDialog):
|
||||||
combo.setEnabled(not self._isCloze())
|
combo.setEnabled(not self._isCloze())
|
||||||
self.ignore_change_signals = False
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def _summarizedName(self, idx: int, tmpl: Dict) -> str:
|
def _summarizedName(self, idx: int, tmpl: dict) -> str:
|
||||||
return "{}: {}: {} -> {}".format(
|
return "{}: {}: {} -> {}".format(
|
||||||
idx + 1,
|
idx + 1,
|
||||||
tmpl["name"],
|
tmpl["name"],
|
||||||
|
@ -146,7 +146,7 @@ class CardLayout(QDialog):
|
||||||
def _fieldsOnTemplate(self, fmt: str) -> str:
|
def _fieldsOnTemplate(self, fmt: str) -> str:
|
||||||
matches = re.findall("{{[^#/}]+?}}", fmt)
|
matches = re.findall("{{[^#/}]+?}}", fmt)
|
||||||
chars_allowed = 30
|
chars_allowed = 30
|
||||||
field_names: List[str] = []
|
field_names: list[str] = []
|
||||||
for m in matches:
|
for m in matches:
|
||||||
# strip off mustache
|
# strip off mustache
|
||||||
m = re.sub(r"[{}]", "", m)
|
m = re.sub(r"[{}]", "", m)
|
||||||
|
@ -440,7 +440,7 @@ class CardLayout(QDialog):
|
||||||
# Reading/writing question/answer/css
|
# Reading/writing question/answer/css
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def current_template(self) -> Dict:
|
def current_template(self) -> dict:
|
||||||
if self._isCloze():
|
if self._isCloze():
|
||||||
return self.templates[0]
|
return self.templates[0]
|
||||||
return self.templates[self.ord]
|
return self.templates[self.ord]
|
||||||
|
@ -592,7 +592,7 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
self.mw.taskman.with_progress(get_count, on_done)
|
self.mw.taskman.with_progress(get_count, on_done)
|
||||||
|
|
||||||
def onRemoveInner(self, template: Dict) -> None:
|
def onRemoveInner(self, template: dict) -> None:
|
||||||
self.mm.remove_template(self.model, template)
|
self.mm.remove_template(self.model, template)
|
||||||
|
|
||||||
# ensure current ordinal is within bounds
|
# ensure current ordinal is within bounds
|
||||||
|
@ -668,7 +668,7 @@ class CardLayout(QDialog):
|
||||||
self._flipQA(old, old)
|
self._flipQA(old, old)
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def _flipQA(self, src: Dict, dst: Dict) -> None:
|
def _flipQA(self, src: dict, dst: dict) -> None:
|
||||||
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
||||||
if not m:
|
if not m:
|
||||||
showInfo(tr.card_templates_anki_couldnt_find_the_line_between())
|
showInfo(tr.card_templates_anki_couldnt_find_the_line_between())
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
|
@ -79,7 +79,7 @@ class DeckBrowser:
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if changes.study_queues and handler is not self:
|
if changes.study_queues and handler is not self:
|
||||||
self._refresh_needed = True
|
self._refresh_needed = True
|
||||||
|
@ -175,11 +175,11 @@ class DeckBrowser:
|
||||||
|
|
||||||
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
||||||
buf = """
|
buf = """
|
||||||
<tr><th colspan=5 align=start>%s</th>
|
<tr><th colspan=5 align=start>{}</th>
|
||||||
<th class=count>%s</th>
|
<th class=count>{}</th>
|
||||||
<th class=count></th>
|
<th class=count></th>
|
||||||
<th class=count>%s</th>
|
<th class=count>{}</th>
|
||||||
<th class=optscol></th></tr>""" % (
|
<th class=optscol></th></tr>""".format(
|
||||||
tr.decks_deck(),
|
tr.decks_deck(),
|
||||||
tr.actions_new(),
|
tr.actions_new(),
|
||||||
tr.statistics_due_count(),
|
tr.statistics_due_count(),
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QLineEdit
|
from PyQt5.QtWidgets import QLineEdit
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ from aqt.utils import (
|
||||||
|
|
||||||
|
|
||||||
class DeckConf(QDialog):
|
class DeckConf(QDialog):
|
||||||
def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None:
|
def __init__(self, mw: aqt.AnkiQt, deck: dict) -> None:
|
||||||
QDialog.__init__(self, mw)
|
QDialog.__init__(self, mw)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
|
@ -74,7 +74,7 @@ class DeckConf(QDialog):
|
||||||
|
|
||||||
def setupConfs(self) -> None:
|
def setupConfs(self) -> None:
|
||||||
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
||||||
self.conf: Optional[DeckConfigDict] = None
|
self.conf: DeckConfigDict | None = None
|
||||||
self.loadConfs()
|
self.loadConfs()
|
||||||
|
|
||||||
def loadConfs(self) -> None:
|
def loadConfs(self) -> None:
|
||||||
|
@ -175,7 +175,7 @@ class DeckConf(QDialog):
|
||||||
# Loading
|
# Loading
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def listToUser(self, l: List[Union[int, float]]) -> str:
|
def listToUser(self, l: list[Union[int, float]]) -> str:
|
||||||
def num_to_user(n: Union[int, float]) -> str:
|
def num_to_user(n: Union[int, float]) -> str:
|
||||||
if n == round(n):
|
if n == round(n):
|
||||||
return str(int(n))
|
return str(int(n))
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.deckconf
|
import aqt.deckconf
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
@ -67,7 +65,7 @@ class DeckOptionsDialog(QDialog):
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
|
|
||||||
def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> None:
|
def confirm_deck_then_display_options(active_card: Card | None = None) -> None:
|
||||||
decks = [aqt.mw.col.decks.current()]
|
decks = [aqt.mw.col.decks.current()]
|
||||||
if card := active_card:
|
if card := active_card:
|
||||||
if card.odid and card.odid != decks[0]["id"]:
|
if card.odid and card.odid != decks[0]["id"]:
|
||||||
|
@ -83,7 +81,7 @@ def confirm_deck_then_display_options(active_card: Optional[Card] = None) -> Non
|
||||||
_deck_prompt_dialog(decks)
|
_deck_prompt_dialog(decks)
|
||||||
|
|
||||||
|
|
||||||
def _deck_prompt_dialog(decks: List[DeckDict]) -> None:
|
def _deck_prompt_dialog(decks: list[DeckDict]) -> None:
|
||||||
diag = QDialog(aqt.mw.app.activeWindow())
|
diag = QDialog(aqt.mw.app.activeWindow())
|
||||||
diag.setWindowTitle("Anki")
|
diag.setWindowTitle("Anki")
|
||||||
box = QVBoxLayout()
|
box = QVBoxLayout()
|
||||||
|
|
|
@ -14,7 +14,7 @@ import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast
|
from typing import Any, Callable, Match, cast
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
import requests
|
import requests
|
||||||
|
@ -104,14 +104,14 @@ class Editor:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.widget = widget
|
self.widget = widget
|
||||||
self.parentWindow = parentWindow
|
self.parentWindow = parentWindow
|
||||||
self.note: Optional[Note] = None
|
self.note: Note | None = None
|
||||||
self.addMode = addMode
|
self.addMode = addMode
|
||||||
self.currentField: Optional[int] = None
|
self.currentField: int | None = None
|
||||||
# Similar to currentField, but not set to None on a blur. May be
|
# Similar to currentField, but not set to None on a blur. May be
|
||||||
# outside the bounds of the current notetype.
|
# outside the bounds of the current notetype.
|
||||||
self.last_field_index: Optional[int] = None
|
self.last_field_index: int | None = None
|
||||||
# current card, for card layout
|
# current card, for card layout
|
||||||
self.card: Optional[Card] = None
|
self.card: Card | None = None
|
||||||
self.setupOuter()
|
self.setupOuter()
|
||||||
self.setupWeb()
|
self.setupWeb()
|
||||||
self.setupShortcuts()
|
self.setupShortcuts()
|
||||||
|
@ -147,7 +147,7 @@ class Editor:
|
||||||
default_css=True,
|
default_css=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
lefttopbtns: List[str] = []
|
lefttopbtns: list[str] = []
|
||||||
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
||||||
|
|
||||||
lefttopbtns_defs = [
|
lefttopbtns_defs = [
|
||||||
|
@ -156,7 +156,7 @@ class Editor:
|
||||||
]
|
]
|
||||||
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
||||||
|
|
||||||
righttopbtns: List[str] = []
|
righttopbtns: list[str] = []
|
||||||
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
||||||
# legacy filter
|
# legacy filter
|
||||||
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
||||||
|
@ -191,9 +191,9 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
|
|
||||||
def addButton(
|
def addButton(
|
||||||
self,
|
self,
|
||||||
icon: Optional[str],
|
icon: str | None,
|
||||||
cmd: str,
|
cmd: str,
|
||||||
func: Callable[["Editor"], None],
|
func: Callable[[Editor], None],
|
||||||
tip: str = "",
|
tip: str = "",
|
||||||
label: str = "",
|
label: str = "",
|
||||||
id: str = None,
|
id: str = None,
|
||||||
|
@ -242,11 +242,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
|
|
||||||
def _addButton(
|
def _addButton(
|
||||||
self,
|
self,
|
||||||
icon: Optional[str],
|
icon: str | None,
|
||||||
cmd: str,
|
cmd: str,
|
||||||
tip: str = "",
|
tip: str = "",
|
||||||
label: str = "",
|
label: str = "",
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
toggleable: bool = False,
|
toggleable: bool = False,
|
||||||
disables: bool = True,
|
disables: bool = True,
|
||||||
rightside: bool = True,
|
rightside: bool = True,
|
||||||
|
@ -302,7 +302,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
|
|
||||||
def setupShortcuts(self) -> None:
|
def setupShortcuts(self) -> None:
|
||||||
# if a third element is provided, enable shortcut even when no field selected
|
# if a third element is provided, enable shortcut even when no field selected
|
||||||
cuts: List[Tuple] = []
|
cuts: list[tuple] = []
|
||||||
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
gui_hooks.editor_did_init_shortcuts(cuts, self)
|
||||||
for row in cuts:
|
for row in cuts:
|
||||||
if len(row) == 2:
|
if len(row) == 2:
|
||||||
|
@ -449,7 +449,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def set_note(
|
def set_note(
|
||||||
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
|
self, note: Note | None, hide: bool = True, focusTo: int | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"Make NOTE the current note."
|
"Make NOTE the current note."
|
||||||
self.note = note
|
self.note = note
|
||||||
|
@ -462,7 +462,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
def loadNoteKeepingFocus(self) -> None:
|
def loadNoteKeepingFocus(self) -> None:
|
||||||
self.loadNote(self.currentField)
|
self.loadNote(self.currentField)
|
||||||
|
|
||||||
def loadNote(self, focusTo: Optional[int] = None) -> None:
|
def loadNote(self, focusTo: int | None = None) -> None:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -488,7 +488,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
|
text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
|
||||||
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f")
|
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f")
|
||||||
|
|
||||||
js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s); setColorButtons(%s); setTags(%s); " % (
|
js = "setFields({}); setFonts({}); focusField({}); setNoteId({}); setColorButtons({}); setTags({}); ".format(
|
||||||
json.dumps(data),
|
json.dumps(data),
|
||||||
json.dumps(self.fonts()),
|
json.dumps(self.fonts()),
|
||||||
json.dumps(focusTo),
|
json.dumps(focusTo),
|
||||||
|
@ -510,7 +510,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
initiator=self
|
initiator=self
|
||||||
)
|
)
|
||||||
|
|
||||||
def fonts(self) -> List[Tuple[str, int, bool]]:
|
def fonts(self) -> list[tuple[str, int, bool]]:
|
||||||
return [
|
return [
|
||||||
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
|
(gui_hooks.editor_will_use_font_for_field(f["font"]), f["size"], f["rtl"])
|
||||||
for f in self.note.note_type()["flds"]
|
for f in self.note.note_type()["flds"]
|
||||||
|
@ -573,7 +573,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool:
|
def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
return True
|
return True
|
||||||
m = self.note.note_type()
|
m = self.note.note_type()
|
||||||
|
@ -700,7 +700,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
# Media downloads
|
# Media downloads
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def urlToLink(self, url: str) -> Optional[str]:
|
def urlToLink(self, url: str) -> str | None:
|
||||||
fname = self.urlToFile(url)
|
fname = self.urlToFile(url)
|
||||||
if not fname:
|
if not fname:
|
||||||
return None
|
return None
|
||||||
|
@ -715,7 +715,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
av_player.play_file(fname)
|
av_player.play_file(fname)
|
||||||
return f"[sound:{html.escape(fname, quote=False)}]"
|
return f"[sound:{html.escape(fname, quote=False)}]"
|
||||||
|
|
||||||
def urlToFile(self, url: str) -> Optional[str]:
|
def urlToFile(self, url: str) -> str | None:
|
||||||
l = url.lower()
|
l = url.lower()
|
||||||
for suffix in pics + audio:
|
for suffix in pics + audio:
|
||||||
if l.endswith(f".{suffix}"):
|
if l.endswith(f".{suffix}"):
|
||||||
|
@ -760,7 +760,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
fname = f"paste-{csum}{ext}"
|
fname = f"paste-{csum}{ext}"
|
||||||
return self._addMediaFromData(fname, data)
|
return self._addMediaFromData(fname, data)
|
||||||
|
|
||||||
def _retrieveURL(self, url: str) -> Optional[str]:
|
def _retrieveURL(self, url: str) -> str | None:
|
||||||
"Download file into media folder and return local filename or None."
|
"Download file into media folder and return local filename or None."
|
||||||
# urllib doesn't understand percent-escaped utf8, but requires things like
|
# urllib doesn't understand percent-escaped utf8, but requires things like
|
||||||
# '#' to be escaped.
|
# '#' to be escaped.
|
||||||
|
@ -774,7 +774,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
# fetch it into a temporary folder
|
# fetch it into a temporary folder
|
||||||
self.mw.progress.start(immediate=not local, parent=self.parentWindow)
|
self.mw.progress.start(immediate=not local, parent=self.parentWindow)
|
||||||
content_type = None
|
content_type = None
|
||||||
error_msg: Optional[str] = None
|
error_msg: str | None = None
|
||||||
try:
|
try:
|
||||||
if local:
|
if local:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
|
@ -972,7 +972,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
for name, val in list(self.note.items()):
|
for name, val in list(self.note.items()):
|
||||||
m = re.findall(r"\{\{c(\d+)::", val)
|
m = re.findall(r"\{\{c(\d+)::", val)
|
||||||
if m:
|
if m:
|
||||||
highest = max(highest, sorted([int(x) for x in m])[-1])
|
highest = max(highest, sorted(int(x) for x in m)[-1])
|
||||||
# reuse last?
|
# reuse last?
|
||||||
if not KeyboardModifiersPressed().alt:
|
if not KeyboardModifiersPressed().alt:
|
||||||
highest += 1
|
highest += 1
|
||||||
|
@ -1069,7 +1069,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
# Links from HTML
|
# Links from HTML
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_links: Dict[str, Callable] = dict(
|
_links: dict[str, Callable] = dict(
|
||||||
fields=onFields,
|
fields=onFields,
|
||||||
cards=onCardLayout,
|
cards=onCardLayout,
|
||||||
bold=toggleBold,
|
bold=toggleBold,
|
||||||
|
@ -1163,7 +1163,7 @@ class EditorWebView(AnkiWebView):
|
||||||
# returns (html, isInternal)
|
# returns (html, isInternal)
|
||||||
def _processMime(
|
def _processMime(
|
||||||
self, mime: QMimeData, extended: bool = False, drop_event: bool = False
|
self, mime: QMimeData, extended: bool = False, drop_event: bool = False
|
||||||
) -> Tuple[str, bool]:
|
) -> tuple[str, bool]:
|
||||||
# print("html=%s image=%s urls=%s txt=%s" % (
|
# print("html=%s image=%s urls=%s txt=%s" % (
|
||||||
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()))
|
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText()))
|
||||||
# print("html", mime.html())
|
# print("html", mime.html())
|
||||||
|
@ -1193,7 +1193,7 @@ class EditorWebView(AnkiWebView):
|
||||||
return html, True
|
return html, True
|
||||||
return "", False
|
return "", False
|
||||||
|
|
||||||
def _processUrls(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
def _processUrls(self, mime: QMimeData, extended: bool = False) -> str | None:
|
||||||
if not mime.hasUrls():
|
if not mime.hasUrls():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1206,7 +1206,7 @@ class EditorWebView(AnkiWebView):
|
||||||
|
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
def _processText(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
def _processText(self, mime: QMimeData, extended: bool = False) -> str | None:
|
||||||
if not mime.hasText():
|
if not mime.hasText():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1245,7 +1245,7 @@ class EditorWebView(AnkiWebView):
|
||||||
processed.pop()
|
processed.pop()
|
||||||
return "".join(processed)
|
return "".join(processed)
|
||||||
|
|
||||||
def _processImage(self, mime: QMimeData, extended: bool = False) -> Optional[str]:
|
def _processImage(self, mime: QMimeData, extended: bool = False) -> str | None:
|
||||||
if not mime.hasImage():
|
if not mime.hasImage():
|
||||||
return None
|
return None
|
||||||
im = QImage(mime.imageData())
|
im = QImage(mime.imageData())
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, List
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
|
@ -89,7 +89,7 @@ class EmptyCardsDialog(QDialog):
|
||||||
self.mw.taskman.run_in_background(delete, on_done)
|
self.mw.taskman.run_in_background(delete, on_done)
|
||||||
|
|
||||||
def _delete_cards(self, keep_notes: bool) -> int:
|
def _delete_cards(self, keep_notes: bool) -> int:
|
||||||
to_delete: List[CardId] = []
|
to_delete: list[CardId] = []
|
||||||
note: EmptyCardsReport.NoteWithEmptyCards
|
note: EmptyCardsReport.NoteWithEmptyCards
|
||||||
for note in self.report.notes:
|
for note in self.report.notes:
|
||||||
if keep_notes and note.will_delete_note:
|
if keep_notes and note.will_delete_note:
|
||||||
|
|
|
@ -7,7 +7,6 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -29,21 +28,21 @@ class ExportDialog(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mw: aqt.main.AnkiQt,
|
mw: aqt.main.AnkiQt,
|
||||||
did: Optional[DeckId] = None,
|
did: DeckId | None = None,
|
||||||
cids: Optional[List[CardId]] = None,
|
cids: list[CardId] | None = None,
|
||||||
):
|
):
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.col = mw.col.weakref()
|
self.col = mw.col.weakref()
|
||||||
self.frm = aqt.forms.exporting.Ui_ExportDialog()
|
self.frm = aqt.forms.exporting.Ui_ExportDialog()
|
||||||
self.frm.setupUi(self)
|
self.frm.setupUi(self)
|
||||||
self.exporter: Optional[Exporter] = None
|
self.exporter: Exporter | None = None
|
||||||
self.cids = cids
|
self.cids = cids
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
self.setup(did)
|
self.setup(did)
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def setup(self, did: Optional[DeckId]) -> None:
|
def setup(self, did: DeckId | None) -> None:
|
||||||
self.exporters = exporters(self.col)
|
self.exporters = exporters(self.col)
|
||||||
# if a deck specified, start with .apkg type selected
|
# if a deck specified, start with .apkg type selected
|
||||||
idx = 0
|
idx = 0
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import List, Optional, Tuple
|
from __future__ import annotations
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChangesWithId, SearchNode
|
from anki.collection import OpChangesWithId, SearchNode
|
||||||
|
@ -36,8 +36,8 @@ class FilteredDeckConfigDialog(QDialog):
|
||||||
self,
|
self,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
deck_id: DeckId = DeckId(0),
|
deck_id: DeckId = DeckId(0),
|
||||||
search: Optional[str] = None,
|
search: str | None = None,
|
||||||
search_2: Optional[str] = None,
|
search_2: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""If 'deck_id' is non-zero, load and modify its settings.
|
"""If 'deck_id' is non-zero, load and modify its settings.
|
||||||
Otherwise, build a new deck and derive settings from the current deck.
|
Otherwise, build a new deck and derive settings from the current deck.
|
||||||
|
@ -162,15 +162,13 @@ class FilteredDeckConfigDialog(QDialog):
|
||||||
def reopen(
|
def reopen(
|
||||||
self,
|
self,
|
||||||
_mw: AnkiQt,
|
_mw: AnkiQt,
|
||||||
search: Optional[str] = None,
|
search: str | None = None,
|
||||||
search_2: Optional[str] = None,
|
search_2: str | None = None,
|
||||||
_deck: Optional[DeckDict] = None,
|
_deck: DeckDict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.set_custom_searches(search, search_2)
|
self.set_custom_searches(search, search_2)
|
||||||
|
|
||||||
def set_custom_searches(
|
def set_custom_searches(self, search: str | None, search_2: str | None) -> None:
|
||||||
self, search: Optional[str], search_2: Optional[str]
|
|
||||||
) -> None:
|
|
||||||
if search is not None:
|
if search is not None:
|
||||||
self.form.search.setText(search)
|
self.form.search.setText(search)
|
||||||
self.form.search.setFocus()
|
self.form.search.setFocus()
|
||||||
|
@ -218,12 +216,12 @@ class FilteredDeckConfigDialog(QDialog):
|
||||||
else:
|
else:
|
||||||
aqt.dialogs.open("Browser", self.mw, search=(search,))
|
aqt.dialogs.open("Browser", self.mw, search=(search,))
|
||||||
|
|
||||||
def _second_filter(self) -> Tuple[str, ...]:
|
def _second_filter(self) -> tuple[str, ...]:
|
||||||
if self.form.secondFilter.isChecked():
|
if self.form.secondFilter.isChecked():
|
||||||
return (self.form.search_2.text(),)
|
return (self.form.search_2.text(),)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def _learning_search_node(self) -> Tuple[SearchNode, ...]:
|
def _learning_search_node(self) -> tuple[SearchNode, ...]:
|
||||||
"""Return a search node that matches learning cards if the old scheduler is enabled.
|
"""Return a search node that matches learning cards if the old scheduler is enabled.
|
||||||
If it's a rebuild, exclude cards from this filtered deck as those will be reset.
|
If it's a rebuild, exclude cards from this filtered deck as those will be reset.
|
||||||
"""
|
"""
|
||||||
|
@ -238,7 +236,7 @@ class FilteredDeckConfigDialog(QDialog):
|
||||||
return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
|
return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def _filtered_search_node(self) -> Tuple[SearchNode]:
|
def _filtered_search_node(self) -> tuple[SearchNode]:
|
||||||
"""Return a search node that matches cards in filtered decks, if applicable excluding those
|
"""Return a search node that matches cards in filtered decks, if applicable excluding those
|
||||||
in the deck being rebuild."""
|
in the deck being rebuild."""
|
||||||
if self.deck.id:
|
if self.deck.id:
|
||||||
|
@ -320,12 +318,12 @@ class FilteredDeckConfigDialog(QDialog):
|
||||||
########################################################
|
########################################################
|
||||||
# fixme: remove once we drop support for v1
|
# fixme: remove once we drop support for v1
|
||||||
|
|
||||||
def listToUser(self, values: List[Union[float, int]]) -> str:
|
def listToUser(self, values: list[Union[float, int]]) -> str:
|
||||||
return " ".join(
|
return " ".join(
|
||||||
[str(int(val)) if int(val) == val else str(val) for val in values]
|
[str(int(val)) if int(val) == val else str(val) for val in values]
|
||||||
)
|
)
|
||||||
|
|
||||||
def userToList(self, line: QLineEdit, minSize: int = 1) -> Optional[List[float]]:
|
def userToList(self, line: QLineEdit, minSize: int = 1) -> list[float] | None:
|
||||||
items = str(line.text()).split(" ")
|
items = str(line.text()).split(" ")
|
||||||
ret = []
|
ret = []
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, cast
|
from typing import cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import SearchNode
|
from anki.collection import SearchNode
|
||||||
|
@ -33,9 +33,9 @@ class Flag:
|
||||||
class FlagManager:
|
class FlagManager:
|
||||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._flags: Optional[List[Flag]] = None
|
self._flags: list[Flag] | None = None
|
||||||
|
|
||||||
def all(self) -> List[Flag]:
|
def all(self) -> list[Flag]:
|
||||||
"""Return a list of all flags."""
|
"""Return a list of all flags."""
|
||||||
if self._flags is None:
|
if self._flags is None:
|
||||||
self._load_flags()
|
self._load_flags()
|
||||||
|
@ -55,7 +55,7 @@ class FlagManager:
|
||||||
gui_hooks.flag_label_did_change()
|
gui_hooks.flag_label_did_change()
|
||||||
|
|
||||||
def _load_flags(self) -> None:
|
def _load_flags(self) -> None:
|
||||||
labels = cast(Dict[str, str], self.mw.col.get_config("flagLabels", {}))
|
labels = cast(dict[str, str], self.mw.col.get_config("flagLabels", {}))
|
||||||
icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED)
|
icon = ColoredIcon(path=":/icons/flag.svg", color=colors.DISABLED)
|
||||||
|
|
||||||
self._flags = [
|
self._flags = [
|
||||||
|
|
|
@ -8,7 +8,7 @@ import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import anki.importing as importing
|
import anki.importing as importing
|
||||||
import aqt.deckchooser
|
import aqt.deckchooser
|
||||||
|
@ -34,7 +34,7 @@ from aqt.utils import (
|
||||||
|
|
||||||
|
|
||||||
class ChangeMap(QDialog):
|
class ChangeMap(QDialog):
|
||||||
def __init__(self, mw: AnkiQt, model: Dict, current: str) -> None:
|
def __init__(self, mw: AnkiQt, model: dict, current: str) -> None:
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
Legacy support
|
Legacy support
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, List
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
|
@ -20,7 +22,7 @@ def bodyClass(col, card) -> str: # type: ignore
|
||||||
return theme_manager.body_classes_for_card_ord(card.ord)
|
return theme_manager.body_classes_for_card_ord(card.ord)
|
||||||
|
|
||||||
|
|
||||||
def allSounds(text) -> List: # type: ignore
|
def allSounds(text) -> list: # type: ignore
|
||||||
print("allSounds() deprecated")
|
print("allSounds() deprecated")
|
||||||
return aqt.mw.col.media._extract_filenames(text)
|
return aqt.mw.col.media._extract_filenames(text)
|
||||||
|
|
||||||
|
|
|
@ -13,19 +13,7 @@ import zipfile
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import (
|
from typing import Any, Literal, Sequence, TextIO, TypeVar, cast
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Literal,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
TextIO,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
|
@ -105,13 +93,13 @@ class AnkiQt(QMainWindow):
|
||||||
profileManager: ProfileManagerType,
|
profileManager: ProfileManagerType,
|
||||||
backend: _RustBackend,
|
backend: _RustBackend,
|
||||||
opts: Namespace,
|
opts: Namespace,
|
||||||
args: List[Any],
|
args: list[Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
QMainWindow.__init__(self)
|
QMainWindow.__init__(self)
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self.state: MainWindowState = "startup"
|
self.state: MainWindowState = "startup"
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.col: Optional[Collection] = None
|
self.col: Collection | None = None
|
||||||
self.taskman = TaskManager(self)
|
self.taskman = TaskManager(self)
|
||||||
self.media_syncer = MediaSyncer(self)
|
self.media_syncer = MediaSyncer(self)
|
||||||
aqt.mw = self
|
aqt.mw = self
|
||||||
|
@ -230,7 +218,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.pm.meta["firstRun"] = False
|
self.pm.meta["firstRun"] = False
|
||||||
self.pm.save()
|
self.pm.save()
|
||||||
|
|
||||||
self.pendingImport: Optional[str] = None
|
self.pendingImport: str | None = None
|
||||||
self.restoringBackup = False
|
self.restoringBackup = False
|
||||||
# profile not provided on command line?
|
# profile not provided on command line?
|
||||||
if not self.pm.name:
|
if not self.pm.name:
|
||||||
|
@ -380,7 +368,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
profiles = self.pm.profiles()
|
profiles = self.pm.profiles()
|
||||||
|
|
||||||
def downgrade() -> List[str]:
|
def downgrade() -> list[str]:
|
||||||
return self.pm.downgrade(profiles)
|
return self.pm.downgrade(profiles)
|
||||||
|
|
||||||
def on_done(future: Future) -> None:
|
def on_done(future: Future) -> None:
|
||||||
|
@ -399,7 +387,7 @@ class AnkiQt(QMainWindow):
|
||||||
|
|
||||||
self.taskman.run_in_background(downgrade, on_done)
|
self.taskman.run_in_background(downgrade, on_done)
|
||||||
|
|
||||||
def loadProfile(self, onsuccess: Optional[Callable] = None) -> None:
|
def loadProfile(self, onsuccess: Callable | None = None) -> None:
|
||||||
if not self.loadCollection():
|
if not self.loadCollection():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -678,7 +666,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.maybe_check_for_addon_updates()
|
self.maybe_check_for_addon_updates()
|
||||||
self.deckBrowser.show()
|
self.deckBrowser.show()
|
||||||
|
|
||||||
def _selectedDeck(self) -> Optional[DeckDict]:
|
def _selectedDeck(self) -> DeckDict | None:
|
||||||
did = self.col.decks.selected()
|
did = self.col.decks.selected()
|
||||||
if not self.col.decks.name_if_exists(did):
|
if not self.col.decks.name_if_exists(did):
|
||||||
showInfo(tr.qt_misc_please_select_a_deck())
|
showInfo(tr.qt_misc_please_select_a_deck())
|
||||||
|
@ -721,7 +709,7 @@ class AnkiQt(QMainWindow):
|
||||||
gui_hooks.operation_did_execute(op, None)
|
gui_hooks.operation_did_execute(op, None)
|
||||||
|
|
||||||
def on_operation_did_execute(
|
def on_operation_did_execute(
|
||||||
self, changes: OpChanges, handler: Optional[object]
|
self, changes: OpChanges, handler: object | None
|
||||||
) -> None:
|
) -> None:
|
||||||
"Notify current screen of changes."
|
"Notify current screen of changes."
|
||||||
focused = current_window() == self
|
focused = current_window() == self
|
||||||
|
@ -741,7 +729,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.toolbar.update_sync_status()
|
self.toolbar.update_sync_status()
|
||||||
|
|
||||||
def on_focus_did_change(
|
def on_focus_did_change(
|
||||||
self, new_focus: Optional[QWidget], _old: Optional[QWidget]
|
self, new_focus: QWidget | None, _old: QWidget | None
|
||||||
) -> None:
|
) -> None:
|
||||||
"If main window has received focus, ensure current UI state is updated."
|
"If main window has received focus, ensure current UI state is updated."
|
||||||
if new_focus and new_focus.window() == self:
|
if new_focus and new_focus.window() == self:
|
||||||
|
@ -799,7 +787,7 @@ class AnkiQt(QMainWindow):
|
||||||
self,
|
self,
|
||||||
link: str,
|
link: str,
|
||||||
name: str,
|
name: str,
|
||||||
key: Optional[str] = None,
|
key: str | None = None,
|
||||||
class_: str = "",
|
class_: str = "",
|
||||||
id: str = "",
|
id: str = "",
|
||||||
extra: str = "",
|
extra: str = "",
|
||||||
|
@ -810,8 +798,8 @@ class AnkiQt(QMainWindow):
|
||||||
else:
|
else:
|
||||||
key = ""
|
key = ""
|
||||||
return """
|
return """
|
||||||
<button id="%s" class="%s" onclick="pycmd('%s');return false;"
|
<button id="{}" class="{}" onclick="pycmd('{}');return false;"
|
||||||
title="%s" %s>%s</button>""" % (
|
title="{}" {}>{}</button>""".format(
|
||||||
id,
|
id,
|
||||||
class_,
|
class_,
|
||||||
link,
|
link,
|
||||||
|
@ -878,7 +866,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
|
|
||||||
self.errorHandler = aqt.errors.ErrorHandler(self)
|
self.errorHandler = aqt.errors.ErrorHandler(self)
|
||||||
|
|
||||||
def setupAddons(self, args: Optional[List]) -> None:
|
def setupAddons(self, args: list | None) -> None:
|
||||||
import aqt.addons
|
import aqt.addons
|
||||||
|
|
||||||
self.addonManager = aqt.addons.AddonManager(self)
|
self.addonManager = aqt.addons.AddonManager(self)
|
||||||
|
@ -903,7 +891,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
)
|
)
|
||||||
self.pm.set_last_addon_update_check(intTime())
|
self.pm.set_last_addon_update_check(intTime())
|
||||||
|
|
||||||
def on_updates_installed(self, log: List[DownloadLogEntry]) -> None:
|
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
|
||||||
if log:
|
if log:
|
||||||
show_log_to_user(self, log)
|
show_log_to_user(self, log)
|
||||||
|
|
||||||
|
@ -1024,11 +1012,11 @@ title="%s" %s>%s</button>""" % (
|
||||||
("y", self.on_sync_button_clicked),
|
("y", self.on_sync_button_clicked),
|
||||||
]
|
]
|
||||||
self.applyShortcuts(globalShortcuts)
|
self.applyShortcuts(globalShortcuts)
|
||||||
self.stateShortcuts: List[QShortcut] = []
|
self.stateShortcuts: list[QShortcut] = []
|
||||||
|
|
||||||
def applyShortcuts(
|
def applyShortcuts(
|
||||||
self, shortcuts: Sequence[Tuple[str, Callable]]
|
self, shortcuts: Sequence[tuple[str, Callable]]
|
||||||
) -> List[QShortcut]:
|
) -> list[QShortcut]:
|
||||||
qshortcuts = []
|
qshortcuts = []
|
||||||
for key, fn in shortcuts:
|
for key, fn in shortcuts:
|
||||||
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
|
scut = QShortcut(QKeySequence(key), self, activated=fn) # type: ignore
|
||||||
|
@ -1036,7 +1024,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
qshortcuts.append(scut)
|
qshortcuts.append(scut)
|
||||||
return qshortcuts
|
return qshortcuts
|
||||||
|
|
||||||
def setStateShortcuts(self, shortcuts: List[Tuple[str, Callable]]) -> None:
|
def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None:
|
||||||
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
|
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
|
||||||
# legacy hook
|
# legacy hook
|
||||||
runHook(f"{self.state}StateShortcuts", shortcuts)
|
runHook(f"{self.state}StateShortcuts", shortcuts)
|
||||||
|
@ -1154,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
|
|
||||||
def onDeckConf(self, deck: Optional[DeckDict] = None) -> None:
|
def onDeckConf(self, deck: DeckDict | None = None) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Importing & exporting
|
# Importing & exporting
|
||||||
|
@ -1175,7 +1163,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
|
|
||||||
aqt.importing.onImport(self)
|
aqt.importing.onImport(self)
|
||||||
|
|
||||||
def onExport(self, did: Optional[DeckId] = None) -> None:
|
def onExport(self, did: DeckId | None = None) -> None:
|
||||||
import aqt.exporting
|
import aqt.exporting
|
||||||
|
|
||||||
aqt.exporting.ExportDialog(self, did=did)
|
aqt.exporting.ExportDialog(self, did=did)
|
||||||
|
@ -1246,7 +1234,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
if self.pm.meta.get("suppressUpdate", None) != ver:
|
if self.pm.meta.get("suppressUpdate", None) != ver:
|
||||||
aqt.update.askAndUpdate(self, ver)
|
aqt.update.askAndUpdate(self, ver)
|
||||||
|
|
||||||
def newMsg(self, data: Dict) -> None:
|
def newMsg(self, data: dict) -> None:
|
||||||
aqt.update.showMessages(self, data)
|
aqt.update.showMessages(self, data)
|
||||||
|
|
||||||
def clockIsOff(self, diff: int) -> None:
|
def clockIsOff(self, diff: int) -> None:
|
||||||
|
@ -1299,7 +1287,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||||
gui_hooks.focus_did_change.append(self.on_focus_did_change)
|
gui_hooks.focus_did_change.append(self.on_focus_did_change)
|
||||||
|
|
||||||
self._activeWindowOnPlay: Optional[QWidget] = None
|
self._activeWindowOnPlay: QWidget | None = None
|
||||||
|
|
||||||
def onOdueInvalid(self) -> None:
|
def onOdueInvalid(self) -> None:
|
||||||
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
|
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
|
||||||
|
@ -1486,12 +1474,12 @@ title="%s" %s>%s</button>""" % (
|
||||||
c._render_output = None
|
c._render_output = None
|
||||||
pprint.pprint(c.__dict__)
|
pprint.pprint(c.__dict__)
|
||||||
|
|
||||||
def _debugCard(self) -> Optional[anki.cards.Card]:
|
def _debugCard(self) -> anki.cards.Card | None:
|
||||||
card = self.reviewer.card
|
card = self.reviewer.card
|
||||||
self._card_repr(card)
|
self._card_repr(card)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def _debugBrowserCard(self) -> Optional[anki.cards.Card]:
|
def _debugBrowserCard(self) -> anki.cards.Card | None:
|
||||||
card = aqt.dialogs._dialogs["Browser"][1].card
|
card = aqt.dialogs._dialogs["Browser"][1].card
|
||||||
self._card_repr(card)
|
self._card_repr(card)
|
||||||
return card
|
return card
|
||||||
|
@ -1564,7 +1552,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
_dummy1 = windll
|
_dummy1 = windll
|
||||||
_dummy2 = wintypes
|
_dummy2 = wintypes
|
||||||
|
|
||||||
def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None:
|
def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
|
||||||
if not self.hideMenuAccels:
|
if not self.hideMenuAccels:
|
||||||
return
|
return
|
||||||
tgt = tgt or self
|
tgt = tgt or self
|
||||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import itertools
|
import itertools
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Iterable, List, Optional, Sequence, TypeVar
|
from typing import Iterable, Sequence, TypeVar
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import SearchNode
|
from anki.collection import SearchNode
|
||||||
|
@ -26,7 +26,7 @@ from aqt.utils import (
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def chunked_list(l: Iterable[T], n: int) -> Iterable[List[T]]:
|
def chunked_list(l: Iterable[T], n: int) -> Iterable[list[T]]:
|
||||||
l = iter(l)
|
l = iter(l)
|
||||||
while True:
|
while True:
|
||||||
res = list(itertools.islice(l, n))
|
res = list(itertools.islice(l, n))
|
||||||
|
@ -41,11 +41,11 @@ def check_media_db(mw: aqt.AnkiQt) -> None:
|
||||||
|
|
||||||
|
|
||||||
class MediaChecker:
|
class MediaChecker:
|
||||||
progress_dialog: Optional[aqt.progress.ProgressDialog]
|
progress_dialog: aqt.progress.ProgressDialog | None
|
||||||
|
|
||||||
def __init__(self, mw: aqt.AnkiQt) -> None:
|
def __init__(self, mw: aqt.AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._progress_timer: Optional[QTimer] = None
|
self._progress_timer: QTimer | None = None
|
||||||
|
|
||||||
def check(self) -> None:
|
def check(self) -> None:
|
||||||
self.progress_dialog = self.mw.progress.start()
|
self.progress_dialog = self.mw.progress.start()
|
||||||
|
|
|
@ -11,7 +11,6 @@ import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_cors # type: ignore
|
import flask_cors # type: ignore
|
||||||
|
@ -179,7 +178,7 @@ def allroutes(pathin: str) -> Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _redirectWebExports(path: str) -> Tuple[str, str]:
|
def _redirectWebExports(path: str) -> tuple[str, str]:
|
||||||
# catch /_anki references and rewrite them to web export folder
|
# catch /_anki references and rewrite them to web export folder
|
||||||
targetPath = "_anki/"
|
targetPath = "_anki/"
|
||||||
if path.startswith(targetPath):
|
if path.startswith(targetPath):
|
||||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, List, Optional, Union
|
from typing import Any, Callable, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import Progress
|
from anki.collection import Progress
|
||||||
|
@ -30,8 +30,8 @@ class MediaSyncer:
|
||||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._syncing: bool = False
|
self._syncing: bool = False
|
||||||
self._log: List[LogEntryWithTime] = []
|
self._log: list[LogEntryWithTime] = []
|
||||||
self._progress_timer: Optional[QTimer] = None
|
self._progress_timer: QTimer | None = None
|
||||||
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
||||||
|
|
||||||
def _on_progress(self) -> None:
|
def _on_progress(self) -> None:
|
||||||
|
@ -98,7 +98,7 @@ class MediaSyncer:
|
||||||
self._log_and_notify(tr.sync_media_failed())
|
self._log_and_notify(tr.sync_media_failed())
|
||||||
showWarning(str(exc))
|
showWarning(str(exc))
|
||||||
|
|
||||||
def entries(self) -> List[LogEntryWithTime]:
|
def entries(self) -> list[LogEntryWithTime]:
|
||||||
return self._log
|
return self._log
|
||||||
|
|
||||||
def abort(self) -> None:
|
def abort(self) -> None:
|
||||||
|
@ -125,7 +125,7 @@ class MediaSyncer:
|
||||||
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
|
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
|
||||||
diag.show()
|
diag.show()
|
||||||
|
|
||||||
timer: Optional[QTimer] = None
|
timer: QTimer | None = None
|
||||||
|
|
||||||
def check_finished() -> None:
|
def check_finished() -> None:
|
||||||
if not self.is_syncing():
|
if not self.is_syncing():
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from typing import List, Optional
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -75,7 +76,7 @@ class ModelChooser(QHBoxLayout):
|
||||||
# edit button
|
# edit button
|
||||||
edit = QPushButton(tr.qt_misc_manage(), clicked=self.onEdit) # type: ignore
|
edit = QPushButton(tr.qt_misc_manage(), clicked=self.onEdit) # type: ignore
|
||||||
|
|
||||||
def nameFunc() -> List[str]:
|
def nameFunc() -> list[str]:
|
||||||
return [nt.name for nt in self.deck.models.all_names_and_ids()]
|
return [nt.name for nt in self.deck.models.all_names_and_ids()]
|
||||||
|
|
||||||
ret = StudyDeck(
|
ret = StudyDeck(
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, List, Optional, Sequence
|
from typing import Any, Optional, Sequence
|
||||||
|
|
||||||
import aqt.clayout
|
import aqt.clayout
|
||||||
from anki import stdmodels
|
from anki import stdmodels
|
||||||
|
@ -239,7 +241,7 @@ class AddModel(QDialog):
|
||||||
self.dialog.setupUi(self)
|
self.dialog.setupUi(self)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
# standard models
|
# standard models
|
||||||
self.notetypes: List[
|
self.notetypes: list[
|
||||||
Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
|
Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
|
||||||
] = []
|
] = []
|
||||||
for (name, func) in stdmodels.get_stock_notetypes(self.col):
|
for (name, func) in stdmodels.get_stock_notetypes(self.col):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# coding: utf-8
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# mpv.py - Control mpv from Python using JSON IPC
|
# mpv.py - Control mpv from Python using JSON IPC
|
||||||
|
@ -41,7 +40,7 @@ from distutils.spawn import ( # pylint: disable=import-error,no-name-in-module
|
||||||
find_executable,
|
find_executable,
|
||||||
)
|
)
|
||||||
from queue import Empty, Full, Queue
|
from queue import Empty, Full, Queue
|
||||||
from typing import Dict, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from anki.utils import isWin
|
from anki.utils import isWin
|
||||||
|
|
||||||
|
@ -80,7 +79,7 @@ class MPVBase:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
executable = find_executable("mpv")
|
executable = find_executable("mpv")
|
||||||
popenEnv: Optional[Dict[str, str]] = None
|
popenEnv: Optional[dict[str, str]] = None
|
||||||
|
|
||||||
default_argv = [
|
default_argv = [
|
||||||
"--idle",
|
"--idle",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from typing import List, Optional
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
@ -98,7 +99,7 @@ class NotetypeChooser(QHBoxLayout):
|
||||||
edit = QPushButton(tr.qt_misc_manage())
|
edit = QPushButton(tr.qt_misc_manage())
|
||||||
qconnect(edit.clicked, self.onEdit)
|
qconnect(edit.clicked, self.onEdit)
|
||||||
|
|
||||||
def nameFunc() -> List[str]:
|
def nameFunc() -> list[str]:
|
||||||
return sorted(self.mw.col.models.all_names())
|
return sorted(self.mw.col.models.all_names())
|
||||||
|
|
||||||
ret = StudyDeck(
|
ret = StudyDeck(
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from concurrent.futures._base import Future
|
from concurrent.futures._base import Future
|
||||||
from typing import Any, Callable, Generic, Optional, Protocol, TypeVar, Union
|
from typing import Any, Callable, Generic, Protocol, TypeVar, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import (
|
from anki.collection import (
|
||||||
|
@ -61,26 +61,26 @@ class CollectionOp(Generic[ResultWithChanges]):
|
||||||
passed to `failure` if it is provided.
|
passed to `failure` if it is provided.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_success: Optional[Callable[[ResultWithChanges], Any]] = None
|
_success: Callable[[ResultWithChanges], Any] | None = None
|
||||||
_failure: Optional[Callable[[Exception], Any]] = None
|
_failure: Callable[[Exception], Any] | None = None
|
||||||
|
|
||||||
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._op = op
|
self._op = op
|
||||||
|
|
||||||
def success(
|
def success(
|
||||||
self, success: Optional[Callable[[ResultWithChanges], Any]]
|
self, success: Callable[[ResultWithChanges], Any] | None
|
||||||
) -> CollectionOp[ResultWithChanges]:
|
) -> CollectionOp[ResultWithChanges]:
|
||||||
self._success = success
|
self._success = success
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def failure(
|
def failure(
|
||||||
self, failure: Optional[Callable[[Exception], Any]]
|
self, failure: Callable[[Exception], Any] | None
|
||||||
) -> CollectionOp[ResultWithChanges]:
|
) -> CollectionOp[ResultWithChanges]:
|
||||||
self._failure = failure
|
self._failure = failure
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def run_in_background(self, *, initiator: Optional[object] = None) -> None:
|
def run_in_background(self, *, initiator: object | None = None) -> None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
assert mw
|
assert mw
|
||||||
|
@ -121,7 +121,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
||||||
def _fire_change_hooks_after_op_performed(
|
def _fire_change_hooks_after_op_performed(
|
||||||
self,
|
self,
|
||||||
result: ResultWithChanges,
|
result: ResultWithChanges,
|
||||||
handler: Optional[object],
|
handler: object | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
|
@ -158,8 +158,8 @@ class QueryOp(Generic[T]):
|
||||||
passed to `failure` if it is provided.
|
passed to `failure` if it is provided.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_failure: Optional[Callable[[Exception], Any]] = None
|
_failure: Callable[[Exception], Any] | None = None
|
||||||
_progress: Union[bool, str] = False
|
_progress: bool | str = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -172,11 +172,11 @@ class QueryOp(Generic[T]):
|
||||||
self._op = op
|
self._op = op
|
||||||
self._success = success
|
self._success = success
|
||||||
|
|
||||||
def failure(self, failure: Optional[Callable[[Exception], Any]]) -> QueryOp[T]:
|
def failure(self, failure: Callable[[Exception], Any] | None) -> QueryOp[T]:
|
||||||
self._failure = failure
|
self._failure = failure
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_progress(self, label: Optional[str] = None) -> QueryOp[T]:
|
def with_progress(self, label: str | None = None) -> QueryOp[T]:
|
||||||
self._progress = label or True
|
self._progress = label or True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ class QueryOp(Generic[T]):
|
||||||
def wrapped_op() -> T:
|
def wrapped_op() -> T:
|
||||||
assert mw
|
assert mw
|
||||||
if self._progress:
|
if self._progress:
|
||||||
label: Optional[str]
|
label: str | None
|
||||||
if isinstance(self._progress, str):
|
if isinstance(self._progress, str):
|
||||||
label = self._progress
|
label = self._progress
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
||||||
from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs
|
from anki.decks import DeckCollapseScope, DeckDict, DeckId, UpdateDeckConfigs
|
||||||
|
@ -50,7 +50,7 @@ def add_deck_dialog(
|
||||||
*,
|
*,
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
default_text: str = "",
|
default_text: str = "",
|
||||||
) -> Optional[CollectionOp[OpChangesWithId]]:
|
) -> CollectionOp[OpChangesWithId] | None:
|
||||||
if name := getOnlyText(
|
if name := getOnlyText(
|
||||||
tr.decks_new_deck_name(), default=default_text, parent=parent
|
tr.decks_new_deck_name(), default=default_text, parent=parent
|
||||||
).strip():
|
).strip():
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from anki.collection import OpChanges, OpChangesWithCount
|
from anki.collection import OpChanges, OpChangesWithCount
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
|
@ -43,7 +43,7 @@ def find_and_replace(
|
||||||
search: str,
|
search: str,
|
||||||
replacement: str,
|
replacement: str,
|
||||||
regex: bool,
|
regex: bool,
|
||||||
field_name: Optional[str],
|
field_name: str | None,
|
||||||
match_case: bool,
|
match_case: bool,
|
||||||
) -> CollectionOp[OpChangesWithCount]:
|
) -> CollectionOp[OpChangesWithCount]:
|
||||||
return CollectionOp(
|
return CollectionOp(
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
|
@ -29,8 +29,8 @@ def set_due_date_dialog(
|
||||||
*,
|
*,
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
card_ids: Sequence[CardId],
|
card_ids: Sequence[CardId],
|
||||||
config_key: Optional[Config.String.V],
|
config_key: Config.String.V | None,
|
||||||
) -> Optional[CollectionOp[OpChanges]]:
|
) -> CollectionOp[OpChanges] | None:
|
||||||
assert aqt.mw
|
assert aqt.mw
|
||||||
if not card_ids:
|
if not card_ids:
|
||||||
return None
|
return None
|
||||||
|
@ -77,7 +77,7 @@ def forget_cards(
|
||||||
|
|
||||||
def reposition_new_cards_dialog(
|
def reposition_new_cards_dialog(
|
||||||
*, parent: QWidget, card_ids: Sequence[CardId]
|
*, parent: QWidget, card_ids: Sequence[CardId]
|
||||||
) -> Optional[CollectionOp[OpChangesWithCount]]:
|
) -> CollectionOp[OpChangesWithCount] | None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
assert mw
|
assert mw
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
from typing import Any, Callable
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
|
@ -72,7 +72,7 @@ class Overview:
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if changes.study_queues:
|
if changes.study_queues:
|
||||||
self._refresh_needed = True
|
self._refresh_needed = True
|
||||||
|
@ -115,7 +115,7 @@ class Overview:
|
||||||
openLink(url)
|
openLink(url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
def _shortcutKeys(self) -> list[tuple[str, Callable]]:
|
||||||
return [
|
return [
|
||||||
("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
|
("o", lambda: display_options_for_deck(self.mw.col.decks.current())),
|
||||||
("r", self.rebuild_current_filtered_deck),
|
("r", self.rebuild_current_filtered_deck),
|
||||||
|
@ -202,7 +202,7 @@ class Overview:
|
||||||
def _show_finished_screen(self) -> None:
|
def _show_finished_screen(self) -> None:
|
||||||
self.web.load_ts_page("congrats")
|
self.web.load_ts_page("congrats")
|
||||||
|
|
||||||
def _desc(self, deck: Dict[str, Any]) -> str:
|
def _desc(self, deck: dict[str, Any]) -> str:
|
||||||
if deck["dyn"]:
|
if deck["dyn"]:
|
||||||
desc = tr.studying_this_is_a_special_deck_for()
|
desc = tr.studying_this_is_a_special_deck_for()
|
||||||
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
|
desc += f" {tr.studying_cards_will_be_automatically_returned_to()}"
|
||||||
|
@ -219,19 +219,19 @@ class Overview:
|
||||||
dyn = ""
|
dyn = ""
|
||||||
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
|
return f'<div class="descfont descmid description {dyn}">{desc}</div>'
|
||||||
|
|
||||||
def _table(self) -> Optional[str]:
|
def _table(self) -> str | None:
|
||||||
counts = list(self.mw.col.sched.counts())
|
counts = list(self.mw.col.sched.counts())
|
||||||
but = self.mw.button
|
but = self.mw.button
|
||||||
return """
|
return """
|
||||||
<table width=400 cellpadding=5>
|
<table width=400 cellpadding=5>
|
||||||
<tr><td align=center valign=top>
|
<tr><td align=center valign=top>
|
||||||
<table cellspacing=5>
|
<table cellspacing=5>
|
||||||
<tr><td>%s:</td><td><b><span class=new-count>%s</span></b></td></tr>
|
<tr><td>{}:</td><td><b><span class=new-count>{}</span></b></td></tr>
|
||||||
<tr><td>%s:</td><td><b><span class=learn-count>%s</span></b></td></tr>
|
<tr><td>{}:</td><td><b><span class=learn-count>{}</span></b></td></tr>
|
||||||
<tr><td>%s:</td><td><b><span class=review-count>%s</span></b></td></tr>
|
<tr><td>{}:</td><td><b><span class=review-count>{}</span></b></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td><td align=center>
|
</td><td align=center>
|
||||||
%s</td></tr></table>""" % (
|
{}</td></tr></table>""".format(
|
||||||
tr.actions_new(),
|
tr.actions_new(),
|
||||||
counts[0],
|
counts[0],
|
||||||
tr.scheduling_learning(),
|
tr.scheduling_learning(),
|
||||||
|
|
|
@ -9,7 +9,7 @@ import random
|
||||||
import shutil
|
import shutil
|
||||||
import traceback
|
import traceback
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ class VideoDriver(Enum):
|
||||||
return VideoDriver.Software
|
return VideoDriver.Software
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def all_for_platform() -> List[VideoDriver]:
|
def all_for_platform() -> list[VideoDriver]:
|
||||||
all = [VideoDriver.OpenGL]
|
all = [VideoDriver.OpenGL]
|
||||||
if isWin:
|
if isWin:
|
||||||
all.append(VideoDriver.ANGLE)
|
all.append(VideoDriver.ANGLE)
|
||||||
|
@ -81,7 +81,7 @@ metaConf = dict(
|
||||||
defaultLang=None,
|
defaultLang=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
profileConf: Dict[str, Any] = dict(
|
profileConf: dict[str, Any] = dict(
|
||||||
# profile
|
# profile
|
||||||
mainWindowGeom=None,
|
mainWindowGeom=None,
|
||||||
mainWindowState=None,
|
mainWindowState=None,
|
||||||
|
@ -118,12 +118,12 @@ class AnkiRestart(SystemExit):
|
||||||
|
|
||||||
|
|
||||||
class ProfileManager:
|
class ProfileManager:
|
||||||
def __init__(self, base: Optional[str] = None) -> None: #
|
def __init__(self, base: str | None = None) -> None: #
|
||||||
## Settings which should be forgotten each Anki restart
|
## Settings which should be forgotten each Anki restart
|
||||||
self.session: Dict[str, Any] = {}
|
self.session: dict[str, Any] = {}
|
||||||
self.name: Optional[str] = None
|
self.name: str | None = None
|
||||||
self.db: Optional[DB] = None
|
self.db: DB | None = None
|
||||||
self.profile: Optional[Dict] = None
|
self.profile: dict | None = None
|
||||||
# instantiate base folder
|
# instantiate base folder
|
||||||
self.base: str
|
self.base: str
|
||||||
self._setBaseFolder(base)
|
self._setBaseFolder(base)
|
||||||
|
@ -245,8 +245,8 @@ class ProfileManager:
|
||||||
# Profile load/save
|
# Profile load/save
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def profiles(self) -> List:
|
def profiles(self) -> list:
|
||||||
def names() -> List:
|
def names() -> list:
|
||||||
return self.db.list("select name from profiles where name != '_global'")
|
return self.db.list("select name from profiles where name != '_global'")
|
||||||
|
|
||||||
n = names()
|
n = names()
|
||||||
|
@ -393,7 +393,7 @@ class ProfileManager:
|
||||||
# Downgrade
|
# Downgrade
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def downgrade(self, profiles: List[str]) -> List[str]:
|
def downgrade(self, profiles: list[str]) -> list[str]:
|
||||||
"Downgrade all profiles. Return a list of profiles that couldn't be opened."
|
"Downgrade all profiles. Return a list of profiles that couldn't be opened."
|
||||||
problem_profiles = []
|
problem_profiles = []
|
||||||
for name in profiles:
|
for name in profiles:
|
||||||
|
@ -420,7 +420,7 @@ class ProfileManager:
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
|
def _setBaseFolder(self, cmdlineBase: str | None) -> None:
|
||||||
if cmdlineBase:
|
if cmdlineBase:
|
||||||
self.base = os.path.abspath(cmdlineBase)
|
self.base = os.path.abspath(cmdlineBase)
|
||||||
elif os.environ.get("ANKI_BASE"):
|
elif os.environ.get("ANKI_BASE"):
|
||||||
|
@ -612,13 +612,13 @@ create table if not exists profiles
|
||||||
# Profile-specific
|
# Profile-specific
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def set_sync_key(self, val: Optional[str]) -> None:
|
def set_sync_key(self, val: str | None) -> None:
|
||||||
self.profile["syncKey"] = val
|
self.profile["syncKey"] = val
|
||||||
|
|
||||||
def set_sync_username(self, val: Optional[str]) -> None:
|
def set_sync_username(self, val: str | None) -> None:
|
||||||
self.profile["syncUser"] = val
|
self.profile["syncUser"] = val
|
||||||
|
|
||||||
def set_host_number(self, val: Optional[int]) -> None:
|
def set_host_number(self, val: int | None) -> None:
|
||||||
self.profile["hostNum"] = val or 0
|
self.profile["hostNum"] = val or 0
|
||||||
|
|
||||||
def media_syncing_enabled(self) -> bool:
|
def media_syncing_enabled(self) -> bool:
|
||||||
|
@ -627,7 +627,7 @@ create table if not exists profiles
|
||||||
def auto_syncing_enabled(self) -> bool:
|
def auto_syncing_enabled(self) -> bool:
|
||||||
return self.profile["autoSync"]
|
return self.profile["autoSync"]
|
||||||
|
|
||||||
def sync_auth(self) -> Optional[SyncAuth]:
|
def sync_auth(self) -> SyncAuth | None:
|
||||||
hkey = self.profile.get("syncKey")
|
hkey = self.profile.get("syncKey")
|
||||||
if not hkey:
|
if not hkey:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -19,9 +18,9 @@ class ProgressManager:
|
||||||
self.app = mw.app
|
self.app = mw.app
|
||||||
self.inDB = False
|
self.inDB = False
|
||||||
self.blockUpdates = False
|
self.blockUpdates = False
|
||||||
self._show_timer: Optional[QTimer] = None
|
self._show_timer: QTimer | None = None
|
||||||
self._busy_cursor_timer: Optional[QTimer] = None
|
self._busy_cursor_timer: QTimer | None = None
|
||||||
self._win: Optional[ProgressDialog] = None
|
self._win: ProgressDialog | None = None
|
||||||
self._levels = 0
|
self._levels = 0
|
||||||
|
|
||||||
# Safer timers
|
# Safer timers
|
||||||
|
@ -74,10 +73,10 @@ class ProgressManager:
|
||||||
self,
|
self,
|
||||||
max: int = 0,
|
max: int = 0,
|
||||||
min: int = 0,
|
min: int = 0,
|
||||||
label: Optional[str] = None,
|
label: str | None = None,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
immediate: bool = False,
|
immediate: bool = False,
|
||||||
) -> Optional[ProgressDialog]:
|
) -> ProgressDialog | None:
|
||||||
self._levels += 1
|
self._levels += 1
|
||||||
if self._levels > 1:
|
if self._levels > 1:
|
||||||
return None
|
return None
|
||||||
|
@ -112,11 +111,11 @@ class ProgressManager:
|
||||||
|
|
||||||
def update(
|
def update(
|
||||||
self,
|
self,
|
||||||
label: Optional[str] = None,
|
label: str | None = None,
|
||||||
value: Optional[int] = None,
|
value: int | None = None,
|
||||||
process: bool = True,
|
process: bool = True,
|
||||||
maybeShow: bool = True,
|
maybeShow: bool = True,
|
||||||
max: Optional[int] = None,
|
max: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# print self._min, self._counter, self._max, label, time.time() - self._lastTime
|
# print self._min, self._counter, self._max, label, time.time() - self._lastTime
|
||||||
if not self.mw.inMainThread():
|
if not self.mw.inMainThread():
|
||||||
|
|
|
@ -11,7 +11,7 @@ import re
|
||||||
import unicodedata as ucd
|
import unicodedata as ucd
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast
|
from typing import Any, Callable, Literal, Match, Sequence, cast
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class V3CardInfo:
|
||||||
def top_card(self) -> QueuedCards.QueuedCard:
|
def top_card(self) -> QueuedCards.QueuedCard:
|
||||||
return self.queued_cards.cards[0]
|
return self.queued_cards.cards[0]
|
||||||
|
|
||||||
def counts(self) -> Tuple[int, List[int]]:
|
def counts(self) -> tuple[int, list[int]]:
|
||||||
"Returns (idx, counts)."
|
"Returns (idx, counts)."
|
||||||
counts = [
|
counts = [
|
||||||
self.queued_cards.new_count,
|
self.queued_cards.new_count,
|
||||||
|
@ -117,16 +117,16 @@ class Reviewer:
|
||||||
def __init__(self, mw: AnkiQt) -> None:
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.web = mw.web
|
self.web = mw.web
|
||||||
self.card: Optional[Card] = None
|
self.card: Card | None = None
|
||||||
self.cardQueue: List[Card] = []
|
self.cardQueue: list[Card] = []
|
||||||
self.previous_card: Optional[Card] = None
|
self.previous_card: Card | None = None
|
||||||
self.hadCardQueue = False
|
self.hadCardQueue = False
|
||||||
self._answeredIds: List[CardId] = []
|
self._answeredIds: list[CardId] = []
|
||||||
self._recordedAudio: Optional[str] = None
|
self._recordedAudio: str | None = None
|
||||||
self.typeCorrect: str = None # web init happens before this is set
|
self.typeCorrect: str = None # web init happens before this is set
|
||||||
self.state: Optional[str] = None
|
self.state: str | None = None
|
||||||
self._refresh_needed: Optional[RefreshNeeded] = None
|
self._refresh_needed: RefreshNeeded | None = None
|
||||||
self._v3: Optional[V3CardInfo] = None
|
self._v3: V3CardInfo | None = None
|
||||||
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
|
self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1))
|
||||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||||
hooks.card_did_leech.append(self.onLeech)
|
hooks.card_did_leech.append(self.onLeech)
|
||||||
|
@ -141,7 +141,7 @@ class Reviewer:
|
||||||
self.refresh_if_needed()
|
self.refresh_if_needed()
|
||||||
|
|
||||||
# this is only used by add-ons
|
# this is only used by add-ons
|
||||||
def lastCard(self) -> Optional[Card]:
|
def lastCard(self) -> Card | None:
|
||||||
if self._answeredIds:
|
if self._answeredIds:
|
||||||
if not self.card or self._answeredIds[-1] != self.card.id:
|
if not self.card or self._answeredIds[-1] != self.card.id:
|
||||||
try:
|
try:
|
||||||
|
@ -167,7 +167,7 @@ class Reviewer:
|
||||||
self._refresh_needed = None
|
self._refresh_needed = None
|
||||||
|
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: Optional[object], focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if handler is not self:
|
if handler is not self:
|
||||||
if changes.study_queues:
|
if changes.study_queues:
|
||||||
|
@ -234,7 +234,7 @@ class Reviewer:
|
||||||
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
|
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
|
||||||
self.card.start_timer()
|
self.card.start_timer()
|
||||||
|
|
||||||
def get_next_states(self) -> Optional[NextStates]:
|
def get_next_states(self) -> NextStates | None:
|
||||||
if v3 := self._v3:
|
if v3 := self._v3:
|
||||||
return v3.next_states
|
return v3.next_states
|
||||||
else:
|
else:
|
||||||
|
@ -434,7 +434,7 @@ class Reviewer:
|
||||||
|
|
||||||
def _shortcutKeys(
|
def _shortcutKeys(
|
||||||
self,
|
self,
|
||||||
) -> Sequence[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
|
) -> Sequence[Union[tuple[str, Callable], tuple[Qt.Key, Callable]]]:
|
||||||
return [
|
return [
|
||||||
("e", self.mw.onEditCurrent),
|
("e", self.mw.onEditCurrent),
|
||||||
(" ", self.onEnterKey),
|
(" ", self.onEnterKey),
|
||||||
|
@ -587,7 +587,7 @@ class Reviewer:
|
||||||
# can't pass a string in directly, and can't use re.escape as it
|
# can't pass a string in directly, and can't use re.escape as it
|
||||||
# escapes too much
|
# escapes too much
|
||||||
s = """
|
s = """
|
||||||
<span style="font-family: '%s'; font-size: %spx">%s</span>""" % (
|
<span style="font-family: '{}'; font-size: {}px">{}</span>""".format(
|
||||||
self.typeFont,
|
self.typeFont,
|
||||||
self.typeSize,
|
self.typeSize,
|
||||||
res,
|
res,
|
||||||
|
@ -620,23 +620,23 @@ class Reviewer:
|
||||||
|
|
||||||
def tokenizeComparison(
|
def tokenizeComparison(
|
||||||
self, given: str, correct: str
|
self, given: str, correct: str
|
||||||
) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
|
) -> tuple[list[tuple[bool, str]], list[tuple[bool, str]]]:
|
||||||
# compare in NFC form so accents appear correct
|
# compare in NFC form so accents appear correct
|
||||||
given = ucd.normalize("NFC", given)
|
given = ucd.normalize("NFC", given)
|
||||||
correct = ucd.normalize("NFC", correct)
|
correct = ucd.normalize("NFC", correct)
|
||||||
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
||||||
givenElems: List[Tuple[bool, str]] = []
|
givenElems: list[tuple[bool, str]] = []
|
||||||
correctElems: List[Tuple[bool, str]] = []
|
correctElems: list[tuple[bool, str]] = []
|
||||||
givenPoint = 0
|
givenPoint = 0
|
||||||
correctPoint = 0
|
correctPoint = 0
|
||||||
offby = 0
|
offby = 0
|
||||||
|
|
||||||
def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
|
def logBad(old: int, new: int, s: str, array: list[tuple[bool, str]]) -> None:
|
||||||
if old != new:
|
if old != new:
|
||||||
array.append((False, s[old:new]))
|
array.append((False, s[old:new]))
|
||||||
|
|
||||||
def logGood(
|
def logGood(
|
||||||
start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
|
start: int, cnt: int, s: str, array: list[tuple[bool, str]]
|
||||||
) -> None:
|
) -> None:
|
||||||
if cnt:
|
if cnt:
|
||||||
array.append((True, s[start : start + cnt]))
|
array.append((True, s[start : start + cnt]))
|
||||||
|
@ -737,8 +737,8 @@ time = %(time)d;
|
||||||
|
|
||||||
def _showAnswerButton(self) -> None:
|
def _showAnswerButton(self) -> None:
|
||||||
middle = """
|
middle = """
|
||||||
<span class=stattxt>%s</span><br>
|
<span class=stattxt>{}</span><br>
|
||||||
<button title="%s" id="ansbut" class="focus" onclick='pycmd("ans");'>%s</button>""" % (
|
<button title="{}" id="ansbut" class="focus" onclick='pycmd("ans");'>{}</button>""".format(
|
||||||
self._remaining(),
|
self._remaining(),
|
||||||
tr.actions_shortcut_key(val=tr.studying_space()),
|
tr.actions_shortcut_key(val=tr.studying_space()),
|
||||||
tr.studying_show_answer(),
|
tr.studying_show_answer(),
|
||||||
|
@ -763,10 +763,10 @@ time = %(time)d;
|
||||||
if not self.mw.col.conf["dueCounts"]:
|
if not self.mw.col.conf["dueCounts"]:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
counts: List[Union[int, str]]
|
counts: list[Union[int, str]]
|
||||||
if v3 := self._v3:
|
if v3 := self._v3:
|
||||||
idx, counts_ = v3.counts()
|
idx, counts_ = v3.counts()
|
||||||
counts = cast(List[Union[int, str]], counts_)
|
counts = cast(list[Union[int, str]], counts_)
|
||||||
else:
|
else:
|
||||||
# v1/v2 scheduler
|
# v1/v2 scheduler
|
||||||
if self.hadCardQueue:
|
if self.hadCardQueue:
|
||||||
|
@ -790,10 +790,10 @@ time = %(time)d;
|
||||||
else:
|
else:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def _answerButtonList(self) -> Tuple[Tuple[int, str], ...]:
|
def _answerButtonList(self) -> tuple[tuple[int, str], ...]:
|
||||||
button_count = self.mw.col.sched.answerButtons(self.card)
|
button_count = self.mw.col.sched.answerButtons(self.card)
|
||||||
if button_count == 2:
|
if button_count == 2:
|
||||||
buttons_tuple: Tuple[Tuple[int, str], ...] = (
|
buttons_tuple: tuple[tuple[int, str], ...] = (
|
||||||
(1, tr.studying_again()),
|
(1, tr.studying_again()),
|
||||||
(2, tr.studying_good()),
|
(2, tr.studying_good()),
|
||||||
)
|
)
|
||||||
|
@ -847,7 +847,7 @@ time = %(time)d;
|
||||||
buf += "</tr></table>"
|
buf += "</tr></table>"
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
def _buttonTime(self, i: int, v3_labels: Optional[Sequence[str]] = None) -> str:
|
def _buttonTime(self, i: int, v3_labels: Sequence[str] | None = None) -> str:
|
||||||
if not self.mw.col.conf["estTimes"]:
|
if not self.mw.col.conf["estTimes"]:
|
||||||
return "<div class=spacer></div>"
|
return "<div class=spacer></div>"
|
||||||
if v3_labels:
|
if v3_labels:
|
||||||
|
@ -859,7 +859,7 @@ time = %(time)d;
|
||||||
# Leeches
|
# Leeches
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onLeech(self, card: Optional[Card] = None) -> None:
|
def onLeech(self, card: Card | None = None) -> None:
|
||||||
# for now
|
# for now
|
||||||
s = tr.studying_card_was_a_leech()
|
s = tr.studying_card_was_a_leech()
|
||||||
# v3 scheduler doesn't report this
|
# v3 scheduler doesn't report this
|
||||||
|
@ -891,7 +891,7 @@ time = %(time)d;
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# note the shortcuts listed here also need to be defined above
|
# note the shortcuts listed here also need to be defined above
|
||||||
def _contextMenu(self) -> List[Any]:
|
def _contextMenu(self) -> list[Any]:
|
||||||
currentFlag = self.card and self.card.user_flag()
|
currentFlag = self.card and self.card.user_flag()
|
||||||
opts = [
|
opts = [
|
||||||
[
|
[
|
||||||
|
|
|
@ -15,7 +15,7 @@ import wave
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast
|
from typing import TYPE_CHECKING, Any, Callable, cast
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class Player(ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def rank_for_tag(self, tag: AVTag) -> Optional[int]:
|
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||||
"""How suited this player is to playing tag.
|
"""How suited this player is to playing tag.
|
||||||
|
|
||||||
AVPlayer will choose the player that returns the highest rank
|
AVPlayer will choose the player that returns the highest rank
|
||||||
|
@ -105,7 +105,7 @@ def is_audio_file(fname: str) -> bool:
|
||||||
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
||||||
default_rank = 0
|
default_rank = 0
|
||||||
|
|
||||||
def rank_for_tag(self, tag: AVTag) -> Optional[int]:
|
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||||
if isinstance(tag, SoundOrVideoTag):
|
if isinstance(tag, SoundOrVideoTag):
|
||||||
return self.default_rank
|
return self.default_rank
|
||||||
else:
|
else:
|
||||||
|
@ -115,7 +115,7 @@ class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
||||||
class SoundPlayer(Player): # pylint: disable=abstract-method
|
class SoundPlayer(Player): # pylint: disable=abstract-method
|
||||||
default_rank = 0
|
default_rank = 0
|
||||||
|
|
||||||
def rank_for_tag(self, tag: AVTag) -> Optional[int]:
|
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||||
if isinstance(tag, SoundOrVideoTag) and is_audio_file(tag.filename):
|
if isinstance(tag, SoundOrVideoTag) and is_audio_file(tag.filename):
|
||||||
return self.default_rank
|
return self.default_rank
|
||||||
else:
|
else:
|
||||||
|
@ -125,7 +125,7 @@ class SoundPlayer(Player): # pylint: disable=abstract-method
|
||||||
class VideoPlayer(Player): # pylint: disable=abstract-method
|
class VideoPlayer(Player): # pylint: disable=abstract-method
|
||||||
default_rank = 0
|
default_rank = 0
|
||||||
|
|
||||||
def rank_for_tag(self, tag: AVTag) -> Optional[int]:
|
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||||
if isinstance(tag, SoundOrVideoTag) and not is_audio_file(tag.filename):
|
if isinstance(tag, SoundOrVideoTag) and not is_audio_file(tag.filename):
|
||||||
return self.default_rank
|
return self.default_rank
|
||||||
else:
|
else:
|
||||||
|
@ -137,16 +137,16 @@ class VideoPlayer(Player): # pylint: disable=abstract-method
|
||||||
|
|
||||||
|
|
||||||
class AVPlayer:
|
class AVPlayer:
|
||||||
players: List[Player] = []
|
players: list[Player] = []
|
||||||
# when a new batch of audio is played, should the currently playing
|
# when a new batch of audio is played, should the currently playing
|
||||||
# audio be stopped?
|
# audio be stopped?
|
||||||
interrupt_current_audio = True
|
interrupt_current_audio = True
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._enqueued: List[AVTag] = []
|
self._enqueued: list[AVTag] = []
|
||||||
self.current_player: Optional[Player] = None
|
self.current_player: Player | None = None
|
||||||
|
|
||||||
def play_tags(self, tags: List[AVTag]) -> None:
|
def play_tags(self, tags: list[AVTag]) -> None:
|
||||||
"""Clear the existing queue, then start playing provided tags."""
|
"""Clear the existing queue, then start playing provided tags."""
|
||||||
self.clear_queue_and_maybe_interrupt()
|
self.clear_queue_and_maybe_interrupt()
|
||||||
self._enqueued = tags[:]
|
self._enqueued = tags[:]
|
||||||
|
@ -185,7 +185,7 @@ class AVPlayer:
|
||||||
if self.current_player:
|
if self.current_player:
|
||||||
self.current_player.stop()
|
self.current_player.stop()
|
||||||
|
|
||||||
def _pop_next(self) -> Optional[AVTag]:
|
def _pop_next(self) -> AVTag | None:
|
||||||
if not self._enqueued:
|
if not self._enqueued:
|
||||||
return None
|
return None
|
||||||
return self._enqueued.pop(0)
|
return self._enqueued.pop(0)
|
||||||
|
@ -212,7 +212,7 @@ class AVPlayer:
|
||||||
else:
|
else:
|
||||||
tooltip(f"no players found for {tag}")
|
tooltip(f"no players found for {tag}")
|
||||||
|
|
||||||
def _best_player_for_tag(self, tag: AVTag) -> Optional[Player]:
|
def _best_player_for_tag(self, tag: AVTag) -> Player | None:
|
||||||
ranked = []
|
ranked = []
|
||||||
for p in self.players:
|
for p in self.players:
|
||||||
rank = p.rank_for_tag(tag)
|
rank = p.rank_for_tag(tag)
|
||||||
|
@ -234,7 +234,7 @@ av_player = AVPlayer()
|
||||||
|
|
||||||
# return modified command array that points to bundled command, and return
|
# return modified command array that points to bundled command, and return
|
||||||
# required environment
|
# required environment
|
||||||
def _packagedCmd(cmd: List[str]) -> Tuple[Any, Dict[str, str]]:
|
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
|
||||||
cmd = cmd[:]
|
cmd = cmd[:]
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
@ -275,13 +275,13 @@ def retryWait(proc: subprocess.Popen) -> int:
|
||||||
class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
class SimpleProcessPlayer(Player): # pylint: disable=abstract-method
|
||||||
"A player that invokes a new process for each tag to play."
|
"A player that invokes a new process for each tag to play."
|
||||||
|
|
||||||
args: List[str] = []
|
args: list[str] = []
|
||||||
env: Optional[Dict[str, str]] = None
|
env: dict[str, str] | None = None
|
||||||
|
|
||||||
def __init__(self, taskman: TaskManager) -> None:
|
def __init__(self, taskman: TaskManager) -> None:
|
||||||
self._taskman = taskman
|
self._taskman = taskman
|
||||||
self._terminate_flag = False
|
self._terminate_flag = False
|
||||||
self._process: Optional[subprocess.Popen] = None
|
self._process: subprocess.Popen | None = None
|
||||||
|
|
||||||
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
|
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
|
||||||
self._terminate_flag = False
|
self._terminate_flag = False
|
||||||
|
@ -388,7 +388,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
|
||||||
def __init__(self, base_path: str) -> None:
|
def __init__(self, base_path: str) -> None:
|
||||||
mpvPath, self.popenEnv = _packagedCmd(["mpv"])
|
mpvPath, self.popenEnv = _packagedCmd(["mpv"])
|
||||||
self.executable = mpvPath[0]
|
self.executable = mpvPath[0]
|
||||||
self._on_done: Optional[OnDoneCallback] = None
|
self._on_done: OnDoneCallback | None = None
|
||||||
self.default_argv += [f"--config-dir={base_path}"]
|
self.default_argv += [f"--config-dir={base_path}"]
|
||||||
super().__init__(window_id=None, debug=False)
|
super().__init__(window_id=None, debug=False)
|
||||||
|
|
||||||
|
@ -635,7 +635,7 @@ except:
|
||||||
|
|
||||||
|
|
||||||
PYAU_CHANNELS = 1
|
PYAU_CHANNELS = 1
|
||||||
PYAU_INPUT_INDEX: Optional[int] = None
|
PYAU_INPUT_INDEX: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class PyAudioThreadedRecorder(threading.Thread):
|
class PyAudioThreadedRecorder(threading.Thread):
|
||||||
|
@ -839,7 +839,7 @@ def playFromText(text: Any) -> None:
|
||||||
# legacy globals
|
# legacy globals
|
||||||
_player = play
|
_player = play
|
||||||
_queueEraser = clearAudioQueue
|
_queueEraser = clearAudioQueue
|
||||||
mpvManager: Optional["MpvManager"] = None
|
mpvManager: MpvManager | None = None
|
||||||
|
|
||||||
# add everything from this module into anki.sound for backwards compat
|
# add everything from this module into anki.sound for backwards compat
|
||||||
_exports = [i for i in locals().items() if not i[0].startswith("__")]
|
_exports = [i for i in locals().items() if not i[0].startswith("__")]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChangesWithId
|
from anki.collection import OpChangesWithId
|
||||||
|
@ -34,7 +34,7 @@ class StudyDeck(QDialog):
|
||||||
cancel: bool = True,
|
cancel: bool = True,
|
||||||
parent: Optional[QWidget] = None,
|
parent: Optional[QWidget] = None,
|
||||||
dyn: bool = False,
|
dyn: bool = False,
|
||||||
buttons: Optional[List[Union[str, QPushButton]]] = None,
|
buttons: Optional[list[Union[str, QPushButton]]] = None,
|
||||||
geomKey: str = "default",
|
geomKey: str = "default",
|
||||||
) -> None:
|
) -> None:
|
||||||
QDialog.__init__(self, parent or mw)
|
QDialog.__init__(self, parent or mw)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from aqt import colors
|
from aqt import colors
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -22,8 +20,8 @@ class Switch(QAbstractButton):
|
||||||
radius: int = 10,
|
radius: int = 10,
|
||||||
left_label: str = "",
|
left_label: str = "",
|
||||||
right_label: str = "",
|
right_label: str = "",
|
||||||
left_color: Tuple[str, str] = colors.FLAG4_BG,
|
left_color: tuple[str, str] = colors.FLAG4_BG,
|
||||||
right_color: Tuple[str, str] = colors.FLAG3_BG,
|
right_color: tuple[str, str] = colors.FLAG3_BG,
|
||||||
parent: QWidget = None,
|
parent: QWidget = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import enum
|
import enum
|
||||||
import os
|
import os
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Callable, Tuple
|
from typing import Callable
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.errors import Interrupted, SyncError, SyncErrorKind
|
from anki.errors import Interrupted, SyncError, SyncErrorKind
|
||||||
|
@ -288,7 +288,7 @@ def ask_user_to_decide_direction() -> FullSyncChoice:
|
||||||
|
|
||||||
def get_id_and_pass_from_user(
|
def get_id_and_pass_from_user(
|
||||||
mw: aqt.main.AnkiQt, username: str = "", password: str = ""
|
mw: aqt.main.AnkiQt, username: str = "", password: str = ""
|
||||||
) -> Tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
diag = QDialog(mw)
|
diag = QDialog(mw)
|
||||||
diag.setWindowTitle("Anki")
|
diag.setWindowTitle("Anki")
|
||||||
disable_help_button(diag)
|
disable_help_button(diag)
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Iterable, List, Optional, Union
|
from typing import Iterable
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
|
@ -12,14 +12,14 @@ from aqt.qt import *
|
||||||
|
|
||||||
|
|
||||||
class TagEdit(QLineEdit):
|
class TagEdit(QLineEdit):
|
||||||
_completer: Union[QCompleter, TagCompleter]
|
_completer: QCompleter | TagCompleter
|
||||||
|
|
||||||
lostFocus = pyqtSignal()
|
lostFocus = pyqtSignal()
|
||||||
|
|
||||||
# 0 = tags, 1 = decks
|
# 0 = tags, 1 = decks
|
||||||
def __init__(self, parent: QWidget, type: int = 0) -> None:
|
def __init__(self, parent: QWidget, type: int = 0) -> None:
|
||||||
QLineEdit.__init__(self, parent)
|
QLineEdit.__init__(self, parent)
|
||||||
self.col: Optional[Collection] = None
|
self.col: Collection | None = None
|
||||||
self.model = QStringListModel()
|
self.model = QStringListModel()
|
||||||
self.type = type
|
self.type = type
|
||||||
if type == 0:
|
if type == 0:
|
||||||
|
@ -112,11 +112,11 @@ class TagCompleter(QCompleter):
|
||||||
edit: TagEdit,
|
edit: TagEdit,
|
||||||
) -> None:
|
) -> None:
|
||||||
QCompleter.__init__(self, model, parent)
|
QCompleter.__init__(self, model, parent)
|
||||||
self.tags: List[str] = []
|
self.tags: list[str] = []
|
||||||
self.edit = edit
|
self.edit = edit
|
||||||
self.cursor: Optional[int] = None
|
self.cursor: int | None = None
|
||||||
|
|
||||||
def splitPath(self, tags: str) -> List[str]:
|
def splitPath(self, tags: str) -> list[str]:
|
||||||
stripped_tags = tags.strip()
|
stripped_tags = tags.strip()
|
||||||
stripped_tags = re.sub(" +", " ", stripped_tags)
|
stripped_tags = re.sub(" +", " ", stripped_tags)
|
||||||
self.tags = self.edit.col.tags.split(stripped_tags)
|
self.tags = self.edit.col.tags.split(stripped_tags)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html
|
||||||
from typing import List, Optional
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.lang import with_collapsed_whitespace
|
from anki.lang import with_collapsed_whitespace
|
||||||
|
@ -14,7 +15,7 @@ class TagLimit(QDialog):
|
||||||
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
|
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
|
||||||
QDialog.__init__(self, parent, Qt.Window)
|
QDialog.__init__(self, parent, Qt.Window)
|
||||||
self.tags: str = ""
|
self.tags: str = ""
|
||||||
self.tags_list: List[str] = []
|
self.tags_list: list[str] = []
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.parent_: Optional[CustomStudy] = parent
|
self.parent_: Optional[CustomStudy] = parent
|
||||||
self.deck = self.parent_.deck
|
self.deck = self.parent_.deck
|
||||||
|
|
|
@ -12,7 +12,7 @@ from __future__ import annotations
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from concurrent.futures.thread import ThreadPoolExecutor
|
from concurrent.futures.thread import ThreadPoolExecutor
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class TaskManager(QObject):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self.mw = mw.weakref()
|
self.mw = mw.weakref()
|
||||||
self._executor = ThreadPoolExecutor()
|
self._executor = ThreadPoolExecutor()
|
||||||
self._closures: List[Closure] = []
|
self._closures: list[Closure] = []
|
||||||
self._closures_lock = Lock()
|
self._closures_lock = Lock()
|
||||||
qconnect(self._closures_pending, self._on_closures_pending)
|
qconnect(self._closures_pending, self._on_closures_pending)
|
||||||
|
|
||||||
|
@ -42,8 +42,8 @@ class TaskManager(QObject):
|
||||||
def run_in_background(
|
def run_in_background(
|
||||||
self,
|
self,
|
||||||
task: Callable,
|
task: Callable,
|
||||||
on_done: Optional[Callable[[Future], None]] = None,
|
on_done: Callable[[Future], None] | None = None,
|
||||||
args: Optional[Dict[str, Any]] = None,
|
args: dict[str, Any] | None = None,
|
||||||
) -> Future:
|
) -> Future:
|
||||||
"""Use QueryOp()/CollectionOp() in new code.
|
"""Use QueryOp()/CollectionOp() in new code.
|
||||||
|
|
||||||
|
@ -76,9 +76,9 @@ class TaskManager(QObject):
|
||||||
def with_progress(
|
def with_progress(
|
||||||
self,
|
self,
|
||||||
task: Callable,
|
task: Callable,
|
||||||
on_done: Optional[Callable[[Future], None]] = None,
|
on_done: Callable[[Future], None] | None = None,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
label: Optional[str] = None,
|
label: str | None = None,
|
||||||
immediate: bool = False,
|
immediate: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"Use QueryOp()/CollectionOp() in new code."
|
"Use QueryOp()/CollectionOp() in new code."
|
||||||
|
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from anki.utils import isMac
|
from anki.utils import isMac
|
||||||
from aqt import QApplication, colors, gui_hooks, isWin
|
from aqt import QApplication, colors, gui_hooks, isWin
|
||||||
|
@ -17,7 +16,7 @@ from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt
|
||||||
class ColoredIcon:
|
class ColoredIcon:
|
||||||
path: str
|
path: str
|
||||||
# (day, night)
|
# (day, night)
|
||||||
color: Tuple[str, str]
|
color: tuple[str, str]
|
||||||
|
|
||||||
def current_color(self, night_mode: bool) -> str:
|
def current_color(self, night_mode: bool) -> str:
|
||||||
if night_mode:
|
if night_mode:
|
||||||
|
@ -25,17 +24,17 @@ class ColoredIcon:
|
||||||
else:
|
else:
|
||||||
return self.color[0]
|
return self.color[0]
|
||||||
|
|
||||||
def with_color(self, color: Tuple[str, str]) -> ColoredIcon:
|
def with_color(self, color: tuple[str, str]) -> ColoredIcon:
|
||||||
return ColoredIcon(path=self.path, color=color)
|
return ColoredIcon(path=self.path, color=color)
|
||||||
|
|
||||||
|
|
||||||
class ThemeManager:
|
class ThemeManager:
|
||||||
_night_mode_preference = False
|
_night_mode_preference = False
|
||||||
_icon_cache_light: Dict[str, QIcon] = {}
|
_icon_cache_light: dict[str, QIcon] = {}
|
||||||
_icon_cache_dark: Dict[str, QIcon] = {}
|
_icon_cache_dark: dict[str, QIcon] = {}
|
||||||
_icon_size = 128
|
_icon_size = 128
|
||||||
_dark_mode_available: Optional[bool] = None
|
_dark_mode_available: bool | None = None
|
||||||
default_palette: Optional[QPalette] = None
|
default_palette: QPalette | None = None
|
||||||
|
|
||||||
# Qt applies a gradient to the buttons in dark mode
|
# Qt applies a gradient to the buttons in dark mode
|
||||||
# from about #505050 to #606060.
|
# from about #505050 to #606060.
|
||||||
|
@ -65,7 +64,7 @@ class ThemeManager:
|
||||||
|
|
||||||
night_mode = property(get_night_mode, set_night_mode)
|
night_mode = property(get_night_mode, set_night_mode)
|
||||||
|
|
||||||
def icon_from_resources(self, path: Union[str, ColoredIcon]) -> QIcon:
|
def icon_from_resources(self, path: str | ColoredIcon) -> QIcon:
|
||||||
"Fetch icon from Qt resources, and invert if in night mode."
|
"Fetch icon from Qt resources, and invert if in night mode."
|
||||||
if self.night_mode:
|
if self.night_mode:
|
||||||
cache = self._icon_cache_light
|
cache = self._icon_cache_light
|
||||||
|
@ -101,7 +100,7 @@ class ThemeManager:
|
||||||
|
|
||||||
return cache.setdefault(path, icon)
|
return cache.setdefault(path, icon)
|
||||||
|
|
||||||
def body_class(self, night_mode: Optional[bool] = None) -> str:
|
def body_class(self, night_mode: bool | None = None) -> str:
|
||||||
"Returns space-separated class list for platform/theme."
|
"Returns space-separated class list for platform/theme."
|
||||||
classes = []
|
classes = []
|
||||||
if isWin:
|
if isWin:
|
||||||
|
@ -120,17 +119,17 @@ class ThemeManager:
|
||||||
return " ".join(classes)
|
return " ".join(classes)
|
||||||
|
|
||||||
def body_classes_for_card_ord(
|
def body_classes_for_card_ord(
|
||||||
self, card_ord: int, night_mode: Optional[bool] = None
|
self, card_ord: int, night_mode: bool | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"Returns body classes used when showing a card."
|
"Returns body classes used when showing a card."
|
||||||
return f"card card{card_ord+1} {self.body_class(night_mode)}"
|
return f"card card{card_ord+1} {self.body_class(night_mode)}"
|
||||||
|
|
||||||
def color(self, colors: Tuple[str, str]) -> str:
|
def color(self, colors: tuple[str, str]) -> str:
|
||||||
"""Given day/night colors, return the correct one for the current theme."""
|
"""Given day/night colors, return the correct one for the current theme."""
|
||||||
idx = 1 if self.night_mode else 0
|
idx = 1 if self.night_mode else 0
|
||||||
return colors[idx]
|
return colors[idx]
|
||||||
|
|
||||||
def qcolor(self, colors: Tuple[str, str]) -> QColor:
|
def qcolor(self, colors: tuple[str, str]) -> QColor:
|
||||||
return QColor(self.color(colors))
|
return QColor(self.color(colors))
|
||||||
|
|
||||||
def apply_style(self, app: QApplication) -> None:
|
def apply_style(self, app: QApplication) -> None:
|
||||||
|
@ -166,27 +165,27 @@ QToolTip {
|
||||||
|
|
||||||
if not self.macos_dark_mode():
|
if not self.macos_dark_mode():
|
||||||
buf += """
|
buf += """
|
||||||
QScrollBar { background-color: %s; }
|
QScrollBar {{ background-color: {}; }}
|
||||||
QScrollBar::handle { background-color: %s; border-radius: 5px; }
|
QScrollBar::handle {{ background-color: {}; border-radius: 5px; }}
|
||||||
|
|
||||||
QScrollBar:horizontal { height: 12px; }
|
QScrollBar:horizontal {{ height: 12px; }}
|
||||||
QScrollBar::handle:horizontal { min-width: 50px; }
|
QScrollBar::handle:horizontal {{ min-width: 50px; }}
|
||||||
|
|
||||||
QScrollBar:vertical { width: 12px; }
|
QScrollBar:vertical {{ width: 12px; }}
|
||||||
QScrollBar::handle:vertical { min-height: 50px; }
|
QScrollBar::handle:vertical {{ min-height: 50px; }}
|
||||||
|
|
||||||
QScrollBar::add-line {
|
QScrollBar::add-line {{
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}}
|
||||||
|
|
||||||
QScrollBar::sub-line {
|
QScrollBar::sub-line {{
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}}
|
||||||
|
|
||||||
QTabWidget { background-color: %s; }
|
QTabWidget {{ background-color: {}; }}
|
||||||
""" % (
|
""".format(
|
||||||
self.color(colors.WINDOW_BG),
|
self.color(colors.WINDOW_BG),
|
||||||
# fushion-button-hover-bg
|
# fushion-button-hover-bg
|
||||||
"#656565",
|
"#656565",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.sync import SyncStatus
|
from anki.sync import SyncStatus
|
||||||
|
@ -29,7 +29,7 @@ class Toolbar:
|
||||||
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
|
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.web = web
|
self.web = web
|
||||||
self.link_handlers: Dict[str, Callable] = {
|
self.link_handlers: dict[str, Callable] = {
|
||||||
"study": self._studyLinkHandler,
|
"study": self._studyLinkHandler,
|
||||||
}
|
}
|
||||||
self.web.setFixedHeight(30)
|
self.web.setFixedHeight(30)
|
||||||
|
@ -38,8 +38,8 @@ class Toolbar:
|
||||||
def draw(
|
def draw(
|
||||||
self,
|
self,
|
||||||
buf: str = "",
|
buf: str = "",
|
||||||
web_context: Optional[Any] = None,
|
web_context: Any | None = None,
|
||||||
link_handler: Optional[Callable[[str], Any]] = None,
|
link_handler: Callable[[str], Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
web_context = web_context or TopToolbar(self)
|
web_context = web_context or TopToolbar(self)
|
||||||
link_handler = link_handler or self._linkHandler
|
link_handler = link_handler or self._linkHandler
|
||||||
|
@ -65,8 +65,8 @@ class Toolbar:
|
||||||
cmd: str,
|
cmd: str,
|
||||||
label: str,
|
label: str,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
tip: Optional[str] = None,
|
tip: str | None = None,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generates HTML link element and registers link handler
|
"""Generates HTML link element and registers link handler
|
||||||
|
|
||||||
|
@ -218,8 +218,8 @@ class BottomBar(Toolbar):
|
||||||
def draw(
|
def draw(
|
||||||
self,
|
self,
|
||||||
buf: str = "",
|
buf: str = "",
|
||||||
web_context: Optional[Any] = None,
|
web_context: Any | None = None,
|
||||||
link_handler: Optional[Callable[[str], Any]] = None,
|
link_handler: Callable[[str], Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# note: some screens may override this
|
# note: some screens may override this
|
||||||
web_context = web_context or BottomToolbar(self)
|
web_context = web_context or BottomToolbar(self)
|
||||||
|
|
|
@ -36,7 +36,7 @@ import threading
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Any, List, Optional, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -61,17 +61,17 @@ class TTSVoiceMatch:
|
||||||
|
|
||||||
class TTSPlayer:
|
class TTSPlayer:
|
||||||
default_rank = 0
|
default_rank = 0
|
||||||
_available_voices: Optional[List[TTSVoice]] = None
|
_available_voices: list[TTSVoice] | None = None
|
||||||
|
|
||||||
def get_available_voices(self) -> List[TTSVoice]:
|
def get_available_voices(self) -> list[TTSVoice]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def voices(self) -> List[TTSVoice]:
|
def voices(self) -> list[TTSVoice]:
|
||||||
if self._available_voices is None:
|
if self._available_voices is None:
|
||||||
self._available_voices = self.get_available_voices()
|
self._available_voices = self.get_available_voices()
|
||||||
return self._available_voices
|
return self._available_voices
|
||||||
|
|
||||||
def voice_for_tag(self, tag: TTSTag) -> Optional[TTSVoiceMatch]:
|
def voice_for_tag(self, tag: TTSTag) -> TTSVoiceMatch | None:
|
||||||
avail_voices = self.voices()
|
avail_voices = self.voices()
|
||||||
|
|
||||||
rank = self.default_rank
|
rank = self.default_rank
|
||||||
|
@ -103,7 +103,7 @@ class TTSPlayer:
|
||||||
|
|
||||||
class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
|
class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
|
||||||
# mypy gets confused if rank_for_tag is defined in TTSPlayer
|
# mypy gets confused if rank_for_tag is defined in TTSPlayer
|
||||||
def rank_for_tag(self, tag: AVTag) -> Optional[int]:
|
def rank_for_tag(self, tag: AVTag) -> int | None:
|
||||||
if not isinstance(tag, TTSTag):
|
if not isinstance(tag, TTSTag):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -118,10 +118,10 @@ class TTSProcessPlayer(SimpleProcessPlayer, TTSPlayer):
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def all_tts_voices() -> List[TTSVoice]:
|
def all_tts_voices() -> list[TTSVoice]:
|
||||||
from aqt.sound import av_player
|
from aqt.sound import av_player
|
||||||
|
|
||||||
all_voices: List[TTSVoice] = []
|
all_voices: list[TTSVoice] = []
|
||||||
for p in av_player.players:
|
for p in av_player.players:
|
||||||
getter = getattr(p, "voices", None)
|
getter = getattr(p, "voices", None)
|
||||||
if not getter:
|
if not getter:
|
||||||
|
@ -185,7 +185,7 @@ class MacTTSPlayer(TTSProcessPlayer):
|
||||||
self._process.stdin.close()
|
self._process.stdin.close()
|
||||||
self._wait_for_termination(tag)
|
self._wait_for_termination(tag)
|
||||||
|
|
||||||
def get_available_voices(self) -> List[TTSVoice]:
|
def get_available_voices(self) -> list[TTSVoice]:
|
||||||
cmd = subprocess.run(
|
cmd = subprocess.run(
|
||||||
["say", "-v", "?"], capture_output=True, check=True, encoding="utf8"
|
["say", "-v", "?"], capture_output=True, check=True, encoding="utf8"
|
||||||
)
|
)
|
||||||
|
@ -197,7 +197,7 @@ class MacTTSPlayer(TTSProcessPlayer):
|
||||||
voices.append(voice)
|
voices.append(voice)
|
||||||
return voices
|
return voices
|
||||||
|
|
||||||
def _parse_voice_line(self, line: str) -> Optional[TTSVoice]:
|
def _parse_voice_line(self, line: str) -> TTSVoice | None:
|
||||||
m = self.VOICE_HELP_LINE_RE.match(line)
|
m = self.VOICE_HELP_LINE_RE.match(line)
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
|
@ -484,7 +484,7 @@ if isWin:
|
||||||
except:
|
except:
|
||||||
speaker = None
|
speaker = None
|
||||||
|
|
||||||
def get_available_voices(self) -> List[TTSVoice]:
|
def get_available_voices(self) -> list[TTSVoice]:
|
||||||
if self.speaker is None:
|
if self.speaker is None:
|
||||||
return []
|
return []
|
||||||
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
||||||
|
@ -533,7 +533,7 @@ if isWin:
|
||||||
id: Any
|
id: Any
|
||||||
|
|
||||||
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
|
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
|
||||||
voice_list: List[Any] = []
|
voice_list: list[Any] = []
|
||||||
tmppath = os.path.join(tmpdir(), "tts.wav")
|
tmppath = os.path.join(tmpdir(), "tts.wav")
|
||||||
|
|
||||||
def import_voices(self) -> None:
|
def import_voices(self) -> None:
|
||||||
|
@ -545,7 +545,7 @@ if isWin:
|
||||||
print("winrt tts voices unavailable:", e)
|
print("winrt tts voices unavailable:", e)
|
||||||
self.voice_list = []
|
self.voice_list = []
|
||||||
|
|
||||||
def get_available_voices(self) -> List[TTSVoice]:
|
def get_available_voices(self) -> list[TTSVoice]:
|
||||||
t = threading.Thread(target=self.import_voices)
|
t = threading.Thread(target=self.import_voices)
|
||||||
t.start()
|
t.start()
|
||||||
t.join()
|
t.join()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class LatestVersionFinder(QThread):
|
||||||
self.main = main
|
self.main = main
|
||||||
self.config = main.pm.meta
|
self.config = main.pm.meta
|
||||||
|
|
||||||
def _data(self) -> Dict[str, Any]:
|
def _data(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"ver": versionWithBuild(),
|
"ver": versionWithBuild(),
|
||||||
"os": platDesc(),
|
"os": platDesc(),
|
||||||
|
@ -73,6 +73,6 @@ def askAndUpdate(mw: aqt.AnkiQt, ver: str) -> None:
|
||||||
openLink(aqt.appWebsite)
|
openLink(aqt.appWebsite)
|
||||||
|
|
||||||
|
|
||||||
def showMessages(mw: aqt.AnkiQt, data: Dict) -> None:
|
def showMessages(mw: aqt.AnkiQt, data: dict) -> None:
|
||||||
showText(data["msg"], parent=mw, type="html")
|
showText(data["msg"], parent=mw, type="html")
|
||||||
mw.pm.meta["lastMsg"] = data["msgId"]
|
mw.pm.meta["lastMsg"] = data["msgId"]
|
||||||
|
|
101
qt/aqt/utils.py
101
qt/aqt/utils.py
|
@ -7,18 +7,7 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Any, Literal, Sequence, cast
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
List,
|
|
||||||
Literal,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAction,
|
QAction,
|
||||||
|
@ -79,7 +68,7 @@ def openHelp(section: HelpPageArgument) -> None:
|
||||||
openLink(link)
|
openLink(link)
|
||||||
|
|
||||||
|
|
||||||
def openLink(link: Union[str, QUrl]) -> None:
|
def openLink(link: str | QUrl) -> None:
|
||||||
tooltip(tr.qt_misc_loading(), period=1000)
|
tooltip(tr.qt_misc_loading(), period=1000)
|
||||||
with noBundledLibs():
|
with noBundledLibs():
|
||||||
QDesktopServices.openUrl(QUrl(link))
|
QDesktopServices.openUrl(QUrl(link))
|
||||||
|
@ -87,10 +76,10 @@ def openLink(link: Union[str, QUrl]) -> None:
|
||||||
|
|
||||||
def showWarning(
|
def showWarning(
|
||||||
text: str,
|
text: str,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
help: HelpPageArgument = "",
|
help: HelpPageArgument = "",
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
textFormat: Optional[TextFormat] = None,
|
textFormat: TextFormat | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Show a small warning with an OK button."
|
"Show a small warning with an OK button."
|
||||||
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
||||||
|
@ -98,10 +87,10 @@ def showWarning(
|
||||||
|
|
||||||
def showCritical(
|
def showCritical(
|
||||||
text: str,
|
text: str,
|
||||||
parent: Optional[QDialog] = None,
|
parent: QDialog | None = None,
|
||||||
help: str = "",
|
help: str = "",
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
textFormat: Optional[TextFormat] = None,
|
textFormat: TextFormat | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Show a small critical error with an OK button."
|
"Show a small critical error with an OK button."
|
||||||
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
||||||
|
@ -109,12 +98,12 @@ def showCritical(
|
||||||
|
|
||||||
def showInfo(
|
def showInfo(
|
||||||
text: str,
|
text: str,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
help: HelpPageArgument = "",
|
help: HelpPageArgument = "",
|
||||||
type: str = "info",
|
type: str = "info",
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
textFormat: Optional[TextFormat] = None,
|
textFormat: TextFormat | None = None,
|
||||||
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
|
customBtns: list[QMessageBox.StandardButton] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Show a small info window with an OK button."
|
"Show a small info window with an OK button."
|
||||||
parent_widget: QWidget
|
parent_widget: QWidget
|
||||||
|
@ -157,16 +146,16 @@ def showInfo(
|
||||||
|
|
||||||
def showText(
|
def showText(
|
||||||
txt: str,
|
txt: str,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
type: str = "text",
|
type: str = "text",
|
||||||
run: bool = True,
|
run: bool = True,
|
||||||
geomKey: Optional[str] = None,
|
geomKey: str | None = None,
|
||||||
minWidth: int = 500,
|
minWidth: int = 500,
|
||||||
minHeight: int = 400,
|
minHeight: int = 400,
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
copyBtn: bool = False,
|
copyBtn: bool = False,
|
||||||
plain_text_edit: bool = False,
|
plain_text_edit: bool = False,
|
||||||
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
|
) -> tuple[QDialog, QDialogButtonBox] | None:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
diag = QDialog(parent)
|
diag = QDialog(parent)
|
||||||
|
@ -174,7 +163,7 @@ def showText(
|
||||||
disable_help_button(diag)
|
disable_help_button(diag)
|
||||||
layout = QVBoxLayout(diag)
|
layout = QVBoxLayout(diag)
|
||||||
diag.setLayout(layout)
|
diag.setLayout(layout)
|
||||||
text: Union[QPlainTextEdit, QTextBrowser]
|
text: QPlainTextEdit | QTextBrowser
|
||||||
if plain_text_edit:
|
if plain_text_edit:
|
||||||
# used by the importer
|
# used by the importer
|
||||||
text = QPlainTextEdit()
|
text = QPlainTextEdit()
|
||||||
|
@ -228,7 +217,7 @@ def askUser(
|
||||||
parent: QWidget = None,
|
parent: QWidget = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
defaultno: bool = False,
|
defaultno: bool = False,
|
||||||
msgfunc: Optional[Callable] = None,
|
msgfunc: Callable | None = None,
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"Show a yes/no question. Return true if yes."
|
"Show a yes/no question. Return true if yes."
|
||||||
|
@ -256,13 +245,13 @@ class ButtonedDialog(QMessageBox):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
text: str,
|
text: str,
|
||||||
buttons: List[str],
|
buttons: list[str],
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
):
|
):
|
||||||
QMessageBox.__init__(self, parent)
|
QMessageBox.__init__(self, parent)
|
||||||
self._buttons: List[QPushButton] = []
|
self._buttons: list[QPushButton] = []
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.help = help
|
self.help = help
|
||||||
self.setIcon(QMessageBox.Warning)
|
self.setIcon(QMessageBox.Warning)
|
||||||
|
@ -289,8 +278,8 @@ class ButtonedDialog(QMessageBox):
|
||||||
|
|
||||||
def askUserDialog(
|
def askUserDialog(
|
||||||
text: str,
|
text: str,
|
||||||
buttons: List[str],
|
buttons: list[str],
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
) -> ButtonedDialog:
|
) -> ButtonedDialog:
|
||||||
|
@ -303,10 +292,10 @@ def askUserDialog(
|
||||||
class GetTextDialog(QDialog):
|
class GetTextDialog(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent: Optional[QWidget],
|
parent: QWidget | None,
|
||||||
question: str,
|
question: str,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
edit: Optional[QLineEdit] = None,
|
edit: QLineEdit | None = None,
|
||||||
default: str = "",
|
default: str = "",
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
minWidth: int = 400,
|
minWidth: int = 400,
|
||||||
|
@ -350,14 +339,14 @@ class GetTextDialog(QDialog):
|
||||||
|
|
||||||
def getText(
|
def getText(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
edit: Optional[QLineEdit] = None,
|
edit: QLineEdit | None = None,
|
||||||
default: str = "",
|
default: str = "",
|
||||||
title: str = "Anki",
|
title: str = "Anki",
|
||||||
geomKey: Optional[str] = None,
|
geomKey: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
"Returns (string, succeeded)."
|
"Returns (string, succeeded)."
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
|
@ -384,7 +373,7 @@ def getOnlyText(*args: Any, **kwargs: Any) -> str:
|
||||||
# fixme: these utilities could be combined into a single base class
|
# fixme: these utilities could be combined into a single base class
|
||||||
# unused by Anki, but used by add-ons
|
# unused by Anki, but used by add-ons
|
||||||
def chooseList(
|
def chooseList(
|
||||||
prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
|
prompt: str, choices: list[str], startrow: int = 0, parent: Any = None
|
||||||
) -> int:
|
) -> int:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow()
|
parent = aqt.mw.app.activeWindow()
|
||||||
|
@ -408,7 +397,7 @@ def chooseList(
|
||||||
|
|
||||||
def getTag(
|
def getTag(
|
||||||
parent: QWidget, deck: Collection, question: str, **kwargs: Any
|
parent: QWidget, deck: Collection, question: str, **kwargs: Any
|
||||||
) -> Tuple[str, int]:
|
) -> tuple[str, int]:
|
||||||
from aqt.tagedit import TagEdit
|
from aqt.tagedit import TagEdit
|
||||||
|
|
||||||
te = TagEdit(parent)
|
te = TagEdit(parent)
|
||||||
|
@ -433,12 +422,12 @@ def getFile(
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
title: str,
|
title: str,
|
||||||
# single file returned unless multi=True
|
# single file returned unless multi=True
|
||||||
cb: Optional[Callable[[Union[str, Sequence[str]]], None]],
|
cb: Callable[[str | Sequence[str]], None] | None,
|
||||||
filter: str = "*.*",
|
filter: str = "*.*",
|
||||||
dir: Optional[str] = None,
|
dir: str | None = None,
|
||||||
key: Optional[str] = None,
|
key: str | None = None,
|
||||||
multi: bool = False, # controls whether a single or multiple files is returned
|
multi: bool = False, # controls whether a single or multiple files is returned
|
||||||
) -> Optional[Union[Sequence[str], str]]:
|
) -> Sequence[str] | str | None:
|
||||||
"Ask the user for a file."
|
"Ask the user for a file."
|
||||||
assert not dir or not key
|
assert not dir or not key
|
||||||
if not dir:
|
if not dir:
|
||||||
|
@ -480,7 +469,7 @@ def getSaveFile(
|
||||||
dir_description: str,
|
dir_description: str,
|
||||||
key: str,
|
key: str,
|
||||||
ext: str,
|
ext: str,
|
||||||
fname: Optional[str] = None,
|
fname: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
||||||
variable. The file dialog will default to open with FNAME."""
|
variable. The file dialog will default to open with FNAME."""
|
||||||
|
@ -520,7 +509,7 @@ def saveGeom(widget: QWidget, key: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def restoreGeom(
|
def restoreGeom(
|
||||||
widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
|
widget: QWidget, key: str, offset: int | None = None, adjustSize: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
key += "Geom"
|
key += "Geom"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
|
@ -562,12 +551,12 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
||||||
widget.move(x, y)
|
widget.move(x, y)
|
||||||
|
|
||||||
|
|
||||||
def saveState(widget: Union[QFileDialog, QMainWindow], key: str) -> None:
|
def saveState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
||||||
key += "State"
|
key += "State"
|
||||||
aqt.mw.pm.profile[key] = widget.saveState()
|
aqt.mw.pm.profile[key] = widget.saveState()
|
||||||
|
|
||||||
|
|
||||||
def restoreState(widget: Union[QFileDialog, QMainWindow], key: str) -> None:
|
def restoreState(widget: QFileDialog | QMainWindow, key: str) -> None:
|
||||||
key += "State"
|
key += "State"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
widget.restoreState(aqt.mw.pm.profile[key])
|
widget.restoreState(aqt.mw.pm.profile[key])
|
||||||
|
@ -614,7 +603,7 @@ def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def restore_combo_index_for_session(
|
def restore_combo_index_for_session(
|
||||||
widget: QComboBox, history: List[str], key: str
|
widget: QComboBox, history: list[str], key: str
|
||||||
) -> None:
|
) -> None:
|
||||||
textKey = f"{key}ComboActiveText"
|
textKey = f"{key}ComboActiveText"
|
||||||
indexKey = f"{key}ComboActiveIndex"
|
indexKey = f"{key}ComboActiveIndex"
|
||||||
|
@ -625,7 +614,7 @@ def restore_combo_index_for_session(
|
||||||
widget.setCurrentIndex(index)
|
widget.setCurrentIndex(index)
|
||||||
|
|
||||||
|
|
||||||
def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
|
def save_combo_history(comboBox: QComboBox, history: list[str], name: str) -> str:
|
||||||
name += "BoxHistory"
|
name += "BoxHistory"
|
||||||
text_input = comboBox.lineEdit().text()
|
text_input = comboBox.lineEdit().text()
|
||||||
if text_input in history:
|
if text_input in history:
|
||||||
|
@ -639,7 +628,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> st
|
||||||
return text_input
|
return text_input
|
||||||
|
|
||||||
|
|
||||||
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
|
def restore_combo_history(comboBox: QComboBox, name: str) -> list[str]:
|
||||||
name += "BoxHistory"
|
name += "BoxHistory"
|
||||||
history = aqt.mw.pm.profile.get(name, [])
|
history = aqt.mw.pm.profile.get(name, [])
|
||||||
comboBox.addItems([""] + history)
|
comboBox.addItems([""] + history)
|
||||||
|
@ -693,7 +682,7 @@ def downArrow() -> str:
|
||||||
return "▾"
|
return "▾"
|
||||||
|
|
||||||
|
|
||||||
def current_window() -> Optional[QWidget]:
|
def current_window() -> QWidget | None:
|
||||||
if widget := QApplication.focusWidget():
|
if widget := QApplication.focusWidget():
|
||||||
return widget.window()
|
return widget.window()
|
||||||
else:
|
else:
|
||||||
|
@ -703,14 +692,14 @@ def current_window() -> Optional[QWidget]:
|
||||||
# Tooltips
|
# Tooltips
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_tooltipTimer: Optional[QTimer] = None
|
_tooltipTimer: QTimer | None = None
|
||||||
_tooltipLabel: Optional[QLabel] = None
|
_tooltipLabel: QLabel | None = None
|
||||||
|
|
||||||
|
|
||||||
def tooltip(
|
def tooltip(
|
||||||
msg: str,
|
msg: str,
|
||||||
period: int = 3000,
|
period: int = 3000,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
x_offset: int = 0,
|
x_offset: int = 0,
|
||||||
y_offset: int = 100,
|
y_offset: int = 100,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -785,7 +774,7 @@ class MenuList:
|
||||||
print(
|
print(
|
||||||
"MenuList will be removed; please copy it into your add-on's code if you need it."
|
"MenuList will be removed; please copy it into your add-on's code if you need it."
|
||||||
)
|
)
|
||||||
self.children: List[MenuListChild] = []
|
self.children: list[MenuListChild] = []
|
||||||
|
|
||||||
def addItem(self, title: str, func: Callable) -> MenuItem:
|
def addItem(self, title: str, func: Callable) -> MenuItem:
|
||||||
item = MenuItem(title, func)
|
item = MenuItem(title, func)
|
||||||
|
@ -800,7 +789,7 @@ class MenuList:
|
||||||
self.children.append(submenu)
|
self.children.append(submenu)
|
||||||
return submenu
|
return submenu
|
||||||
|
|
||||||
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
|
def addChild(self, child: SubMenu | QAction | MenuList) -> None:
|
||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
|
|
||||||
def renderTo(self, qmenu: QMenu) -> None:
|
def renderTo(self, qmenu: QMenu) -> None:
|
||||||
|
@ -894,7 +883,7 @@ Add-ons, last update check: {}
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
# adapted from version detection in qutebrowser
|
# adapted from version detection in qutebrowser
|
||||||
def opengl_vendor() -> Optional[str]:
|
def opengl_vendor() -> str | None:
|
||||||
old_context = QOpenGLContext.currentContext()
|
old_context = QOpenGLContext.currentContext()
|
||||||
old_surface = None if old_context is None else old_context.surface()
|
old_surface = None if old_context is None else old_context.surface()
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import dataclasses
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Callable, List, Optional, Sequence, Tuple, cast
|
from typing import Any, Callable, Optional, Sequence, cast
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki.lang import is_rtl
|
from anki.lang import is_rtl
|
||||||
|
@ -206,8 +206,8 @@ class WebContent:
|
||||||
|
|
||||||
body: str = ""
|
body: str = ""
|
||||||
head: str = ""
|
head: str = ""
|
||||||
css: List[str] = dataclasses.field(default_factory=lambda: [])
|
css: list[str] = dataclasses.field(default_factory=lambda: [])
|
||||||
js: List[str] = dataclasses.field(default_factory=lambda: [])
|
js: list[str] = dataclasses.field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
# Main web view
|
# Main web view
|
||||||
|
@ -232,7 +232,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
||||||
|
|
||||||
self._domDone = True
|
self._domDone = True
|
||||||
self._pendingActions: List[Tuple[str, Sequence[Any]]] = []
|
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
||||||
self.requiresCol = True
|
self.requiresCol = True
|
||||||
self.setPage(self._page)
|
self.setPage(self._page)
|
||||||
|
|
||||||
|
@ -415,22 +415,22 @@ border-radius:5px; font-family: Helvetica }"""
|
||||||
font = f'font-size:14px;font-family:"{family}";'
|
font = f'font-size:14px;font-family:"{family}";'
|
||||||
button_style = """
|
button_style = """
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
button{
|
button{{
|
||||||
background-color: %(color_btn)s;
|
background-color: {color_btn};
|
||||||
font-family:"%(family)s"; }
|
font-family:"{family}"; }}
|
||||||
button:focus{ border-color: %(color_hl)s }
|
button:focus{{ border-color: {color_hl} }}
|
||||||
button:active, button:active:hover { background-color: %(color_hl)s; color: %(color_hl_txt)s;}
|
button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}}
|
||||||
/* Input field focus outline */
|
/* Input field focus outline */
|
||||||
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
|
textarea:focus, input:focus, input[type]:focus, .uneditable-input:focus,
|
||||||
div[contenteditable="true"]:focus {
|
div[contenteditable="true"]:focus {{
|
||||||
outline: 0 none;
|
outline: 0 none;
|
||||||
border-color: %(color_hl)s;
|
border-color: {color_hl};
|
||||||
}""" % {
|
}}""".format(
|
||||||
"family": family,
|
family=family,
|
||||||
"color_btn": color_btn,
|
color_btn=color_btn,
|
||||||
"color_hl": color_hl,
|
color_hl=color_hl,
|
||||||
"color_hl_txt": color_hl_txt,
|
color_hl_txt=color_hl_txt,
|
||||||
}
|
)
|
||||||
|
|
||||||
zoom = self.zoomFactor()
|
zoom = self.zoomFactor()
|
||||||
|
|
||||||
|
@ -454,8 +454,8 @@ html {{ {font} }}
|
||||||
def stdHtml(
|
def stdHtml(
|
||||||
self,
|
self,
|
||||||
body: str,
|
body: str,
|
||||||
css: Optional[List[str]] = None,
|
css: Optional[list[str]] = None,
|
||||||
js: Optional[List[str]] = None,
|
js: Optional[list[str]] = None,
|
||||||
head: str = "",
|
head: str = "",
|
||||||
context: Optional[Any] = None,
|
context: Optional[Any] = None,
|
||||||
default_css: bool = True,
|
default_css: bool = True,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
System File Locations
|
System File Locations
|
||||||
Retrieves common system path names on Windows XP/Vista
|
Retrieves common system path names on Windows XP/Vista
|
||||||
|
@ -15,7 +13,7 @@ __author__ = "Ryan Ginstrom"
|
||||||
__description__ = "Retrieves common Windows system paths as Unicode strings"
|
__description__ = "Retrieves common Windows system paths as Unicode strings"
|
||||||
|
|
||||||
|
|
||||||
class PathConstants(object):
|
class PathConstants:
|
||||||
"""
|
"""
|
||||||
Define constants here to avoid dependency on shellcon.
|
Define constants here to avoid dependency on shellcon.
|
||||||
Put it in a class to avoid polluting namespace
|
Put it in a class to avoid polluting namespace
|
||||||
|
|
Loading…
Reference in a new issue