mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
PEP8 for rest of pylib (#1451)
* PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
This commit is contained in:
parent
1fab547d46
commit
9dc3cf216a
56 changed files with 502 additions and 377 deletions
|
@ -57,4 +57,4 @@ good-names =
|
||||||
tr,
|
tr,
|
||||||
db,
|
db,
|
||||||
ok,
|
ok,
|
||||||
ip,
|
ip,
|
||||||
|
|
|
@ -49,7 +49,6 @@ py_library(
|
||||||
requirement("flask"),
|
requirement("flask"),
|
||||||
requirement("waitress"),
|
requirement("waitress"),
|
||||||
requirement("markdown"),
|
requirement("markdown"),
|
||||||
"//pylib/anki/_vendor:stringcase",
|
|
||||||
] + orjson_if_available(),
|
] + orjson_if_available(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,7 +83,6 @@ py_wheel(
|
||||||
"decorator",
|
"decorator",
|
||||||
"protobuf>=3.17",
|
"protobuf>=3.17",
|
||||||
"markdown",
|
"markdown",
|
||||||
"stringcase",
|
|
||||||
"orjson",
|
"orjson",
|
||||||
'psutil; sys_platform == "win32"',
|
'psutil; sys_platform == "win32"',
|
||||||
'distro; sys_platform != "darwin" and sys_platform != "win32"',
|
'distro; sys_platform != "darwin" and sys_platform != "win32"',
|
||||||
|
|
|
@ -66,7 +66,7 @@ class RustBackend(RustBackendGenerated):
|
||||||
) -> None:
|
) -> None:
|
||||||
# pick up global defaults if not provided
|
# pick up global defaults if not provided
|
||||||
if langs is None:
|
if langs is None:
|
||||||
langs = [anki.lang.currentLang]
|
langs = [anki.lang.current_lang]
|
||||||
|
|
||||||
init_msg = backend_pb2.BackendInit(
|
init_msg = backend_pb2.BackendInit(
|
||||||
preferred_langs=langs,
|
preferred_langs=langs,
|
||||||
|
|
|
@ -17,11 +17,12 @@ VariableTarget = tuple[Any, str]
|
||||||
DeprecatedAliasTarget = Union[Callable, VariableTarget]
|
DeprecatedAliasTarget = Union[Callable, VariableTarget]
|
||||||
|
|
||||||
|
|
||||||
def _target_to_string(target: DeprecatedAliasTarget) -> str:
|
def _target_to_string(target: DeprecatedAliasTarget | None) -> str:
|
||||||
|
if target is None:
|
||||||
|
return ""
|
||||||
if name := getattr(target, "__name__", None):
|
if name := getattr(target, "__name__", None):
|
||||||
return name
|
return name
|
||||||
else:
|
return target[1] # type: ignore
|
||||||
return target[1] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def partial_path(full_path: str, components: int) -> str:
|
def partial_path(full_path: str, components: int) -> str:
|
||||||
|
@ -29,14 +30,34 @@ def partial_path(full_path: str, components: int) -> str:
|
||||||
return os.path.join(*path.parts[-components:])
|
return os.path.join(*path.parts[-components:])
|
||||||
|
|
||||||
|
|
||||||
def print_deprecation_warning(msg: str, frame: int = 2) -> None:
|
def print_deprecation_warning(msg: str, frame: int = 1) -> None:
|
||||||
path, linenum, _, _ = traceback.extract_stack(limit=5)[frame]
|
# skip one frame to get to caller
|
||||||
|
# then by default, skip one more frame as caller themself usually wants to
|
||||||
|
# print their own caller
|
||||||
|
path, linenum, _, _ = traceback.extract_stack(limit=frame + 2)[0]
|
||||||
path = partial_path(path, components=3)
|
path = partial_path(path, components=3)
|
||||||
print(f"{path}:{linenum}:{msg}")
|
print(f"{path}:{linenum}:{msg}")
|
||||||
|
|
||||||
|
|
||||||
def _print_warning(old: str, doc: str, frame: int = 1) -> None:
|
def _print_warning(old: str, doc: str, frame: int = 1) -> None:
|
||||||
return print_deprecation_warning(f"{old} is deprecated: {doc}", frame=frame)
|
return print_deprecation_warning(f"{old} is deprecated: {doc}", frame=frame + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_replacement_warning(old: str, new: str, frame: int = 1) -> None:
|
||||||
|
doc = f"please use '{new}'" if new else "please implement your own"
|
||||||
|
_print_warning(old, doc, frame=frame + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_remapped_and_replacement(
|
||||||
|
mixin: DeprecatedNamesMixin | DeprecatedNamesMixinForModule, name: str
|
||||||
|
) -> tuple[str, str | None]:
|
||||||
|
if some_tuple := mixin._deprecated_attributes.get(name):
|
||||||
|
return some_tuple
|
||||||
|
|
||||||
|
remapped = mixin._deprecated_aliases.get(name) or stringcase.snakecase(name)
|
||||||
|
if remapped == name:
|
||||||
|
raise AttributeError
|
||||||
|
return (remapped, remapped)
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedNamesMixin:
|
class DeprecatedNamesMixin:
|
||||||
|
@ -45,7 +66,7 @@ class DeprecatedNamesMixin:
|
||||||
# deprecated name -> new name
|
# deprecated name -> new name
|
||||||
_deprecated_aliases: dict[str, str] = {}
|
_deprecated_aliases: dict[str, str] = {}
|
||||||
# deprecated name -> [new internal name, new name shown to user]
|
# deprecated name -> [new internal name, new name shown to user]
|
||||||
_deprecated_attributes: dict[str, tuple[str, str]] = {}
|
_deprecated_attributes: dict[str, tuple[str, str | None]] = {}
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -53,27 +74,16 @@ class DeprecatedNamesMixin:
|
||||||
@no_type_check
|
@no_type_check
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
try:
|
try:
|
||||||
remapped, replacement = self._get_remapped_and_replacement(name)
|
remapped, replacement = _get_remapped_and_replacement(self, name)
|
||||||
out = getattr(self, remapped)
|
out = getattr(self, remapped)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
_print_warning(f"'{name}'", f"please use '{replacement}'")
|
_print_replacement_warning(name, replacement)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@no_type_check
|
|
||||||
def _get_remapped_and_replacement(self, name: str) -> tuple[str, str]:
|
|
||||||
if some_tuple := self._deprecated_attributes.get(name):
|
|
||||||
return some_tuple
|
|
||||||
|
|
||||||
remapped = self._deprecated_aliases.get(name) or stringcase.snakecase(name)
|
|
||||||
if remapped == name:
|
|
||||||
raise AttributeError
|
|
||||||
return (remapped, remapped)
|
|
||||||
|
|
||||||
@no_type_check
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None:
|
def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None:
|
||||||
"""Manually add aliases that are not a simple transform.
|
"""Manually add aliases that are not a simple transform.
|
||||||
|
@ -84,17 +94,16 @@ 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()}
|
||||||
|
|
||||||
@no_type_check
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_deprecated_attributes(
|
def register_deprecated_attributes(
|
||||||
cls,
|
cls,
|
||||||
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget],
|
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget | None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Manually add deprecated attributes without exact substitutes.
|
"""Manually add deprecated attributes without exact substitutes.
|
||||||
|
|
||||||
Pass a tuple of (alias, replacement), where alias is the attribute's new
|
Pass a tuple of (alias, replacement), where alias is the attribute's new
|
||||||
name (by convention: snakecase, prepended with '_legacy_'), and
|
name (by convention: snakecase, prepended with '_legacy_'), and
|
||||||
replacement is any callable to be used instead in new code.
|
replacement is any callable to be used instead in new code or None.
|
||||||
Also note the docstring of `register_deprecated_aliases`.
|
Also note the docstring of `register_deprecated_aliases`.
|
||||||
|
|
||||||
E.g. given `def oldFunc(args): return new_func(additionalLogic(args))`,
|
E.g. given `def oldFunc(args): return new_func(additionalLogic(args))`,
|
||||||
|
@ -107,7 +116,7 @@ class DeprecatedNamesMixin:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedNamesMixinForModule(DeprecatedNamesMixin):
|
class DeprecatedNamesMixinForModule:
|
||||||
"""Provides the functionality of DeprecatedNamesMixin for modules.
|
"""Provides the functionality of DeprecatedNamesMixin for modules.
|
||||||
|
|
||||||
It can be invoked like this:
|
It can be invoked like this:
|
||||||
|
@ -120,23 +129,40 @@ class DeprecatedNamesMixinForModule(DeprecatedNamesMixin):
|
||||||
def __getattr__(name: str) -> Any:
|
def __getattr__(name: str) -> Any:
|
||||||
return _deprecated_names.__getattr__(name)
|
return _deprecated_names.__getattr__(name)
|
||||||
```
|
```
|
||||||
|
See DeprecatedNamesMixin for more documentation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, module_globals: dict[str, Any]) -> None:
|
def __init__(self, module_globals: dict[str, Any]) -> None:
|
||||||
self.module_globals = module_globals
|
self.module_globals = module_globals
|
||||||
|
self._deprecated_aliases: dict[str, str] = {}
|
||||||
|
self._deprecated_attributes: dict[str, tuple[str, str | None]] = {}
|
||||||
|
|
||||||
|
@no_type_check
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
try:
|
try:
|
||||||
remapped, replacement = self._get_remapped_and_replacement(name)
|
remapped, replacement = _get_remapped_and_replacement(self, name)
|
||||||
out = self.module_globals[remapped]
|
out = self.module_globals[remapped]
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
|
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
_print_warning(f"'{name}'", f"please use '{replacement}'", frame=0)
|
# skip an additional frame as we are called from the module `__getattr__`
|
||||||
|
_print_replacement_warning(name, replacement, frame=2)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def register_deprecated_aliases(self, **kwargs: DeprecatedAliasTarget) -> None:
|
||||||
|
self._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
|
||||||
|
|
||||||
|
def register_deprecated_attributes(
|
||||||
|
self,
|
||||||
|
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget | None],
|
||||||
|
) -> None:
|
||||||
|
self._deprecated_attributes = {
|
||||||
|
k: (_target_to_string(v[0]), _target_to_string(v[1]))
|
||||||
|
for k, v in kwargs.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def deprecated(replaced_by: Callable | None = 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`."""
|
||||||
|
@ -144,15 +170,34 @@ def deprecated(replaced_by: Callable | None = None, info: str = "") -> Callable:
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
||||||
if replaced_by:
|
if info:
|
||||||
doc = f"please use {replaced_by.__name__} instead."
|
_print_warning(f"{func.__name__}()", info)
|
||||||
else:
|
else:
|
||||||
doc = info
|
_print_replacement_warning(func.__name__, replaced_by.__name__)
|
||||||
|
|
||||||
_print_warning(f"{func.__name__}()", doc)
|
|
||||||
|
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_func
|
return decorated_func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated_keywords(**replaced_keys: str) -> Callable:
|
||||||
|
"""Pass `oldKey="new_key"` to map the former to the latter, if passed to the
|
||||||
|
decorated function as a key word, and print a deprecation warning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
updated_kwargs = {}
|
||||||
|
for key, val in kwargs.items():
|
||||||
|
if replacement := replaced_keys.get(key):
|
||||||
|
_print_replacement_warning(key, replacement)
|
||||||
|
updated_kwargs[replacement or key] = val
|
||||||
|
|
||||||
|
return func(*args, **updated_kwargs)
|
||||||
|
|
||||||
|
return decorated_func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# stringcase 1.2.0 with python warning fix applied
|
# stringcase 1.2.0 with python warning fix applied
|
||||||
# MIT: https://github.com/okunishinishi/python-stringcase
|
# MIT: https://github.com/okunishinishi/python-stringcase
|
||||||
|
|
||||||
# type: ignore
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
String convert functions
|
String convert functions
|
||||||
|
|
|
@ -66,9 +66,9 @@ from anki.types import assert_exhaustive
|
||||||
from anki.utils import (
|
from anki.utils import (
|
||||||
from_json_bytes,
|
from_json_bytes,
|
||||||
ids2str,
|
ids2str,
|
||||||
intTime,
|
int_time,
|
||||||
splitFields,
|
split_fields,
|
||||||
stripHTMLMedia,
|
strip_html_media,
|
||||||
to_json_bytes,
|
to_json_bytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -292,7 +292,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
self.db.begin()
|
self.db.begin()
|
||||||
|
|
||||||
def set_schema_modified(self) -> None:
|
def set_schema_modified(self) -> None:
|
||||||
self.db.execute("update col set scm=?", intTime(1000))
|
self.db.execute("update col set scm=?", int_time(1000))
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def mod_schema(self, check: bool) -> None:
|
def mod_schema(self, check: bool) -> None:
|
||||||
|
@ -578,12 +578,12 @@ class Collection(DeprecatedNamesMixin):
|
||||||
for nid, mid, flds in self.db.all(
|
for nid, mid, flds in self.db.all(
|
||||||
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
||||||
):
|
):
|
||||||
flds = splitFields(flds)
|
flds = split_fields(flds)
|
||||||
ord = ord_for_mid(mid)
|
ord = ord_for_mid(mid)
|
||||||
if ord is None:
|
if ord is None:
|
||||||
continue
|
continue
|
||||||
val = flds[ord]
|
val = flds[ord]
|
||||||
val = stripHTMLMedia(val)
|
val = strip_html_media(val)
|
||||||
# empty does not count as duplicate
|
# empty does not count as duplicate
|
||||||
if not val:
|
if not val:
|
||||||
continue
|
continue
|
||||||
|
@ -982,7 +982,7 @@ class Collection(DeprecatedNamesMixin):
|
||||||
# restore any siblings
|
# restore any siblings
|
||||||
self.db.execute(
|
self.db.execute(
|
||||||
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
|
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
|
||||||
intTime(),
|
int_time(),
|
||||||
self.usn(),
|
self.usn(),
|
||||||
card.nid,
|
card.nid,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -51,9 +53,9 @@ class DBProxy:
|
||||||
**kwargs: ValueForDB,
|
**kwargs: ValueForDB,
|
||||||
) -> list[Row]:
|
) -> list[Row]:
|
||||||
# mark modified?
|
# mark modified?
|
||||||
s = sql.strip().lower()
|
cananoized = sql.strip().lower()
|
||||||
for stmt in "insert", "update", "delete":
|
for stmt in "insert", "update", "delete":
|
||||||
if s.startswith(stmt):
|
if cananoized.startswith(stmt):
|
||||||
self.modified_in_python = True
|
self.modified_in_python = True
|
||||||
sql, args2 = emulate_named_args(sql, args, kwargs)
|
sql, args2 = emulate_named_args(sql, args, kwargs)
|
||||||
# fetch rows
|
# fetch rows
|
||||||
|
@ -113,11 +115,11 @@ def emulate_named_args(
|
||||||
args2 = list(args)
|
args2 = list(args)
|
||||||
for key, val in kwargs.items():
|
for key, val in kwargs.items():
|
||||||
args2.append(val)
|
args2.append(val)
|
||||||
n = len(args2)
|
number = len(args2)
|
||||||
arg_num[key] = n
|
arg_num[key] = number
|
||||||
# update refs
|
# update refs
|
||||||
def repl(m: Match) -> str:
|
def repl(match: Match) -> str:
|
||||||
arg = m.group(1)
|
arg = match.group(1)
|
||||||
return f"?{arg_num[arg]}"
|
return f"?{arg_num[arg]}"
|
||||||
|
|
||||||
sql = re.sub(":([a-zA-Z_0-9]+)", repl, sql)
|
sql = re.sub(":([a-zA-Z_0-9]+)", repl, sql)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from anki.cards import CardId
|
||||||
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes
|
from anki.utils import from_json_bytes, ids2str, int_time, to_json_bytes
|
||||||
|
|
||||||
# public exports
|
# public exports
|
||||||
DeckTreeNode = decks_pb2.DeckTreeNode
|
DeckTreeNode = decks_pb2.DeckTreeNode
|
||||||
|
@ -544,7 +544,7 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
|
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
|
||||||
did,
|
did,
|
||||||
self.col.usn(),
|
self.col.usn(),
|
||||||
intTime(),
|
int_time(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@deprecated(replaced_by=all_names_and_ids)
|
@deprecated(replaced_by=all_names_and_ids)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
|
@ -19,7 +19,7 @@ from anki import hooks
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
from anki.utils import ids2str, namedtmp, splitFields, stripHTML
|
from anki.utils import ids2str, namedtmp, split_fields, strip_html
|
||||||
|
|
||||||
|
|
||||||
class Exporter:
|
class Exporter:
|
||||||
|
@ -78,7 +78,7 @@ class Exporter:
|
||||||
s = text
|
s = text
|
||||||
s = re.sub(r"(?i)<(br ?/?|div|p)>", " ", s)
|
s = re.sub(r"(?i)<(br ?/?|div|p)>", " ", s)
|
||||||
s = re.sub(r"\[sound:[^]]+\]", "", s)
|
s = re.sub(r"\[sound:[^]]+\]", "", s)
|
||||||
s = stripHTML(s)
|
s = strip_html(s)
|
||||||
s = re.sub(r"[ \n\t]+", " ", s)
|
s = re.sub(r"[ \n\t]+", " ", s)
|
||||||
s = s.strip()
|
s = s.strip()
|
||||||
return s
|
return s
|
||||||
|
@ -161,7 +161,7 @@ where cards.id in %s)"""
|
||||||
if self.includeID:
|
if self.includeID:
|
||||||
row.append(str(id))
|
row.append(str(id))
|
||||||
# fields
|
# fields
|
||||||
row.extend([self.processText(f) for f in splitFields(flds)])
|
row.extend([self.processText(f) for f in split_fields(flds)])
|
||||||
# tags
|
# tags
|
||||||
if self.includeTags:
|
if self.includeTags:
|
||||||
row.append(tags.strip())
|
row.append(tags.strip())
|
||||||
|
@ -280,7 +280,7 @@ class AnkiExporter(Exporter):
|
||||||
for row in notedata:
|
for row in notedata:
|
||||||
flds = row[6]
|
flds = row[6]
|
||||||
mid = row[2]
|
mid = row[2]
|
||||||
for file in self.src.media.filesInStr(mid, flds):
|
for file in self.src.media.files_in_str(mid, flds):
|
||||||
# skip files in subdirs
|
# skip files in subdirs
|
||||||
if file != os.path.basename(file):
|
if file != os.path.basename(file):
|
||||||
continue
|
continue
|
||||||
|
@ -310,7 +310,7 @@ class AnkiExporter(Exporter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def removeSystemTags(self, tags: str) -> Any:
|
def removeSystemTags(self, tags: str) -> Any:
|
||||||
return self.src.tags.remFromStr("marked leech", tags)
|
return self.src.tags.rem_from_str("marked leech", tags)
|
||||||
|
|
||||||
def _modelHasMedia(self, model, fname) -> bool:
|
def _modelHasMedia(self, model, fname) -> bool:
|
||||||
# First check the styling
|
# First check the styling
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Wrapper for requests that adds a callback for tracking upload/download progress.
|
Wrapper for requests that adds a callback for tracking upload/download progress.
|
||||||
"""
|
"""
|
||||||
|
@ -14,12 +16,14 @@ from typing import Any, Callable
|
||||||
import requests
|
import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
|
from anki._legacy import DeprecatedNamesMixin
|
||||||
|
|
||||||
HTTP_BUF_SIZE = 64 * 1024
|
HTTP_BUF_SIZE = 64 * 1024
|
||||||
|
|
||||||
ProgressCallback = Callable[[int, int], None]
|
ProgressCallback = Callable[[int, int], None]
|
||||||
|
|
||||||
|
|
||||||
class HttpClient:
|
class HttpClient(DeprecatedNamesMixin):
|
||||||
|
|
||||||
verify = True
|
verify = True
|
||||||
timeout = 60
|
timeout = 60
|
||||||
|
@ -45,7 +49,7 @@ class HttpClient:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response:
|
def post(self, url: str, data: bytes, headers: dict[str, str] | None) -> Response:
|
||||||
headers["User-Agent"] = self._agentName()
|
headers["User-Agent"] = self._agent_name()
|
||||||
return self.session.post(
|
return self.session.post(
|
||||||
url,
|
url,
|
||||||
data=data,
|
data=data,
|
||||||
|
@ -58,12 +62,12 @@ class HttpClient:
|
||||||
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._agent_name()
|
||||||
return self.session.get(
|
return self.session.get(
|
||||||
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
|
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
|
||||||
)
|
)
|
||||||
|
|
||||||
def streamContent(self, resp: Response) -> bytes:
|
def stream_content(self, resp: Response) -> bytes:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
|
@ -73,7 +77,7 @@ class HttpClient:
|
||||||
buf.write(chunk)
|
buf.write(chunk)
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
def _agentName(self) -> str:
|
def _agent_name(self) -> str:
|
||||||
from anki.buildinfo import version
|
from anki.buildinfo import version
|
||||||
|
|
||||||
return f"Anki {version}"
|
return f"Anki {version}"
|
||||||
|
|
|
@ -14,7 +14,7 @@ from anki.decks import DeckId, DeckManager
|
||||||
from anki.importing.base import Importer
|
from anki.importing.base import Importer
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from anki.utils import intTime, joinFields, splitFields, stripHTMLMedia
|
from anki.utils import int_time, join_fields, split_fields, strip_html_media
|
||||||
|
|
||||||
GUID = 1
|
GUID = 1
|
||||||
MID = 2
|
MID = 2
|
||||||
|
@ -82,7 +82,7 @@ class Anki2Importer(Importer):
|
||||||
|
|
||||||
def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
|
def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
|
||||||
self.log.append(
|
self.log.append(
|
||||||
"[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
|
"[{}] {}".format(action, strip_html_media(noteRow[6].replace("\x1f", ", ")))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _importNotes(self) -> None:
|
def _importNotes(self) -> None:
|
||||||
|
@ -282,7 +282,7 @@ class Anki2Importer(Importer):
|
||||||
# if target is a filtered deck, we'll need a new deck name
|
# if target is a filtered deck, we'll need a new deck name
|
||||||
deck = self.dst.decks.by_name(name)
|
deck = self.dst.decks.by_name(name)
|
||||||
if deck and deck["dyn"]:
|
if deck and deck["dyn"]:
|
||||||
name = "%s %d" % (name, intTime())
|
name = "%s %d" % (name, int_time())
|
||||||
# create in local
|
# create in local
|
||||||
newid = self.dst.decks.id(name)
|
newid = self.dst.decks.id(name)
|
||||||
# pull conf over
|
# pull conf over
|
||||||
|
@ -345,7 +345,7 @@ class Anki2Importer(Importer):
|
||||||
# update cid, nid, etc
|
# update cid, nid, etc
|
||||||
card[1] = self._notes[guid][0]
|
card[1] = self._notes[guid][0]
|
||||||
card[2] = self._did(card[2])
|
card[2] = self._did(card[2])
|
||||||
card[4] = intTime()
|
card[4] = int_time()
|
||||||
card[5] = usn
|
card[5] = usn
|
||||||
# review cards have a due date relative to collection
|
# review cards have a due date relative to collection
|
||||||
if (
|
if (
|
||||||
|
@ -434,7 +434,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _mungeMedia(self, mid: NotetypeId, fieldsStr: str) -> str:
|
def _mungeMedia(self, mid: NotetypeId, fieldsStr: str) -> str:
|
||||||
fields = splitFields(fieldsStr)
|
fields = split_fields(fieldsStr)
|
||||||
|
|
||||||
def repl(match):
|
def repl(match):
|
||||||
fname = match.group("fname")
|
fname = match.group("fname")
|
||||||
|
@ -459,8 +459,8 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
|
||||||
return match.group(0).replace(fname, lname)
|
return match.group(0).replace(fname, lname)
|
||||||
|
|
||||||
for idx, field in enumerate(fields):
|
for idx, field in enumerate(fields):
|
||||||
fields[idx] = self.dst.media.transformNames(field, repl)
|
fields[idx] = self.dst.media.transform_names(field, repl)
|
||||||
return joinFields(fields)
|
return join_fields(fields)
|
||||||
|
|
||||||
# Post-import cleanup
|
# Post-import cleanup
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from typing import Any, 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 max_id
|
||||||
|
|
||||||
# Base importer
|
# Base importer
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -41,7 +41,7 @@ class Importer:
|
||||||
# need to make sure our starting point is safe.
|
# need to make sure our starting point is safe.
|
||||||
|
|
||||||
def _prepareTS(self) -> None:
|
def _prepareTS(self) -> None:
|
||||||
self._ts = maxID(self.dst.db)
|
self._ts = max_id(self.dst.db)
|
||||||
|
|
||||||
def ts(self) -> Any:
|
def ts(self) -> Any:
|
||||||
self._ts += 1
|
self._ts += 1
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import cast
|
||||||
|
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||||
from anki.stdmodels import addBasicModel, addClozeModel
|
from anki.stdmodels import _legacy_add_basic_model, _legacy_add_cloze_model
|
||||||
|
|
||||||
|
|
||||||
class MnemosyneImporter(NoteImporter):
|
class MnemosyneImporter(NoteImporter):
|
||||||
|
@ -132,7 +132,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
||||||
data.append(n)
|
data.append(n)
|
||||||
# add a basic model
|
# add a basic model
|
||||||
if not model:
|
if not model:
|
||||||
model = addBasicModel(self.col)
|
model = _legacy_add_basic_model(self.col)
|
||||||
model["name"] = "Mnemosyne-FrontOnly"
|
model["name"] = "Mnemosyne-FrontOnly"
|
||||||
mm = self.col.models
|
mm = self.col.models
|
||||||
mm.save(model)
|
mm.save(model)
|
||||||
|
@ -144,7 +144,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
||||||
self.importNotes(data)
|
self.importNotes(data)
|
||||||
|
|
||||||
def _addFrontBacks(self, notes):
|
def _addFrontBacks(self, notes):
|
||||||
m = addBasicModel(self.col)
|
m = _legacy_add_basic_model(self.col)
|
||||||
m["name"] = "Mnemosyne-FrontBack"
|
m["name"] = "Mnemosyne-FrontBack"
|
||||||
mm = self.col.models
|
mm = self.col.models
|
||||||
t = mm.new_template("Back")
|
t = mm.new_template("Back")
|
||||||
|
@ -200,7 +200,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
||||||
n.cards = orig.get("cards", {})
|
n.cards = orig.get("cards", {})
|
||||||
data.append(n)
|
data.append(n)
|
||||||
# add cloze model
|
# add cloze model
|
||||||
model = addClozeModel(self.col)
|
model = _legacy_add_cloze_model(self.col)
|
||||||
model["name"] = "Mnemosyne-Cloze"
|
model["name"] = "Mnemosyne-Cloze"
|
||||||
mm = self.col.models
|
mm = self.col.models
|
||||||
mm.save(model)
|
mm.save(model)
|
||||||
|
|
|
@ -16,12 +16,12 @@ from anki.importing.base import Importer
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from anki.utils import (
|
from anki.utils import (
|
||||||
fieldChecksum,
|
field_checksum,
|
||||||
guid64,
|
guid64,
|
||||||
intTime,
|
int_time,
|
||||||
joinFields,
|
join_fields,
|
||||||
splitFields,
|
split_fields,
|
||||||
timestampID,
|
timestamp_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
|
TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
|
||||||
|
@ -135,7 +135,7 @@ class NoteImporter(Importer):
|
||||||
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(timestamp_id(self.col.db, "notes"))
|
||||||
# loop through the notes
|
# loop through the notes
|
||||||
updates: list[Updates] = []
|
updates: list[Updates] = []
|
||||||
updateLog = []
|
updateLog = []
|
||||||
|
@ -158,7 +158,7 @@ class NoteImporter(Importer):
|
||||||
self.col.tr.importing_empty_first_field(val=" ".join(n.fields))
|
self.col.tr.importing_empty_first_field(val=" ".join(n.fields))
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
csum = fieldChecksum(fld0)
|
csum = field_checksum(fld0)
|
||||||
# earlier in import?
|
# earlier in import?
|
||||||
if fld0 in firsts and self.importMode != ADD_MODE:
|
if fld0 in firsts and self.importMode != ADD_MODE:
|
||||||
# duplicates in source file; log and ignore
|
# duplicates in source file; log and ignore
|
||||||
|
@ -171,7 +171,7 @@ class NoteImporter(Importer):
|
||||||
# csum is not a guarantee; have to check
|
# csum is not a guarantee; have to check
|
||||||
for id in csums[csum]:
|
for id in csums[csum]:
|
||||||
flds = self.col.db.scalar("select flds from notes where id = ?", id)
|
flds = self.col.db.scalar("select flds from notes where id = ?", id)
|
||||||
sflds = splitFields(flds)
|
sflds = split_fields(flds)
|
||||||
if fld0 == sflds[0]:
|
if fld0 == sflds[0]:
|
||||||
# duplicate
|
# duplicate
|
||||||
found = True
|
found = True
|
||||||
|
@ -246,7 +246,7 @@ class NoteImporter(Importer):
|
||||||
id,
|
id,
|
||||||
guid64(),
|
guid64(),
|
||||||
self.model["id"],
|
self.model["id"],
|
||||||
intTime(),
|
int_time(),
|
||||||
self.col.usn(),
|
self.col.usn(),
|
||||||
self.col.tags.join(n.tags),
|
self.col.tags.join(n.tags),
|
||||||
n.fieldsStr,
|
n.fieldsStr,
|
||||||
|
@ -273,14 +273,22 @@ class NoteImporter(Importer):
|
||||||
self.processFields(n, sflds)
|
self.processFields(n, sflds)
|
||||||
if self._tagsMapped:
|
if self._tagsMapped:
|
||||||
tags = self.col.tags.join(n.tags)
|
tags = self.col.tags.join(n.tags)
|
||||||
return (intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags)
|
return (
|
||||||
|
int_time(),
|
||||||
|
self.col.usn(),
|
||||||
|
n.fieldsStr,
|
||||||
|
tags,
|
||||||
|
id,
|
||||||
|
n.fieldsStr,
|
||||||
|
tags,
|
||||||
|
)
|
||||||
elif self.tagModified:
|
elif self.tagModified:
|
||||||
tags = self.col.db.scalar("select tags from notes where id = ?", id)
|
tags = self.col.db.scalar("select tags from notes where id = ?", id)
|
||||||
tagList = self.col.tags.split(tags) + self.tagModified.split()
|
tagList = self.col.tags.split(tags) + self.tagModified.split()
|
||||||
tags = self.col.tags.join(tagList)
|
tags = self.col.tags.join(tagList)
|
||||||
return (intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr)
|
return (int_time(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr)
|
||||||
else:
|
else:
|
||||||
return (intTime(), self.col.usn(), n.fieldsStr, id, n.fieldsStr)
|
return (int_time(), 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()")
|
||||||
|
@ -321,7 +329,7 @@ where id = ? and flds != ?""",
|
||||||
else:
|
else:
|
||||||
sidx = self._fmap[f][0]
|
sidx = self._fmap[f][0]
|
||||||
fields[sidx] = note.fields[c]
|
fields[sidx] = note.fields[c]
|
||||||
note.fieldsStr = joinFields(fields)
|
note.fieldsStr = join_fields(fields)
|
||||||
# temporary fix for the following issue until we can update the code:
|
# temporary fix for the following issue until we can update the code:
|
||||||
# https://forums.ankiweb.net/t/python-checksum-rust-checksum/8195/16
|
# https://forums.ankiweb.net/t/python-checksum-rust-checksum/8195/16
|
||||||
if self.col.get_config_bool(Config.Bool.NORMALIZE_NOTE_TEXT):
|
if self.col.get_config_bool(Config.Bool.NORMALIZE_NOTE_TEXT):
|
||||||
|
|
|
@ -9,7 +9,7 @@ import time
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||||
from anki.stdmodels import addForwardReverse
|
from anki.stdmodels import _legacy_add_forward_reverse
|
||||||
|
|
||||||
ONE_DAY = 60 * 60 * 24
|
ONE_DAY = 60 * 60 * 24
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class PaukerImporter(NoteImporter):
|
||||||
allowHTML = True
|
allowHTML = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
model = addForwardReverse(self.col)
|
model = _legacy_add_forward_reverse(self.col)
|
||||||
model["name"] = "Pauker"
|
model["name"] = "Pauker"
|
||||||
self.col.models.save(model, updateReqs=False)
|
self.col.models.save(model, updateReqs=False)
|
||||||
self.col.models.set_current(model)
|
self.col.models.set_current(model)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from xml.dom.minidom import Element, Text
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||||
from anki.stdmodels import addBasicModel
|
from anki.stdmodels import _legacy_add_basic_model
|
||||||
|
|
||||||
|
|
||||||
class SmartDict(dict):
|
class SmartDict(dict):
|
||||||
|
@ -87,7 +87,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
"""Initialize internal varables.
|
"""Initialize internal varables.
|
||||||
Pameters to be exposed to GUI are stored in self.META"""
|
Pameters to be exposed to GUI are stored in self.META"""
|
||||||
NoteImporter.__init__(self, col, file)
|
NoteImporter.__init__(self, col, file)
|
||||||
m = addBasicModel(self.col)
|
m = _legacy_add_basic_model(self.col)
|
||||||
m["name"] = "Supermemo"
|
m["name"] = "Supermemo"
|
||||||
self.col.models.save(m)
|
self.col.models.save(m)
|
||||||
self.initMapping()
|
self.initMapping()
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
# 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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import locale
|
import locale
|
||||||
import re
|
import re
|
||||||
import weakref
|
import weakref
|
||||||
|
from typing import Any, no_type_check
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki._backend
|
import anki._backend
|
||||||
import anki.i18n_pb2 as _pb
|
import anki.i18n_pb2 as _pb
|
||||||
|
from anki._legacy import DeprecatedNamesMixinForModule
|
||||||
|
|
||||||
# public exports
|
# public exports
|
||||||
TR = anki._backend.LegacyTranslationEnum
|
TR = anki._backend.LegacyTranslationEnum
|
||||||
|
@ -138,21 +142,21 @@ def lang_to_disk_lang(lang: str) -> str:
|
||||||
):
|
):
|
||||||
return lang.replace("_", "-")
|
return lang.replace("_", "-")
|
||||||
# other languages have the region portion stripped
|
# other languages have the region portion stripped
|
||||||
m = re.match("(.*)_", lang)
|
match = re.match("(.*)_", lang)
|
||||||
if m:
|
if match:
|
||||||
return m.group(1)
|
return match.group(1)
|
||||||
else:
|
else:
|
||||||
return lang
|
return lang
|
||||||
|
|
||||||
|
|
||||||
# the currently set interface language
|
# the currently set interface language
|
||||||
currentLang = "en"
|
current_lang = "en" # pylint: disable=invalid-name
|
||||||
|
|
||||||
# the current Fluent translation instance. Code in pylib/ should
|
# the current Fluent translation instance. Code in pylib/ should
|
||||||
# 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: anki._backend.RustBackend | None = None
|
current_i18n: anki._backend.RustBackend | None = None # pylint: disable=invalid-name
|
||||||
tr_legacyglobal = anki._backend.Translations(None)
|
tr_legacyglobal = anki._backend.Translations(None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -161,14 +165,14 @@ def _(str: str) -> str:
|
||||||
return str
|
return str
|
||||||
|
|
||||||
|
|
||||||
def ngettext(single: str, plural: str, n: int) -> str:
|
def ngettext(single: str, plural: str, num: int) -> str:
|
||||||
print(f"ngettext() is deprecated: {plural}")
|
print(f"ngettext() is deprecated: {plural}")
|
||||||
return plural
|
return plural
|
||||||
|
|
||||||
|
|
||||||
def set_lang(lang: str) -> None:
|
def set_lang(lang: str) -> None:
|
||||||
global currentLang, current_i18n
|
global current_lang, current_i18n # pylint: disable=invalid-name
|
||||||
currentLang = lang
|
current_lang = lang
|
||||||
current_i18n = anki._backend.RustBackend(langs=[lang])
|
current_i18n = anki._backend.RustBackend(langs=[lang])
|
||||||
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
||||||
|
|
||||||
|
@ -186,19 +190,19 @@ def get_def_lang(lang: str | None = None) -> tuple[int, str]:
|
||||||
user_lang = compatMap[user_lang]
|
user_lang = compatMap[user_lang]
|
||||||
idx = None
|
idx = None
|
||||||
lang = None
|
lang = None
|
||||||
en = None
|
en_idx = None
|
||||||
for l in (user_lang, sys_lang):
|
for preferred_lang in (user_lang, sys_lang):
|
||||||
for c, (name, code) in enumerate(langs):
|
for lang_idx, (name, code) in enumerate(langs):
|
||||||
if code == "en_US":
|
if code == "en_US":
|
||||||
en = c
|
en_idx = lang_idx
|
||||||
if code == l:
|
if code == preferred_lang:
|
||||||
idx = c
|
idx = lang_idx
|
||||||
lang = l
|
lang = preferred_lang
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
break
|
break
|
||||||
# if the specified language and the system language aren't available, revert to english
|
# if the specified language and the system language aren't available, revert to english
|
||||||
if idx is None:
|
if idx is None:
|
||||||
idx = en
|
idx = en_idx
|
||||||
lang = "en_US"
|
lang = "en_US"
|
||||||
return (idx, lang)
|
return (idx, lang)
|
||||||
|
|
||||||
|
@ -209,9 +213,17 @@ def is_rtl(lang: str) -> bool:
|
||||||
|
|
||||||
# strip off unicode isolation markers from a translated string
|
# strip off unicode isolation markers from a translated string
|
||||||
# for testing purposes
|
# for testing purposes
|
||||||
def without_unicode_isolation(s: str) -> str:
|
def without_unicode_isolation(string: str) -> str:
|
||||||
return s.replace("\u2068", "").replace("\u2069", "")
|
return string.replace("\u2068", "").replace("\u2069", "")
|
||||||
|
|
||||||
|
|
||||||
def with_collapsed_whitespace(s: str) -> str:
|
def with_collapsed_whitespace(string: str) -> str:
|
||||||
return re.sub(r"\s+", " ", s)
|
return re.sub(r"\s+", " ", string)
|
||||||
|
|
||||||
|
|
||||||
|
_deprecated_names = DeprecatedNamesMixinForModule(globals())
|
||||||
|
|
||||||
|
|
||||||
|
@no_type_check
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
return _deprecated_names.__getattr__(name)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
@ -25,7 +27,8 @@ svgCommands = [
|
||||||
["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"],
|
["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"],
|
||||||
]
|
]
|
||||||
|
|
||||||
build = True # if off, use existing media but don't create new
|
# if off, use existing media but don't create new
|
||||||
|
build = True # pylint: disable=invalid-name
|
||||||
|
|
||||||
# add standard tex install location to osx
|
# add standard tex install location to osx
|
||||||
if isMac:
|
if isMac:
|
||||||
|
@ -135,10 +138,10 @@ def _save_latex_image(
|
||||||
|
|
||||||
# commands to use
|
# commands to use
|
||||||
if svg:
|
if svg:
|
||||||
latexCmds = svgCommands
|
latex_cmds = svgCommands
|
||||||
ext = "svg"
|
ext = "svg"
|
||||||
else:
|
else:
|
||||||
latexCmds = pngCommands
|
latex_cmds = pngCommands
|
||||||
ext = "png"
|
ext = "png"
|
||||||
|
|
||||||
# write into a temp file
|
# write into a temp file
|
||||||
|
@ -152,9 +155,9 @@ def _save_latex_image(
|
||||||
try:
|
try:
|
||||||
# generate png/svg
|
# generate png/svg
|
||||||
os.chdir(tmpdir())
|
os.chdir(tmpdir())
|
||||||
for latexCmd in latexCmds:
|
for latex_cmd in latex_cmds:
|
||||||
if call(latexCmd, stdout=log, stderr=log):
|
if call(latex_cmd, stdout=log, stderr=log):
|
||||||
return _errMsg(col, latexCmd[0], texpath)
|
return _err_msg(col, latex_cmd[0], texpath)
|
||||||
# add to media
|
# add to media
|
||||||
with open(png_or_svg, "rb") as file:
|
with open(png_or_svg, "rb") as file:
|
||||||
data = file.read()
|
data = file.read()
|
||||||
|
@ -166,12 +169,12 @@ def _save_latex_image(
|
||||||
log.close()
|
log.close()
|
||||||
|
|
||||||
|
|
||||||
def _errMsg(col: anki.collection.Collection, type: str, texpath: str) -> Any:
|
def _err_msg(col: anki.collection.Collection, type: str, texpath: str) -> Any:
|
||||||
msg = f"{col.tr.media_error_executing(val=type)}<br>"
|
msg = f"{col.tr.media_error_executing(val=type)}<br>"
|
||||||
msg += f"{col.tr.media_generated_file(val=texpath)}<br>"
|
msg += f"{col.tr.media_generated_file(val=texpath)}<br>"
|
||||||
try:
|
try:
|
||||||
with open(namedtmp("latex_log.txt", rm=False), encoding="utf8") as f:
|
with open(namedtmp("latex_log.txt", remove=False), encoding="utf8") as file:
|
||||||
log = f.read()
|
log = file.read()
|
||||||
if not log:
|
if not log:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
msg += f"<small><pre>{html.escape(log)}</pre></small>"
|
msg += f"<small><pre>{html.escape(log)}</pre></small>"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -11,12 +13,13 @@ import time
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from anki import media_pb2
|
from anki import media_pb2
|
||||||
|
from anki._legacy import DeprecatedNamesMixin, deprecated_keywords
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.latex import render_latex, render_latex_returning_errors
|
from anki.latex import render_latex, render_latex_returning_errors
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from anki.sound import SoundOrVideoTag
|
from anki.sound import SoundOrVideoTag
|
||||||
from anki.template import av_tags_to_native
|
from anki.template import av_tags_to_native
|
||||||
from anki.utils import intTime
|
from anki.utils import int_time
|
||||||
|
|
||||||
|
|
||||||
def media_paths_from_col_path(col_path: str) -> tuple[str, str]:
|
def media_paths_from_col_path(col_path: str) -> tuple[str, str]:
|
||||||
|
@ -33,7 +36,7 @@ CheckMediaResponse = media_pb2.CheckMediaResponse
|
||||||
# - and audio handling code
|
# - and audio handling code
|
||||||
|
|
||||||
|
|
||||||
class MediaManager:
|
class MediaManager(DeprecatedNamesMixin):
|
||||||
|
|
||||||
sound_regexps = [r"(?i)(\[sound:(?P<fname>[^]]+)\])"]
|
sound_regexps = [r"(?i)(\[sound:(?P<fname>[^]]+)\])"]
|
||||||
html_media_regexps = [
|
html_media_regexps = [
|
||||||
|
@ -68,9 +71,9 @@ class MediaManager:
|
||||||
raise Exception("invalidTempFolder") from exc
|
raise Exception("invalidTempFolder") from exc
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
d = dict(self.__dict__)
|
dict_ = dict(self.__dict__)
|
||||||
del d["col"]
|
del dict_["col"]
|
||||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
return f"{super().__repr__()} {pprint.pformat(dict_, width=300)}"
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
if self.col.server:
|
if self.col.server:
|
||||||
|
@ -122,8 +125,8 @@ class MediaManager:
|
||||||
"""Add basename of path to the media folder, renaming if not unique.
|
"""Add basename of path to the media folder, renaming if not unique.
|
||||||
|
|
||||||
Returns possibly-renamed filename."""
|
Returns possibly-renamed filename."""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as file:
|
||||||
return self.write_data(os.path.basename(path), f.read())
|
return self.write_data(os.path.basename(path), file.read())
|
||||||
|
|
||||||
def write_data(self, desired_fname: str, data: bytes) -> str:
|
def write_data(self, desired_fname: str, data: bytes) -> str:
|
||||||
"""Write the file to the media folder, renaming if not unique.
|
"""Write the file to the media folder, renaming if not unique.
|
||||||
|
@ -155,10 +158,11 @@ class MediaManager:
|
||||||
# String manipulation
|
# String manipulation
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def filesInStr(
|
@deprecated_keywords(includeRemote="include_remote")
|
||||||
self, mid: NotetypeId, string: str, includeRemote: bool = False
|
def files_in_str(
|
||||||
|
self, mid: NotetypeId, string: str, include_remote: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
l = []
|
files = []
|
||||||
model = self.col.models.get(mid)
|
model = self.col.models.get(mid)
|
||||||
# handle latex
|
# handle latex
|
||||||
string = render_latex(string, model, self.col)
|
string = render_latex(string, model, self.col)
|
||||||
|
@ -166,12 +170,12 @@ class MediaManager:
|
||||||
for reg in self.regexps:
|
for reg in self.regexps:
|
||||||
for match in re.finditer(reg, string):
|
for match in re.finditer(reg, string):
|
||||||
fname = match.group("fname")
|
fname = match.group("fname")
|
||||||
isLocal = not re.match("(https?|ftp)://", fname.lower())
|
is_local = not re.match("(https?|ftp)://", fname.lower())
|
||||||
if isLocal or includeRemote:
|
if is_local or include_remote:
|
||||||
l.append(fname)
|
files.append(fname)
|
||||||
return l
|
return files
|
||||||
|
|
||||||
def transformNames(self, txt: str, func: Callable) -> Any:
|
def transform_names(self, txt: str, func: Callable) -> Any:
|
||||||
for reg in self.regexps:
|
for reg in self.regexps:
|
||||||
txt = re.sub(reg, func, txt)
|
txt = re.sub(reg, func, txt)
|
||||||
return txt
|
return txt
|
||||||
|
@ -182,7 +186,7 @@ class MediaManager:
|
||||||
txt = re.sub(reg, "", txt)
|
txt = re.sub(reg, "", txt)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def escapeImages(self, string: str, unescape: bool = False) -> str:
|
def escape_images(self, string: str, unescape: bool = False) -> str:
|
||||||
"escape_media_filenames alias for compatibility with add-ons."
|
"escape_media_filenames alias for compatibility with add-ons."
|
||||||
return self.escape_media_filenames(string, unescape)
|
return self.escape_media_filenames(string, unescape)
|
||||||
|
|
||||||
|
@ -229,7 +233,7 @@ class MediaManager:
|
||||||
checked += 1
|
checked += 1
|
||||||
elap = time.time() - last_progress
|
elap = time.time() - last_progress
|
||||||
if elap >= 0.3 and progress_cb is not None:
|
if elap >= 0.3 and progress_cb is not None:
|
||||||
last_progress = intTime()
|
last_progress = int_time()
|
||||||
if not progress_cb(checked):
|
if not progress_cb(checked):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -240,28 +244,35 @@ class MediaManager:
|
||||||
|
|
||||||
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
|
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
|
||||||
|
|
||||||
def stripIllegal(self, str: str) -> str:
|
def _legacy_strip_illegal(self, str: str) -> str:
|
||||||
# currently used by ankiconnect
|
# currently used by ankiconnect
|
||||||
print("stripIllegal() will go away")
|
|
||||||
return re.sub(self._illegalCharReg, "", str)
|
return re.sub(self._illegalCharReg, "", str)
|
||||||
|
|
||||||
def hasIllegal(self, s: str) -> bool:
|
def _legacy_has_illegal(self, string: str) -> bool:
|
||||||
print("hasIllegal() will go away")
|
if re.search(self._illegalCharReg, string):
|
||||||
if re.search(self._illegalCharReg, s):
|
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
s.encode(sys.getfilesystemencoding())
|
string.encode(sys.getfilesystemencoding())
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def findChanges(self) -> None:
|
def _legacy_find_changes(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
addFile = add_file
|
@deprecated_keywords(typeHint="type_hint")
|
||||||
|
def _legacy_write_data(
|
||||||
def writeData(self, opath: str, data: bytes, typeHint: str | None = None) -> str:
|
self, opath: str, data: bytes, type_hint: str | None = None
|
||||||
|
) -> str:
|
||||||
fname = os.path.basename(opath)
|
fname = os.path.basename(opath)
|
||||||
if typeHint:
|
if type_hint:
|
||||||
fname = self.add_extension_based_on_mime(fname, typeHint)
|
fname = self.add_extension_based_on_mime(fname, type_hint)
|
||||||
return self.write_data(fname, data)
|
return self.write_data(fname, data)
|
||||||
|
|
||||||
|
|
||||||
|
MediaManager.register_deprecated_attributes(
|
||||||
|
stripIllegal=(MediaManager._legacy_strip_illegal, None),
|
||||||
|
hasIllegal=(MediaManager._legacy_has_illegal, None),
|
||||||
|
findChanges=(MediaManager._legacy_find_changes, None),
|
||||||
|
writeData=(MediaManager._legacy_write_data, MediaManager.write_data),
|
||||||
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from anki import hooks, notes_pb2
|
||||||
from anki._legacy import DeprecatedNamesMixin
|
from anki._legacy import DeprecatedNamesMixin
|
||||||
from anki.consts import MODEL_STD
|
from anki.consts import MODEL_STD
|
||||||
from anki.models import NotetypeDict, NotetypeId, TemplateDict
|
from anki.models import NotetypeDict, NotetypeId, TemplateDict
|
||||||
from anki.utils import joinFields
|
from anki.utils import join_fields
|
||||||
|
|
||||||
DuplicateOrEmptyResult = notes_pb2.NoteFieldsCheckResponse.State
|
DuplicateOrEmptyResult = notes_pb2.NoteFieldsCheckResponse.State
|
||||||
NoteFieldsCheckResult = notes_pb2.NoteFieldsCheckResponse.State
|
NoteFieldsCheckResult = notes_pb2.NoteFieldsCheckResponse.State
|
||||||
|
@ -84,7 +84,7 @@ class Note(DeprecatedNamesMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
def joined_fields(self) -> str:
|
def joined_fields(self) -> str:
|
||||||
return joinFields(self.fields)
|
return join_fields(self.fields)
|
||||||
|
|
||||||
def ephemeral_card(
|
def ephemeral_card(
|
||||||
self,
|
self,
|
||||||
|
@ -164,7 +164,7 @@ class Note(DeprecatedNamesMixin):
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def has_tag(self, tag: str) -> bool:
|
def has_tag(self, tag: str) -> bool:
|
||||||
return self.col.tags.inList(tag, self.tags)
|
return self.col.tags.in_list(tag, self.tags)
|
||||||
|
|
||||||
def remove_tag(self, tag: str) -> None:
|
def remove_tag(self, tag: str) -> None:
|
||||||
rem = []
|
rem = []
|
||||||
|
@ -179,7 +179,7 @@ class Note(DeprecatedNamesMixin):
|
||||||
self.tags.append(tag)
|
self.tags.append(tag)
|
||||||
|
|
||||||
def string_tags(self) -> Any:
|
def string_tags(self) -> Any:
|
||||||
return self.col.tags.join(self.col.tags.canonify(self.tags))
|
return self.col.tags.join(self.tags)
|
||||||
|
|
||||||
def set_tags_from_str(self, tags: str) -> None:
|
def set_tags_from_str(self, tags: str) -> None:
|
||||||
self.tags = self.col.tags.split(tags)
|
self.tags = self.col.tags.split(tags)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
# future.
|
# future.
|
||||||
#
|
#
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from anki.decks import DeckTreeNode
|
from anki.decks import DeckTreeNode
|
||||||
from anki.errors import InvalidInput, NotFoundError
|
from anki.errors import InvalidInput, NotFoundError
|
||||||
|
|
|
@ -25,7 +25,7 @@ from anki.cards import CardId
|
||||||
from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
|
from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
|
||||||
from anki.decks import DeckConfigDict, DeckId, DeckTreeNode
|
from anki.decks import DeckConfigDict, DeckId, DeckTreeNode
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from anki.utils import ids2str, intTime
|
from anki.utils import ids2str, int_time
|
||||||
|
|
||||||
|
|
||||||
class SchedulerBase(DeprecatedNamesMixin):
|
class SchedulerBase(DeprecatedNamesMixin):
|
||||||
|
@ -52,7 +52,7 @@ class SchedulerBase(DeprecatedNamesMixin):
|
||||||
def deck_due_tree(self, top_deck_id: int = 0) -> DeckTreeNode:
|
def deck_due_tree(self, top_deck_id: int = 0) -> DeckTreeNode:
|
||||||
"""Returns a tree of decks with counts.
|
"""Returns a tree of decks with counts.
|
||||||
If top_deck_id provided, counts are limited to that node."""
|
If top_deck_id provided, counts are limited to that node."""
|
||||||
return self.col._backend.deck_tree(top_deck_id=top_deck_id, now=intTime())
|
return self.col._backend.deck_tree(top_deck_id=top_deck_id, now=int_time())
|
||||||
|
|
||||||
# Deck finished state & custom study
|
# Deck finished state & custom study
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -14,7 +14,7 @@ from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
from anki.utils import ids2str, intTime
|
from anki.utils import ids2str, int_time
|
||||||
|
|
||||||
from .v2 import QueueConfig
|
from .v2 import QueueConfig
|
||||||
from .v2 import Scheduler as V2
|
from .v2 import Scheduler as V2
|
||||||
|
@ -88,7 +88,7 @@ class Scheduler(V2):
|
||||||
milliseconds_delta=+card.time_taken(),
|
milliseconds_delta=+card.time_taken(),
|
||||||
)
|
)
|
||||||
|
|
||||||
card.mod = intTime()
|
card.mod = int_time()
|
||||||
card.usn = self.col.usn()
|
card.usn = self.col.usn()
|
||||||
card.flush()
|
card.flush()
|
||||||
|
|
||||||
|
@ -389,7 +389,7 @@ due = odue, queue = {QUEUE_TYPE_REV}, mod = %d, usn = %d, odue = 0
|
||||||
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CARD_TYPE_REV}
|
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CARD_TYPE_REV}
|
||||||
%s
|
%s
|
||||||
"""
|
"""
|
||||||
% (intTime(), self.col.usn(), extra)
|
% (int_time(), self.col.usn(), extra)
|
||||||
)
|
)
|
||||||
# new cards in learning
|
# new cards in learning
|
||||||
self.forgetCards(
|
self.forgetCards(
|
||||||
|
@ -406,7 +406,7 @@ where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CAR
|
||||||
select sum(left/1000) from
|
select sum(left/1000) from
|
||||||
(select left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
|
(select left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
|
||||||
did,
|
did,
|
||||||
intTime() + self.col.conf["collapseTime"],
|
int_time() + self.col.conf["collapseTime"],
|
||||||
self.reportLimit,
|
self.reportLimit,
|
||||||
)
|
)
|
||||||
or 0
|
or 0
|
||||||
|
|
|
@ -17,7 +17,7 @@ from anki.consts import *
|
||||||
from anki.decks import DeckConfigDict, DeckDict, DeckId
|
from anki.decks import DeckConfigDict, DeckDict, DeckId
|
||||||
from anki.lang import FormatTimeSpan
|
from anki.lang import FormatTimeSpan
|
||||||
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||||
from anki.utils import ids2str, intTime
|
from anki.utils import ids2str, int_time
|
||||||
|
|
||||||
CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
|
CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
|
||||||
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
|
SchedTimingToday = scheduler_pb2.SchedTimingTodayResponse
|
||||||
|
@ -269,7 +269,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)"""
|
||||||
|
|
||||||
# scan for any newly due learning cards every minute
|
# scan for any newly due learning cards every minute
|
||||||
def _updateLrnCutoff(self, force: bool) -> bool:
|
def _updateLrnCutoff(self, force: bool) -> bool:
|
||||||
nextCutoff = intTime() + self.col.conf["collapseTime"]
|
nextCutoff = int_time() + self.col.conf["collapseTime"]
|
||||||
if nextCutoff - self._lrnCutoff > 60 or force:
|
if nextCutoff - self._lrnCutoff > 60 or force:
|
||||||
self._lrnCutoff = nextCutoff
|
self._lrnCutoff = nextCutoff
|
||||||
return True
|
return True
|
||||||
|
@ -320,7 +320,7 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
|
||||||
return False
|
return False
|
||||||
if self._lrnQueue:
|
if self._lrnQueue:
|
||||||
return True
|
return True
|
||||||
cutoff = intTime() + self.col.conf["collapseTime"]
|
cutoff = int_time() + self.col.conf["collapseTime"]
|
||||||
self._lrnQueue = self.col.db.all( # type: ignore
|
self._lrnQueue = self.col.db.all( # type: ignore
|
||||||
f"""
|
f"""
|
||||||
select due, id from cards where
|
select due, id from cards where
|
||||||
|
@ -457,7 +457,7 @@ limit ?"""
|
||||||
|
|
||||||
self._answerCard(card, ease)
|
self._answerCard(card, ease)
|
||||||
|
|
||||||
card.mod = intTime()
|
card.mod = int_time()
|
||||||
card.usn = self.col.usn()
|
card.usn = self.col.usn()
|
||||||
card.flush()
|
card.flush()
|
||||||
|
|
||||||
|
@ -615,7 +615,7 @@ limit ?"""
|
||||||
fuzz = random.randrange(0, max(1, maxExtra))
|
fuzz = random.randrange(0, max(1, maxExtra))
|
||||||
card.due = min(self.day_cutoff - 1, card.due + fuzz)
|
card.due = min(self.day_cutoff - 1, card.due + fuzz)
|
||||||
card.queue = QUEUE_TYPE_LRN
|
card.queue = QUEUE_TYPE_LRN
|
||||||
if card.due < (intTime() + self.col.conf["collapseTime"]):
|
if card.due < (int_time() + self.col.conf["collapseTime"]):
|
||||||
self.lrnCount += 1
|
self.lrnCount += 1
|
||||||
# if the queue is not empty and there's nothing else to do, make
|
# if the queue is not empty and there's nothing else to do, make
|
||||||
# sure we don't put it at the head of the queue and end up showing
|
# sure we don't put it at the head of the queue and end up showing
|
||||||
|
@ -696,7 +696,7 @@ limit ?"""
|
||||||
) -> 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:
|
||||||
now = intTime()
|
now = int_time()
|
||||||
delays = delays[-left:]
|
delays = delays[-left:]
|
||||||
ok = 0
|
ok = 0
|
||||||
for idx, delay in enumerate(delays):
|
for idx, delay in enumerate(delays):
|
||||||
|
@ -777,7 +777,7 @@ limit ?"""
|
||||||
if ease == BUTTON_ONE:
|
if ease == BUTTON_ONE:
|
||||||
# repeat after delay
|
# repeat after delay
|
||||||
card.queue = QUEUE_TYPE_PREVIEW
|
card.queue = QUEUE_TYPE_PREVIEW
|
||||||
card.due = intTime() + self._previewDelay(card)
|
card.due = int_time() + self._previewDelay(card)
|
||||||
self.lrnCount += 1
|
self.lrnCount += 1
|
||||||
else:
|
else:
|
||||||
# BUTTON_TWO
|
# BUTTON_TWO
|
||||||
|
|
|
@ -22,7 +22,7 @@ from anki.decks import DeckId
|
||||||
from anki.errors import DBError
|
from anki.errors import DBError
|
||||||
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||||
from anki.types import assert_exhaustive
|
from anki.types import assert_exhaustive
|
||||||
from anki.utils import intTime
|
from anki.utils import int_time
|
||||||
|
|
||||||
QueuedCards = scheduler_pb2.QueuedCards
|
QueuedCards = scheduler_pb2.QueuedCards
|
||||||
SchedulingState = scheduler_pb2.SchedulingState
|
SchedulingState = scheduler_pb2.SchedulingState
|
||||||
|
@ -77,7 +77,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
current_state=states.current,
|
current_state=states.current,
|
||||||
new_state=new_state,
|
new_state=new_state,
|
||||||
rating=rating,
|
rating=rating,
|
||||||
answered_at_millis=intTime(1000),
|
answered_at_millis=int_time(1000),
|
||||||
milliseconds_taken=card.time_taken(),
|
milliseconds_taken=card.time_taken(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Sound/TTS references extracted from card text.
|
Sound/TTS references extracted from card text.
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
# 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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, no_type_check
|
||||||
|
|
||||||
import anki.collection
|
import anki.collection
|
||||||
import anki.models
|
import anki.models
|
||||||
from anki import notetypes_pb2
|
from anki import notetypes_pb2
|
||||||
|
from anki._legacy import DeprecatedNamesMixinForModule
|
||||||
from anki.utils import from_json_bytes
|
from anki.utils import from_json_bytes
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
@ -38,14 +41,14 @@ def get_stock_notetypes(
|
||||||
StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
|
StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
|
||||||
StockNotetypeKind.CLOZE,
|
StockNotetypeKind.CLOZE,
|
||||||
]:
|
]:
|
||||||
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
|
note_type = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
|
||||||
|
|
||||||
def instance_getter(
|
def instance_getter(
|
||||||
model: Any,
|
model: Any,
|
||||||
) -> Callable[[anki.collection.Collection], anki.models.NotetypeDict]:
|
) -> Callable[[anki.collection.Collection], anki.models.NotetypeDict]:
|
||||||
return lambda col: model
|
return lambda col: model
|
||||||
|
|
||||||
out.append((m["name"], instance_getter(m)))
|
out.append((note_type["name"], instance_getter(note_type)))
|
||||||
# add extras from add-ons
|
# add extras from add-ons
|
||||||
for (name_or_func, func) in models:
|
for (name_or_func, func) in models:
|
||||||
if not isinstance(name_or_func, str):
|
if not isinstance(name_or_func, str):
|
||||||
|
@ -61,33 +64,59 @@ def get_stock_notetypes(
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
def _legacy_add_basic_model(
|
||||||
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC)
|
|
||||||
col.models.add(nt)
|
|
||||||
return nt
|
|
||||||
|
|
||||||
|
|
||||||
def addBasicTypingModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
|
||||||
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING)
|
|
||||||
col.models.add(nt)
|
|
||||||
return nt
|
|
||||||
|
|
||||||
|
|
||||||
def addForwardReverse(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
|
||||||
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED)
|
|
||||||
col.models.add(nt)
|
|
||||||
return nt
|
|
||||||
|
|
||||||
|
|
||||||
def addForwardOptionalReverse(
|
|
||||||
col: anki.collection.Collection,
|
col: anki.collection.Collection,
|
||||||
) -> anki.models.NotetypeDict:
|
) -> anki.models.NotetypeDict:
|
||||||
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED)
|
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC)
|
||||||
col.models.add(nt)
|
col.models.add(note_type)
|
||||||
return nt
|
return note_type
|
||||||
|
|
||||||
|
|
||||||
def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
def _legacy_add_basic_typing_model(
|
||||||
nt = _get_stock_notetype(col, StockNotetypeKind.CLOZE)
|
col: anki.collection.Collection,
|
||||||
col.models.add(nt)
|
) -> anki.models.NotetypeDict:
|
||||||
return nt
|
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING)
|
||||||
|
col.models.add(note_type)
|
||||||
|
return note_type
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_add_forward_reverse(
|
||||||
|
col: anki.collection.Collection,
|
||||||
|
) -> anki.models.NotetypeDict:
|
||||||
|
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED)
|
||||||
|
col.models.add(note_type)
|
||||||
|
return note_type
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_add_forward_optional_reverse(
|
||||||
|
col: anki.collection.Collection,
|
||||||
|
) -> anki.models.NotetypeDict:
|
||||||
|
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED)
|
||||||
|
col.models.add(note_type)
|
||||||
|
return note_type
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_add_cloze_model(
|
||||||
|
col: anki.collection.Collection,
|
||||||
|
) -> anki.models.NotetypeDict:
|
||||||
|
note_type = _get_stock_notetype(col, StockNotetypeKind.CLOZE)
|
||||||
|
col.models.add(note_type)
|
||||||
|
return note_type
|
||||||
|
|
||||||
|
|
||||||
|
_deprecated_names = DeprecatedNamesMixinForModule(globals())
|
||||||
|
_deprecated_names.register_deprecated_attributes(
|
||||||
|
addBasicModel=(_legacy_add_basic_model, get_stock_notetypes),
|
||||||
|
addBasicTypingModel=(_legacy_add_basic_typing_model, get_stock_notetypes),
|
||||||
|
addForwardReverse=(_legacy_add_forward_reverse, get_stock_notetypes),
|
||||||
|
addForwardOptionalReverse=(
|
||||||
|
_legacy_add_forward_optional_reverse,
|
||||||
|
get_stock_notetypes,
|
||||||
|
),
|
||||||
|
addClozeModel=(_legacy_add_cloze_model, get_stock_notetypes),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@no_type_check
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
return _deprecated_names.__getattr__(name)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
# Legacy code expects to find Collection in this module.
|
# Legacy code expects to find Collection in this module.
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from anki import sync_pb2
|
from anki import sync_pb2
|
||||||
|
|
||||||
# public exports
|
# public exports
|
||||||
|
|
|
@ -171,7 +171,7 @@ def col_path() -> str:
|
||||||
|
|
||||||
|
|
||||||
def serve() -> None:
|
def serve() -> None:
|
||||||
global col # pylint: disable=C0103
|
global col # pylint: disable=invalid-name
|
||||||
|
|
||||||
col = Collection(col_path(), server=True)
|
col = Collection(col_path(), server=True)
|
||||||
# don't hold an outer transaction open
|
# don't hold an outer transaction open
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Anki maintains a cache of used tags so it can quickly present a list of tags
|
Anki maintains a cache of used tags so it can quickly present a list of tags
|
||||||
for autocomplete and in the browser. For efficiency, deletions are not
|
for autocomplete and in the browser. For efficiency, deletions are not
|
||||||
|
@ -18,6 +20,7 @@ from typing import Collection, Match, Sequence
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.collection
|
import anki.collection
|
||||||
from anki import tags_pb2
|
from anki import tags_pb2
|
||||||
|
from anki._legacy import DeprecatedNamesMixin, deprecated
|
||||||
from anki.collection import OpChanges, OpChangesWithCount
|
from anki.collection import OpChanges, OpChangesWithCount
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
|
@ -28,7 +31,7 @@ TagTreeNode = tags_pb2.TagTreeNode
|
||||||
MARKED_TAG = "marked"
|
MARKED_TAG = "marked"
|
||||||
|
|
||||||
|
|
||||||
class TagManager:
|
class TagManager(DeprecatedNamesMixin):
|
||||||
def __init__(self, col: anki.collection.Collection) -> None:
|
def __init__(self, col: anki.collection.Collection) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
|
|
||||||
|
@ -37,9 +40,9 @@ class TagManager:
|
||||||
return list(self.col._backend.all_tags())
|
return list(self.col._backend.all_tags())
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
d = dict(self.__dict__)
|
dict_ = dict(self.__dict__)
|
||||||
del d["col"]
|
del dict_["col"]
|
||||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
return f"{super().__repr__()} {pprint.pformat(dict_, width=300)}"
|
||||||
|
|
||||||
def tree(self) -> TagTreeNode:
|
def tree(self) -> TagTreeNode:
|
||||||
return self.col._backend.tag_tree()
|
return self.col._backend.tag_tree()
|
||||||
|
@ -50,7 +53,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 by_deck(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=?"
|
||||||
|
@ -133,48 +136,40 @@ class TagManager:
|
||||||
return ""
|
return ""
|
||||||
return f" {' '.join(tags)} "
|
return f" {' '.join(tags)} "
|
||||||
|
|
||||||
def addToStr(self, addtags: str, tags: str) -> str:
|
def rem_from_str(self, deltags: str, tags: str) -> str:
|
||||||
"Add tags if they don't exist, and canonify."
|
|
||||||
currentTags = self.split(tags)
|
|
||||||
for tag in self.split(addtags):
|
|
||||||
if not self.inList(tag, currentTags):
|
|
||||||
currentTags.append(tag)
|
|
||||||
return self.join(self.canonify(currentTags))
|
|
||||||
|
|
||||||
def remFromStr(self, deltags: str, tags: str) -> str:
|
|
||||||
"Delete tags if they exist."
|
"Delete tags if they exist."
|
||||||
|
|
||||||
def wildcard(pat: str, repl: str) -> Match:
|
def wildcard(pat: str, repl: str) -> Match:
|
||||||
pat = re.escape(pat).replace("\\*", ".*")
|
pat = re.escape(pat).replace("\\*", ".*")
|
||||||
return re.match(f"^{pat}$", repl, re.IGNORECASE)
|
return re.match(f"^{pat}$", repl, re.IGNORECASE)
|
||||||
|
|
||||||
currentTags = self.split(tags)
|
current_tags = self.split(tags)
|
||||||
for tag in self.split(deltags):
|
for del_tag in self.split(deltags):
|
||||||
# find tags, ignoring case
|
# find tags, ignoring case
|
||||||
remove = []
|
remove = []
|
||||||
for tx in currentTags:
|
for cur_tag in current_tags:
|
||||||
if (tag.lower() == tx.lower()) or wildcard(tag, tx):
|
if (del_tag.lower() == cur_tag.lower()) or wildcard(del_tag, cur_tag):
|
||||||
remove.append(tx)
|
remove.append(cur_tag)
|
||||||
# remove them
|
# remove them
|
||||||
for r in remove:
|
for rem in remove:
|
||||||
currentTags.remove(r)
|
current_tags.remove(rem)
|
||||||
return self.join(currentTags)
|
return self.join(current_tags)
|
||||||
|
|
||||||
# List-based utilities
|
# List-based utilities
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# this is now a no-op - the tags are canonified when the note is saved
|
@deprecated(info="no-op - tags are now canonified when note is saved")
|
||||||
def canonify(self, tagList: list[str]) -> list[str]:
|
def canonify(self, tag_list: list[str]) -> list[str]:
|
||||||
return tagList
|
return tag_list
|
||||||
|
|
||||||
def inList(self, tag: str, tags: list[str]) -> bool:
|
def in_list(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: list[int] | None = None) -> None:
|
def _legacy_register_notes(self, nids: list[int] | None = None) -> None:
|
||||||
self.clear_unused_tags()
|
self.clear_unused_tags()
|
||||||
|
|
||||||
def register(
|
def register(
|
||||||
|
@ -182,12 +177,19 @@ class TagManager:
|
||||||
) -> 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 _legacy_bulk_add(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 _legacy_bulk_rem(self, ids: list[NoteId], tags: str) -> None:
|
||||||
self.bulkAdd(ids, tags, False)
|
self._legacy_bulk_add(ids, tags, False)
|
||||||
|
|
||||||
|
|
||||||
|
TagManager.register_deprecated_attributes(
|
||||||
|
registerNotes=(TagManager._legacy_register_notes, TagManager.clear_unused_tags),
|
||||||
|
bulkAdd=(TagManager._legacy_bulk_add, TagManager.bulk_add),
|
||||||
|
bulkRem=(TagManager._legacy_bulk_rem, TagManager.bulk_remove),
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This file contains the Python portion of the template rendering code.
|
This file contains the Python portion of the template rendering code.
|
||||||
|
|
||||||
|
@ -215,10 +217,10 @@ class TemplateRenderContext:
|
||||||
def render(self) -> TemplateRenderOutput:
|
def render(self) -> TemplateRenderOutput:
|
||||||
try:
|
try:
|
||||||
partial = self._partially_render()
|
partial = self._partially_render()
|
||||||
except TemplateError as e:
|
except TemplateError as error:
|
||||||
return TemplateRenderOutput(
|
return TemplateRenderOutput(
|
||||||
question_text=str(e),
|
question_text=str(error),
|
||||||
answer_text=str(e),
|
answer_text=str(error),
|
||||||
question_av_tags=[],
|
question_av_tags=[],
|
||||||
answer_av_tags=[],
|
answer_av_tags=[],
|
||||||
)
|
)
|
||||||
|
@ -284,12 +286,12 @@ class TemplateRenderOutput:
|
||||||
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")
|
question, answer = template.get("bqfmt"), template.get("bafmt")
|
||||||
else:
|
else:
|
||||||
q, a = None, None
|
question, answer = None, None
|
||||||
q = q or template.get("qfmt")
|
question = question or template.get("qfmt")
|
||||||
a = a or template.get("afmt")
|
answer = answer or template.get("afmt")
|
||||||
return q, a # type: ignore
|
return question, answer # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def apply_custom_filters(
|
def apply_custom_filters(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
# pylint: enable=invalid-name
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json as _json
|
import json as _json
|
||||||
|
@ -14,11 +16,11 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from typing import Any, Iterable, Iterator
|
from typing import Any, Iterable, Iterator, no_type_check
|
||||||
|
|
||||||
|
from anki._legacy import DeprecatedNamesMixinForModule
|
||||||
from anki.dbproxy import DBProxy
|
from anki.dbproxy import DBProxy
|
||||||
|
|
||||||
_tmpdir: str | None
|
_tmpdir: str | None
|
||||||
|
@ -35,19 +37,11 @@ except:
|
||||||
from_json_bytes = _json.loads
|
from_json_bytes = _json.loads
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
|
||||||
if name == "json":
|
|
||||||
traceback.print_stack(file=sys.stdout)
|
|
||||||
print("add-on should import json directly, not from anki.utils")
|
|
||||||
return _json
|
|
||||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
|
||||||
|
|
||||||
|
|
||||||
# Time handling
|
# Time handling
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def intTime(scale: int = 1) -> int:
|
def int_time(scale: int = 1) -> int:
|
||||||
"The time in integer seconds. Pass scale=1000 to get milliseconds."
|
"The time in integer seconds. Pass scale=1000 to get milliseconds."
|
||||||
return int(time.time() * scale)
|
return int(time.time() * scale)
|
||||||
|
|
||||||
|
@ -56,33 +50,33 @@ def intTime(scale: int = 1) -> int:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def stripHTML(s: str) -> str:
|
def strip_html(txt: str) -> str:
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki.collection import StripHtmlMode
|
from anki.collection import StripHtmlMode
|
||||||
|
|
||||||
return anki.lang.current_i18n.strip_html(text=s, mode=StripHtmlMode.NORMAL)
|
return anki.lang.current_i18n.strip_html(text=txt, mode=StripHtmlMode.NORMAL)
|
||||||
|
|
||||||
|
|
||||||
def stripHTMLMedia(s: str) -> str:
|
def strip_html_media(txt: str) -> str:
|
||||||
"Strip HTML but keep media filenames"
|
"Strip HTML but keep media filenames"
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki.collection import StripHtmlMode
|
from anki.collection import StripHtmlMode
|
||||||
|
|
||||||
return anki.lang.current_i18n.strip_html(
|
return anki.lang.current_i18n.strip_html(
|
||||||
text=s, mode=StripHtmlMode.PRESERVE_MEDIA_FILENAMES
|
text=txt, mode=StripHtmlMode.PRESERVE_MEDIA_FILENAMES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def htmlToTextLine(s: str) -> str:
|
def html_to_text_line(txt: str) -> str:
|
||||||
s = s.replace("<br>", " ")
|
txt = txt.replace("<br>", " ")
|
||||||
s = s.replace("<br />", " ")
|
txt = txt.replace("<br />", " ")
|
||||||
s = s.replace("<div>", " ")
|
txt = txt.replace("<div>", " ")
|
||||||
s = s.replace("\n", " ")
|
txt = txt.replace("\n", " ")
|
||||||
s = re.sub(r"\[sound:[^]]+\]", "", s)
|
txt = re.sub(r"\[sound:[^]]+\]", "", txt)
|
||||||
s = re.sub(r"\[\[type:[^]]+\]\]", "", s)
|
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
|
||||||
s = stripHTMLMedia(s)
|
txt = strip_html_media(txt)
|
||||||
s = s.strip()
|
txt = txt.strip()
|
||||||
return s
|
return txt
|
||||||
|
|
||||||
|
|
||||||
# IDs
|
# IDs
|
||||||
|
@ -94,19 +88,19 @@ def ids2str(ids: Iterable[int | str]) -> str:
|
||||||
return f"({','.join(str(i) for i in ids)})"
|
return f"({','.join(str(i) for i in ids)})"
|
||||||
|
|
||||||
|
|
||||||
def timestampID(db: DBProxy, table: str) -> int:
|
def timestamp_id(db: DBProxy, table: str) -> int:
|
||||||
"Return a non-conflicting timestamp for table."
|
"Return a non-conflicting timestamp for table."
|
||||||
# be careful not to create multiple objects without flushing them, or they
|
# be careful not to create multiple objects without flushing them, or they
|
||||||
# may share an ID.
|
# may share an ID.
|
||||||
t = intTime(1000)
|
timestamp = int_time(1000)
|
||||||
while db.scalar(f"select id from {table} where id = ?", t):
|
while db.scalar(f"select id from {table} where id = ?", timestamp):
|
||||||
t += 1
|
timestamp += 1
|
||||||
return t
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
def maxID(db: DBProxy) -> int:
|
def max_id(db: DBProxy) -> int:
|
||||||
"Return the first safe ID to use."
|
"Return the first safe ID to use."
|
||||||
now = intTime(1000)
|
now = int_time(1000)
|
||||||
for tbl in "cards", "notes":
|
for tbl in "cards", "notes":
|
||||||
now = max(now, db.scalar(f"select max(id) from {tbl}") or 0)
|
now = max(now, db.scalar(f"select max(id) from {tbl}") or 0)
|
||||||
return now + 1
|
return now + 1
|
||||||
|
@ -114,21 +108,20 @@ def maxID(db: DBProxy) -> int:
|
||||||
|
|
||||||
# used in ankiweb
|
# used in ankiweb
|
||||||
def base62(num: int, extra: str = "") -> str:
|
def base62(num: int, extra: str = "") -> str:
|
||||||
s = string
|
table = string.ascii_letters + string.digits + extra
|
||||||
table = s.ascii_letters + s.digits + extra
|
|
||||||
buf = ""
|
buf = ""
|
||||||
while num:
|
while num:
|
||||||
num, i = divmod(num, len(table))
|
num, mod = divmod(num, len(table))
|
||||||
buf = table[i] + buf
|
buf = table[mod] + buf
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|
||||||
_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
_BASE91_EXTRA_CHARS = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
||||||
|
|
||||||
|
|
||||||
def base91(num: int) -> str:
|
def base91(num: int) -> str:
|
||||||
# all printable characters minus quotes, backslash and separators
|
# all printable characters minus quotes, backslash and separators
|
||||||
return base62(num, _base91_extra_chars)
|
return base62(num, _BASE91_EXTRA_CHARS)
|
||||||
|
|
||||||
|
|
||||||
def guid64() -> str:
|
def guid64() -> str:
|
||||||
|
@ -140,11 +133,11 @@ def guid64() -> str:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def joinFields(list: list[str]) -> str:
|
def join_fields(list: list[str]) -> str:
|
||||||
return "\x1f".join(list)
|
return "\x1f".join(list)
|
||||||
|
|
||||||
|
|
||||||
def splitFields(string: str) -> list[str]:
|
def split_fields(string: str) -> list[str]:
|
||||||
return string.split("\x1f")
|
return string.split("\x1f")
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,20 +151,20 @@ def checksum(data: bytes | str) -> str:
|
||||||
return sha1(data).hexdigest()
|
return sha1(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def fieldChecksum(data: str) -> int:
|
def field_checksum(data: str) -> int:
|
||||||
# 32 bit unsigned number from first 8 digits of sha1 hash
|
# 32 bit unsigned number from first 8 digits of sha1 hash
|
||||||
return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16)
|
return int(checksum(strip_html_media(data).encode("utf-8"))[:8], 16)
|
||||||
|
|
||||||
|
|
||||||
# Temp files
|
# Temp files
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
_tmpdir = None
|
_tmpdir = None # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
def tmpdir() -> str:
|
def tmpdir() -> str:
|
||||||
"A reusable temp folder which we clean out on each program invocation."
|
"A reusable temp folder which we clean out on each program invocation."
|
||||||
global _tmpdir
|
global _tmpdir # pylint: disable=invalid-name
|
||||||
if not _tmpdir:
|
if not _tmpdir:
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
|
@ -190,15 +183,15 @@ def tmpdir() -> str:
|
||||||
|
|
||||||
|
|
||||||
def tmpfile(prefix: str = "", suffix: str = "") -> str:
|
def tmpfile(prefix: str = "", suffix: str = "") -> str:
|
||||||
(fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
|
(descriptor, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
|
||||||
os.close(fd)
|
os.close(descriptor)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def namedtmp(name: str, rm: bool = True) -> str:
|
def namedtmp(name: str, remove: bool = True) -> str:
|
||||||
"Return tmpdir+name. Deletes any existing file."
|
"Return tmpdir+name. Deletes any existing file."
|
||||||
path = os.path.join(tmpdir(), name)
|
path = os.path.join(tmpdir(), name)
|
||||||
if rm:
|
if remove:
|
||||||
try:
|
try:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -211,7 +204,7 @@ def namedtmp(name: str, rm: bool = True) -> str:
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def noBundledLibs() -> Iterator[None]:
|
def no_bundled_libs() -> Iterator[None]:
|
||||||
oldlpath = os.environ.pop("LD_LIBRARY_PATH", None)
|
oldlpath = os.environ.pop("LD_LIBRARY_PATH", None)
|
||||||
yield
|
yield
|
||||||
if oldlpath is not None:
|
if oldlpath is not None:
|
||||||
|
@ -222,18 +215,18 @@ 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:
|
||||||
si = subprocess.STARTUPINFO() # type: ignore
|
info = subprocess.STARTUPINFO() # type: ignore
|
||||||
try:
|
try:
|
||||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||||
except:
|
except:
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||||
else:
|
else:
|
||||||
si = None
|
info = None
|
||||||
# run
|
# run
|
||||||
try:
|
try:
|
||||||
with noBundledLibs():
|
with no_bundled_libs():
|
||||||
o = subprocess.Popen(argv, startupinfo=si, **kwargs)
|
process = subprocess.Popen(argv, startupinfo=info, **kwargs)
|
||||||
except OSError:
|
except OSError:
|
||||||
# command not found
|
# command not found
|
||||||
return -1
|
return -1
|
||||||
|
@ -241,7 +234,7 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
|
||||||
if wait:
|
if wait:
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
ret = o.wait()
|
ret = process.wait()
|
||||||
except OSError:
|
except OSError:
|
||||||
# interrupted system call
|
# interrupted system call
|
||||||
continue
|
continue
|
||||||
|
@ -259,13 +252,13 @@ isWin = sys.platform.startswith("win32")
|
||||||
isLin = not isMac and not isWin
|
isLin = not isMac and not isWin
|
||||||
devMode = os.getenv("ANKIDEV", "")
|
devMode = os.getenv("ANKIDEV", "")
|
||||||
|
|
||||||
invalidFilenameChars = ':*?"<>|'
|
INVALID_FILENAME_CHARS = ':*?"<>|'
|
||||||
|
|
||||||
|
|
||||||
def invalidFilename(str: str, dirsep: bool = True) -> str | None:
|
def invalid_filename(str: str, dirsep: bool = True) -> str | None:
|
||||||
for c in invalidFilenameChars:
|
for char in INVALID_FILENAME_CHARS:
|
||||||
if c in str:
|
if char in str:
|
||||||
return c
|
return char
|
||||||
if (dirsep or isWin) and "/" in str:
|
if (dirsep or isWin) and "/" in str:
|
||||||
return "/"
|
return "/"
|
||||||
elif (dirsep or not isWin) and "\\" in str:
|
elif (dirsep or not isWin) and "\\" in str:
|
||||||
|
@ -275,12 +268,10 @@ def invalidFilename(str: str, dirsep: bool = True) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def platDesc() -> str:
|
def plat_desc() -> str:
|
||||||
# we may get an interrupted system call, so try this in a loop
|
# we may get an interrupted system call, so try this in a loop
|
||||||
n = 0
|
|
||||||
theos = "unknown"
|
theos = "unknown"
|
||||||
while n < 100:
|
for _ in range(100):
|
||||||
n += 1
|
|
||||||
try:
|
try:
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if isMac:
|
if isMac:
|
||||||
|
@ -301,33 +292,33 @@ def platDesc() -> str:
|
||||||
return theos
|
return theos
|
||||||
|
|
||||||
|
|
||||||
# Debugging
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class TimedLog:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._last = time.time()
|
|
||||||
|
|
||||||
def log(self, s: str) -> None:
|
|
||||||
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
|
||||||
sys.stderr.write(
|
|
||||||
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)
|
|
||||||
)
|
|
||||||
self._last = time.time()
|
|
||||||
|
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def versionWithBuild() -> str:
|
def version_with_build() -> str:
|
||||||
from anki.buildinfo import buildhash, version
|
from anki.buildinfo import buildhash, version
|
||||||
|
|
||||||
return f"{version} ({buildhash})"
|
return f"{version} ({buildhash})"
|
||||||
|
|
||||||
|
|
||||||
def pointVersion() -> int:
|
def point_version() -> int:
|
||||||
from anki.buildinfo import version
|
from anki.buildinfo import version
|
||||||
|
|
||||||
return int(version.split(".")[-1])
|
return int(version.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
_deprecated_names = DeprecatedNamesMixinForModule(globals())
|
||||||
|
_deprecated_names.register_deprecated_aliases(
|
||||||
|
stripHTML=strip_html,
|
||||||
|
stripHTMLMedia=strip_html_media,
|
||||||
|
timestampID=timestamp_id,
|
||||||
|
maxID=max_id,
|
||||||
|
invalidFilenameChars=(INVALID_FILENAME_CHARS, "INVALID_FILENAME_CHARS"),
|
||||||
|
)
|
||||||
|
_deprecated_names.register_deprecated_attributes(json=((_json, "_json"), None))
|
||||||
|
|
||||||
|
|
||||||
|
@no_type_check
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
return _deprecated_names.__getattr__(name)
|
||||||
|
|
|
@ -39,5 +39,5 @@ ignore_missing_imports = True
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
[mypy-anki._backend.rsbridge]
|
[mypy-anki._backend.rsbridge]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
[mypy-stringcase]
|
[mypy-anki._vendor.stringcase]
|
||||||
ignore_missing_imports = True
|
disallow_untyped_defs = False
|
||||||
|
|
|
@ -9,7 +9,7 @@ import tempfile
|
||||||
from anki.collection import Collection as aopen
|
from anki.collection import Collection as aopen
|
||||||
from anki.dbproxy import emulate_named_args
|
from anki.dbproxy import emulate_named_args
|
||||||
from anki.lang import TR, without_unicode_isolation
|
from anki.lang import TR, without_unicode_isolation
|
||||||
from anki.stdmodels import addBasicModel, get_stock_notetypes
|
from anki.stdmodels import _legacy_add_basic_model, get_stock_notetypes
|
||||||
from anki.utils import isWin
|
from anki.utils import isWin
|
||||||
from tests.shared import assertException, getEmptyCol
|
from tests.shared import assertException, getEmptyCol
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ def test_timestamps():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
assert len(col.models.all_names_and_ids()) == len(get_stock_notetypes(col))
|
assert len(col.models.all_names_and_ids()) == len(get_stock_notetypes(col))
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
addBasicModel(col)
|
_legacy_add_basic_model(col)
|
||||||
assert len(col.models.all_names_and_ids()) == 100 + len(get_stock_notetypes(col))
|
assert len(col.models.all_names_and_ids()) == 100 + len(get_stock_notetypes(col))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ def test_find_cards():
|
||||||
assert len(col.find_cards("-tag:sheep")) == 4
|
assert len(col.find_cards("-tag:sheep")) == 4
|
||||||
col.tags.bulk_add(col.db.list("select id from notes"), "foo bar")
|
col.tags.bulk_add(col.db.list("select id from notes"), "foo bar")
|
||||||
assert len(col.find_cards("tag:foo")) == len(col.find_cards("tag:bar")) == 5
|
assert len(col.find_cards("tag:foo")) == len(col.find_cards("tag:bar")) == 5
|
||||||
col.tags.bulkRem(col.db.list("select id from notes"), "foo")
|
col.tags.bulk_remove(col.db.list("select id from notes"), "foo")
|
||||||
assert len(col.find_cards("tag:foo")) == 0
|
assert len(col.find_cards("tag:foo")) == 0
|
||||||
assert len(col.find_cards("tag:bar")) == 5
|
assert len(col.find_cards("tag:bar")) == 5
|
||||||
# text searches
|
# text searches
|
||||||
|
|
|
@ -17,18 +17,20 @@ def test_add():
|
||||||
with open(path, "w") as note:
|
with open(path, "w") as note:
|
||||||
note.write("hello")
|
note.write("hello")
|
||||||
# new file, should preserve name
|
# new file, should preserve name
|
||||||
assert col.media.addFile(path) == "foo.jpg"
|
assert col.media.add_file(path) == "foo.jpg"
|
||||||
# adding the same file again should not create a duplicate
|
# adding the same file again should not create a duplicate
|
||||||
assert col.media.addFile(path) == "foo.jpg"
|
assert col.media.add_file(path) == "foo.jpg"
|
||||||
# but if it has a different sha1, it should
|
# but if it has a different sha1, it should
|
||||||
with open(path, "w") as note:
|
with open(path, "w") as note:
|
||||||
note.write("world")
|
note.write("world")
|
||||||
assert col.media.addFile(path) == "foo-7c211433f02071597741e6ff5a8ea34789abbf43.jpg"
|
assert (
|
||||||
|
col.media.add_file(path) == "foo-7c211433f02071597741e6ff5a8ea34789abbf43.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_strings():
|
def test_strings():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
mf = col.media.filesInStr
|
mf = col.media.files_in_str
|
||||||
mid = col.models.current()["id"]
|
mid = col.models.current()["id"]
|
||||||
assert mf(mid, "aoeu") == []
|
assert mf(mid, "aoeu") == []
|
||||||
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
||||||
|
@ -61,7 +63,7 @@ def test_deckIntegration():
|
||||||
col.media.dir()
|
col.media.dir()
|
||||||
# put a file into it
|
# put a file into it
|
||||||
file = str(os.path.join(testDir, "support", "fake.png"))
|
file = str(os.path.join(testDir, "support", "fake.png"))
|
||||||
col.media.addFile(file)
|
col.media.add_file(file)
|
||||||
# add a note which references it
|
# add a note which references it
|
||||||
note = col.newNote()
|
note = col.newNote()
|
||||||
note["Front"] = "one"
|
note["Front"] = "one"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import time
|
||||||
|
|
||||||
from anki.consts import MODEL_CLOZE
|
from anki.consts import MODEL_CLOZE
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
from anki.utils import isWin, stripHTML
|
from anki.utils import isWin, strip_html
|
||||||
from tests.shared import getEmptyCol
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ def test_templates():
|
||||||
# and should have updated the other cards' ordinals
|
# and should have updated the other cards' ordinals
|
||||||
c = note.cards()[0]
|
c = note.cards()[0]
|
||||||
assert c.ord == 0
|
assert c.ord == 0
|
||||||
assert stripHTML(c.question()) == "1"
|
assert strip_html(c.question()) == "1"
|
||||||
# it shouldn't be possible to orphan notes by removing templates
|
# it shouldn't be possible to orphan notes by removing templates
|
||||||
t = mm.new_template("template name")
|
t = mm.new_template("template name")
|
||||||
t["qfmt"] = "{{Front}}2"
|
t["qfmt"] = "{{Front}}2"
|
||||||
|
|
|
@ -7,7 +7,7 @@ import time
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.utils import intTime
|
from anki.utils import int_time
|
||||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ def getEmptyCol() -> Collection:
|
||||||
|
|
||||||
def test_clock():
|
def test_clock():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
if (col.sched.day_cutoff - intTime()) < 10 * 60:
|
if (col.sched.day_cutoff - int_time()) < 10 * 60:
|
||||||
raise Exception("Unit tests will fail around the day rollover.")
|
raise Exception("Unit tests will fail around the day rollover.")
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ def test_new():
|
||||||
assert c.queue == QUEUE_TYPE_NEW
|
assert c.queue == QUEUE_TYPE_NEW
|
||||||
assert c.type == CARD_TYPE_NEW
|
assert c.type == CARD_TYPE_NEW
|
||||||
# if we answer it, it should become a learn card
|
# if we answer it, it should become a learn card
|
||||||
t = intTime()
|
t = int_time()
|
||||||
col.sched.answerCard(c, 1)
|
col.sched.answerCard(c, 1)
|
||||||
assert c.queue == QUEUE_TYPE_LRN
|
assert c.queue == QUEUE_TYPE_LRN
|
||||||
assert c.type == CARD_TYPE_LRN
|
assert c.type == CARD_TYPE_LRN
|
||||||
|
|
|
@ -12,7 +12,7 @@ from anki import hooks
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.scheduler import UnburyDeck
|
from anki.scheduler import UnburyDeck
|
||||||
from anki.utils import intTime
|
from anki.utils import int_time
|
||||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ def getEmptyCol():
|
||||||
|
|
||||||
def test_clock():
|
def test_clock():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
if (col.sched.day_cutoff - intTime()) < 10 * 60:
|
if (col.sched.day_cutoff - int_time()) < 10 * 60:
|
||||||
raise Exception("Unit tests will fail around the day rollover.")
|
raise Exception("Unit tests will fail around the day rollover.")
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ def test_new():
|
||||||
assert c.queue == QUEUE_TYPE_NEW
|
assert c.queue == QUEUE_TYPE_NEW
|
||||||
assert c.type == CARD_TYPE_NEW
|
assert c.type == CARD_TYPE_NEW
|
||||||
# if we answer it, it should become a learn card
|
# if we answer it, it should become a learn card
|
||||||
t = intTime()
|
t = int_time()
|
||||||
col.sched.answerCard(c, 1)
|
col.sched.answerCard(c, 1)
|
||||||
assert c.queue == QUEUE_TYPE_LRN
|
assert c.queue == QUEUE_TYPE_LRN
|
||||||
assert c.type == CARD_TYPE_LRN
|
assert c.type == CARD_TYPE_LRN
|
||||||
|
@ -804,14 +804,14 @@ def test_filt_keep_lrn_state():
|
||||||
# should be able to advance learning steps
|
# should be able to advance learning steps
|
||||||
col.sched.answerCard(c, 3)
|
col.sched.answerCard(c, 3)
|
||||||
# should be due at least an hour in the future
|
# should be due at least an hour in the future
|
||||||
assert c.due - intTime() > 60 * 60
|
assert c.due - int_time() > 60 * 60
|
||||||
|
|
||||||
# emptying the deck preserves learning state
|
# emptying the deck preserves learning state
|
||||||
col.sched.empty_filtered_deck(did)
|
col.sched.empty_filtered_deck(did)
|
||||||
c.load()
|
c.load()
|
||||||
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
|
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
|
||||||
assert c.left % 1000 == 1
|
assert c.left % 1000 == 1
|
||||||
assert c.due - intTime() > 60 * 60
|
assert c.due - int_time() > 60 * 60
|
||||||
|
|
||||||
|
|
||||||
def test_preview():
|
def test_preview():
|
||||||
|
@ -1034,7 +1034,7 @@ def test_timing():
|
||||||
c2 = col.sched.getCard()
|
c2 = col.sched.getCard()
|
||||||
assert c2.queue == QUEUE_TYPE_REV
|
assert c2.queue == QUEUE_TYPE_REV
|
||||||
# if the failed card becomes due, it should show first
|
# if the failed card becomes due, it should show first
|
||||||
c.due = intTime() - 1
|
c.due = int_time() - 1
|
||||||
c.flush()
|
c.flush()
|
||||||
col.reset()
|
col.reset()
|
||||||
c = col.sched.getCard()
|
c = col.sched.getCard()
|
||||||
|
@ -1061,7 +1061,7 @@ def test_collapse():
|
||||||
col.sched.answerCard(c2, 1)
|
col.sched.answerCard(c2, 1)
|
||||||
# first should become available again, despite it being due in the future
|
# first should become available again, despite it being due in the future
|
||||||
c3 = col.sched.getCard()
|
c3 = col.sched.getCard()
|
||||||
assert c3.due > intTime()
|
assert c3.due > int_time()
|
||||||
col.sched.answerCard(c3, 4)
|
col.sched.answerCard(c3, 4)
|
||||||
# answer other
|
# answer other
|
||||||
c4 = col.sched.getCard()
|
c4 = col.sched.getCard()
|
||||||
|
|
|
@ -6,7 +6,7 @@ import time
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.utils import versionWithBuild
|
from anki.utils import version_with_build
|
||||||
from aqt.addons import AddonManager, AddonMeta
|
from aqt.addons import AddonManager, AddonMeta
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import disable_help_button, supportText, tooltip, tr
|
from aqt.utils import disable_help_button, supportText, tooltip, tr
|
||||||
|
@ -96,7 +96,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
abouttext = "<center><img src='/_anki/imgs/anki-logo-thin.png'></center>"
|
abouttext = "<center><img src='/_anki/imgs/anki-logo-thin.png'></center>"
|
||||||
abouttext += f"<p>{tr.about_anki_is_a_friendly_intelligent_spaced()}"
|
abouttext += f"<p>{tr.about_anki_is_a_friendly_intelligent_spaced()}"
|
||||||
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
|
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
|
||||||
abouttext += f"<p>{tr.about_version(val=versionWithBuild())}<br>"
|
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
|
||||||
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
|
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
|
||||||
platform.python_version(),
|
platform.python_version(),
|
||||||
QT_VERSION_STR,
|
QT_VERSION_STR,
|
||||||
|
|
|
@ -10,7 +10,7 @@ from anki.collection import OpChanges, SearchNode
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from anki.notes import Note, NoteFieldsCheckResult, NoteId
|
from anki.notes import Note, NoteFieldsCheckResult, NoteId
|
||||||
from anki.utils import htmlToTextLine, isMac
|
from anki.utils import html_to_text_line, isMac
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.deckchooser import DeckChooser
|
from aqt.deckchooser import DeckChooser
|
||||||
from aqt.notetypechooser import NotetypeChooser
|
from aqt.notetypechooser import NotetypeChooser
|
||||||
|
@ -197,7 +197,7 @@ class AddCards(QDialog):
|
||||||
if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))):
|
if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))):
|
||||||
note = self.col.get_note(nid)
|
note = self.col.get_note(nid)
|
||||||
fields = note.fields
|
fields = note.fields
|
||||||
txt = htmlToTextLine(", ".join(fields))
|
txt = html_to_text_line(", ".join(fields))
|
||||||
if len(txt) > 30:
|
if len(txt) > 30:
|
||||||
txt = f"{txt[:30]}..."
|
txt = f"{txt[:30]}..."
|
||||||
line = tr.adding_edit(val=txt)
|
line = tr.adding_edit(val=txt)
|
||||||
|
|
|
@ -95,7 +95,7 @@ class UpdateInfo:
|
||||||
|
|
||||||
ANKIWEB_ID_RE = re.compile(r"^\d+$")
|
ANKIWEB_ID_RE = re.compile(r"^\d+$")
|
||||||
|
|
||||||
current_point_version = anki.utils.pointVersion()
|
current_point_version = anki.utils.point_version()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -983,7 +983,7 @@ def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
return DownloadError(status_code=resp.status_code)
|
return DownloadError(status_code=resp.status_code)
|
||||||
|
|
||||||
data = client.streamContent(resp)
|
data = client.stream_content(resp)
|
||||||
|
|
||||||
fname = re.match(
|
fname = re.match(
|
||||||
"attachment; filename=(.+)", resp.headers["content-disposition"]
|
"attachment; filename=(.+)", resp.headers["content-disposition"]
|
||||||
|
|
|
@ -484,7 +484,7 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.then((toolbar) => toolba
|
||||||
json.dumps(focusTo),
|
json.dumps(focusTo),
|
||||||
json.dumps(self.note.id),
|
json.dumps(self.note.id),
|
||||||
json.dumps([text_color, highlight_color]),
|
json.dumps([text_color, highlight_color]),
|
||||||
json.dumps(self.mw.col.tags.canonify(self.note.tags)),
|
json.dumps(self.note.tags),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.addMode:
|
if self.addMode:
|
||||||
|
@ -674,12 +674,12 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.then((toolbar) => toolba
|
||||||
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
||||||
"""Add to media folder and return local img or sound tag."""
|
"""Add to media folder and return local img or sound tag."""
|
||||||
# copy to media folder
|
# copy to media folder
|
||||||
fname = self.mw.col.media.addFile(path)
|
fname = self.mw.col.media.add_file(path)
|
||||||
# return a local html link
|
# return a local html link
|
||||||
return self.fnameToLink(fname)
|
return self.fnameToLink(fname)
|
||||||
|
|
||||||
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
||||||
return self.mw.col.media.writeData(fname, data)
|
return self.mw.col.media._legacy_write_data(fname, data)
|
||||||
|
|
||||||
def onRecSound(self) -> None:
|
def onRecSound(self) -> None:
|
||||||
aqt.sound.record_audio(
|
aqt.sound.record_audio(
|
||||||
|
|
|
@ -31,7 +31,7 @@ from anki.decks import DeckDict, DeckId
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from anki.sound import AVTag, SoundOrVideoTag
|
from anki.sound import AVTag, SoundOrVideoTag
|
||||||
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
|
from anki.utils import devMode, ids2str, int_time, isMac, isWin, split_fields
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
|
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
|
||||||
from aqt.dbcheck import check_db
|
from aqt.dbcheck import check_db
|
||||||
|
@ -633,11 +633,11 @@ class AnkiQt(QMainWindow):
|
||||||
|
|
||||||
def maybeOptimize(self) -> None:
|
def maybeOptimize(self) -> None:
|
||||||
# have two weeks passed?
|
# have two weeks passed?
|
||||||
if (intTime() - self.pm.profile["lastOptimize"]) < 86400 * 14:
|
if (int_time() - self.pm.profile["lastOptimize"]) < 86400 * 14:
|
||||||
return
|
return
|
||||||
self.progress.start(label=tr.qt_misc_optimizing())
|
self.progress.start(label=tr.qt_misc_optimizing())
|
||||||
self.col.optimize()
|
self.col.optimize()
|
||||||
self.pm.profile["lastOptimize"] = intTime()
|
self.pm.profile["lastOptimize"] = int_time()
|
||||||
self.pm.save()
|
self.pm.save()
|
||||||
self.progress.finish()
|
self.progress.finish()
|
||||||
|
|
||||||
|
@ -877,7 +877,7 @@ title="{}" {}>{}</button>""".format(
|
||||||
|
|
||||||
def maybe_check_for_addon_updates(self) -> None:
|
def maybe_check_for_addon_updates(self) -> None:
|
||||||
last_check = self.pm.last_addon_update_check()
|
last_check = self.pm.last_addon_update_check()
|
||||||
elap = intTime() - last_check
|
elap = int_time() - last_check
|
||||||
|
|
||||||
if elap > 86_400:
|
if elap > 86_400:
|
||||||
check_and_prompt_for_updates(
|
check_and_prompt_for_updates(
|
||||||
|
@ -886,7 +886,7 @@ title="{}" {}>{}</button>""".format(
|
||||||
self.on_updates_installed,
|
self.on_updates_installed,
|
||||||
requested_by_user=False,
|
requested_by_user=False,
|
||||||
)
|
)
|
||||||
self.pm.set_last_addon_update_check(intTime())
|
self.pm.set_last_addon_update_check(int_time())
|
||||||
|
|
||||||
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
|
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
|
||||||
if log:
|
if log:
|
||||||
|
@ -1321,7 +1321,7 @@ title="{}" {}>{}</button>""".format(
|
||||||
for id, mid, flds in col.db.execute(
|
for id, mid, flds in col.db.execute(
|
||||||
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
||||||
):
|
):
|
||||||
fields = splitFields(flds)
|
fields = split_fields(flds)
|
||||||
f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8"))
|
f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8"))
|
||||||
f.write(b"\n")
|
f.write(b"\n")
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import aqt
|
||||||
from anki.collection import Progress
|
from anki.collection import Progress
|
||||||
from anki.errors import Interrupted, NetworkError
|
from anki.errors import Interrupted, NetworkError
|
||||||
from anki.types import assert_exhaustive
|
from anki.types import assert_exhaustive
|
||||||
from anki.utils import intTime
|
from anki.utils import int_time
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import QDialog, QDialogButtonBox, QPushButton, QTextCursor, QTimer, qconnect
|
from aqt.qt import QDialog, QDialogButtonBox, QPushButton, QTextCursor, QTimer, qconnect
|
||||||
from aqt.utils import disable_help_button, showWarning, tr
|
from aqt.utils import disable_help_button, showWarning, tr
|
||||||
|
@ -67,7 +67,7 @@ class MediaSyncer:
|
||||||
self.mw.taskman.run_in_background(run, self._on_finished)
|
self.mw.taskman.run_in_background(run, self._on_finished)
|
||||||
|
|
||||||
def _log_and_notify(self, entry: LogEntry) -> None:
|
def _log_and_notify(self, entry: LogEntry) -> None:
|
||||||
entry_with_time = LogEntryWithTime(time=intTime(), entry=entry)
|
entry_with_time = LogEntryWithTime(time=int_time(), entry=entry)
|
||||||
self._log.append(entry_with_time)
|
self._log.append(entry_with_time)
|
||||||
self.mw.taskman.run_on_main(
|
self.mw.taskman.run_on_main(
|
||||||
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
||||||
|
@ -142,7 +142,7 @@ class MediaSyncer:
|
||||||
last = self._log[-1].time
|
last = self._log[-1].time
|
||||||
else:
|
else:
|
||||||
last = 0
|
last = 0
|
||||||
return intTime() - last
|
return int_time() - last
|
||||||
|
|
||||||
|
|
||||||
class MediaSyncDialog(QDialog):
|
class MediaSyncDialog(QDialog):
|
||||||
|
|
|
@ -244,7 +244,7 @@ class Preferences(QDialog):
|
||||||
|
|
||||||
def current_lang_index(self) -> int:
|
def current_lang_index(self) -> int:
|
||||||
codes = [x[1] for x in anki.lang.langs]
|
codes = [x[1] for x in anki.lang.langs]
|
||||||
lang = anki.lang.currentLang
|
lang = anki.lang.current_lang
|
||||||
if lang in anki.lang.compatMap:
|
if lang in anki.lang.compatMap:
|
||||||
lang = anki.lang.compatMap[lang]
|
lang = anki.lang.compatMap[lang]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -20,7 +20,7 @@ from anki.collection import Collection
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.sync import SyncAuth
|
from anki.sync import SyncAuth
|
||||||
from anki.utils import intTime, isMac, isWin
|
from anki.utils import int_time, isMac, isWin
|
||||||
from aqt import appHelpSite
|
from aqt import appHelpSite
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import disable_help_button, showWarning, tr
|
from aqt.utils import disable_help_button, showWarning, tr
|
||||||
|
@ -68,7 +68,7 @@ class VideoDriver(Enum):
|
||||||
metaConf = dict(
|
metaConf = dict(
|
||||||
ver=0,
|
ver=0,
|
||||||
updates=True,
|
updates=True,
|
||||||
created=intTime(),
|
created=int_time(),
|
||||||
id=random.randrange(0, 2 ** 63),
|
id=random.randrange(0, 2 ** 63),
|
||||||
lastMsg=-1,
|
lastMsg=-1,
|
||||||
suppressUpdate=False,
|
suppressUpdate=False,
|
||||||
|
@ -81,7 +81,7 @@ profileConf: dict[str, Any] = dict(
|
||||||
mainWindowGeom=None,
|
mainWindowGeom=None,
|
||||||
mainWindowState=None,
|
mainWindowState=None,
|
||||||
numBackups=50,
|
numBackups=50,
|
||||||
lastOptimize=intTime(),
|
lastOptimize=int_time(),
|
||||||
# editing
|
# editing
|
||||||
searchHistory=[],
|
searchHistory=[],
|
||||||
lastTextColor="#00f",
|
lastTextColor="#00f",
|
||||||
|
|
|
@ -19,7 +19,7 @@ from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||||
from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards
|
from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards
|
||||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||||
from anki.tags import MARKED_TAG
|
from anki.tags import MARKED_TAG
|
||||||
from anki.utils import stripHTML
|
from anki.utils import strip_html
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.browser.card_info import PreviousReviewerCardInfo, ReviewerCardInfo
|
from aqt.browser.card_info import PreviousReviewerCardInfo, ReviewerCardInfo
|
||||||
from aqt.deckoptions import confirm_deck_then_display_options
|
from aqt.deckoptions import confirm_deck_then_display_options
|
||||||
|
@ -573,7 +573,7 @@ class Reviewer:
|
||||||
# munge correct value
|
# munge correct value
|
||||||
cor = self.mw.col.media.strip(self.typeCorrect)
|
cor = self.mw.col.media.strip(self.typeCorrect)
|
||||||
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
||||||
cor = stripHTML(cor)
|
cor = strip_html(cor)
|
||||||
# ensure we don't chomp multiple whitespace
|
# ensure we don't chomp multiple whitespace
|
||||||
cor = cor.replace(" ", " ")
|
cor = cor.replace(" ", " ")
|
||||||
cor = html.unescape(cor)
|
cor = html.unescape(cor)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import aqt
|
||||||
from anki.errors import Interrupted, SyncError, SyncErrorKind
|
from anki.errors import Interrupted, SyncError, SyncErrorKind
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.sync import SyncOutput, SyncStatus
|
from anki.sync import SyncOutput, SyncStatus
|
||||||
from anki.utils import platDesc
|
from anki.utils import plat_desc
|
||||||
from aqt.qt import (
|
from aqt.qt import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
|
@ -331,4 +331,4 @@ def get_id_and_pass_from_user(
|
||||||
|
|
||||||
|
|
||||||
# export platform version to syncing code
|
# export platform version to syncing code
|
||||||
os.environ["PLATFORM"] = platDesc()
|
os.environ["PLATFORM"] = plat_desc()
|
||||||
|
|
|
@ -39,7 +39,7 @@ class TagLimit(QDialog):
|
||||||
self.exec()
|
self.exec()
|
||||||
|
|
||||||
def rebuildTagList(self) -> None:
|
def rebuildTagList(self) -> None:
|
||||||
usertags = self.mw.col.tags.byDeck(self.deck["id"], True)
|
usertags = self.mw.col.tags.by_deck(self.deck["id"], True)
|
||||||
yes = self.deck.get("activeTags", [])
|
yes = self.deck.get("activeTags", [])
|
||||||
no = self.deck.get("inactiveTags", [])
|
no = self.deck.get("inactiveTags", [])
|
||||||
yesHash = {}
|
yesHash = {}
|
||||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.utils import platDesc, versionWithBuild
|
from anki.utils import plat_desc, version_with_build
|
||||||
from aqt.main import AnkiQt
|
from aqt.main import AnkiQt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import openLink, showText, tr
|
from aqt.utils import openLink, showText, tr
|
||||||
|
@ -26,8 +26,8 @@ class LatestVersionFinder(QThread):
|
||||||
|
|
||||||
def _data(self) -> dict[str, Any]:
|
def _data(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"ver": versionWithBuild(),
|
"ver": version_with_build(),
|
||||||
"os": platDesc(),
|
"os": plat_desc(),
|
||||||
"id": self.config["id"],
|
"id": self.config["id"],
|
||||||
"lm": self.config["lastMsg"],
|
"lm": self.config["lastMsg"],
|
||||||
"crt": self.config["created"],
|
"crt": self.config["created"],
|
||||||
|
|
|
@ -12,7 +12,13 @@ from typing import TYPE_CHECKING, Any, Literal, Sequence
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import Collection, HelpPage
|
from anki.collection import Collection, HelpPage
|
||||||
from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import
|
from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import
|
||||||
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
from anki.utils import (
|
||||||
|
invalid_filename,
|
||||||
|
isMac,
|
||||||
|
isWin,
|
||||||
|
no_bundled_libs,
|
||||||
|
version_with_build,
|
||||||
|
)
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
|
|
||||||
|
@ -57,7 +63,7 @@ def openHelp(section: HelpPageArgument) -> None:
|
||||||
|
|
||||||
def openLink(link: 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 no_bundled_libs():
|
||||||
QDesktopServices.openUrl(QUrl(link))
|
QDesktopServices.openUrl(QUrl(link))
|
||||||
|
|
||||||
|
|
||||||
|
@ -658,7 +664,7 @@ def openFolder(path: str) -> None:
|
||||||
if isWin:
|
if isWin:
|
||||||
subprocess.run(["explorer", f"file://{path}"], check=False)
|
subprocess.run(["explorer", f"file://{path}"], check=False)
|
||||||
else:
|
else:
|
||||||
with noBundledLibs():
|
with no_bundled_libs():
|
||||||
QDesktopServices.openUrl(QUrl(f"file://{path}"))
|
QDesktopServices.openUrl(QUrl(f"file://{path}"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -762,7 +768,7 @@ def closeTooltip() -> None:
|
||||||
|
|
||||||
# true if invalid; print warning
|
# true if invalid; print warning
|
||||||
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
||||||
bad = invalidFilename(str, dirsep)
|
bad = invalid_filename(str, dirsep)
|
||||||
if bad:
|
if bad:
|
||||||
showWarning(tr.qt_misc_the_following_character_can_not_be(val=bad))
|
showWarning(tr.qt_misc_the_following_character_can_not_be(val=bad))
|
||||||
return True
|
return True
|
||||||
|
@ -874,7 +880,7 @@ Platform: {}
|
||||||
Flags: frz={} ao={} sv={}
|
Flags: frz={} ao={} sv={}
|
||||||
Add-ons, last update check: {}
|
Add-ons, last update check: {}
|
||||||
""".format(
|
""".format(
|
||||||
versionWithBuild(),
|
version_with_build(),
|
||||||
platform.python_version(),
|
platform.python_version(),
|
||||||
QT_VERSION_STR,
|
QT_VERSION_STR,
|
||||||
PYQT_VERSION_STR,
|
PYQT_VERSION_STR,
|
||||||
|
|
|
@ -439,7 +439,7 @@ div[contenteditable="true"]:focus {{
|
||||||
window_bg_night = self.get_window_bg_color(True).name()
|
window_bg_night = self.get_window_bg_color(True).name()
|
||||||
body_bg = window_bg_night if theme_manager.night_mode else window_bg_day
|
body_bg = window_bg_night if theme_manager.night_mode else window_bg_day
|
||||||
|
|
||||||
if is_rtl(anki.lang.currentLang):
|
if is_rtl(anki.lang.current_lang):
|
||||||
lang_dir = "rtl"
|
lang_dir = "rtl"
|
||||||
else:
|
else:
|
||||||
lang_dir = "ltr"
|
lang_dir = "ltr"
|
||||||
|
|
Loading…
Reference in a new issue