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,
|
||||
db,
|
||||
ok,
|
||||
ip,
|
||||
ip,
|
||||
|
|
|
@ -49,7 +49,6 @@ py_library(
|
|||
requirement("flask"),
|
||||
requirement("waitress"),
|
||||
requirement("markdown"),
|
||||
"//pylib/anki/_vendor:stringcase",
|
||||
] + orjson_if_available(),
|
||||
)
|
||||
|
||||
|
@ -84,7 +83,6 @@ py_wheel(
|
|||
"decorator",
|
||||
"protobuf>=3.17",
|
||||
"markdown",
|
||||
"stringcase",
|
||||
"orjson",
|
||||
'psutil; sys_platform == "win32"',
|
||||
'distro; sys_platform != "darwin" and sys_platform != "win32"',
|
||||
|
|
|
@ -66,7 +66,7 @@ class RustBackend(RustBackendGenerated):
|
|||
) -> None:
|
||||
# pick up global defaults if not provided
|
||||
if langs is None:
|
||||
langs = [anki.lang.currentLang]
|
||||
langs = [anki.lang.current_lang]
|
||||
|
||||
init_msg = backend_pb2.BackendInit(
|
||||
preferred_langs=langs,
|
||||
|
|
|
@ -17,11 +17,12 @@ VariableTarget = tuple[Any, str]
|
|||
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):
|
||||
return name
|
||||
else:
|
||||
return target[1] # type: ignore
|
||||
return target[1] # type: ignore
|
||||
|
||||
|
||||
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:])
|
||||
|
||||
|
||||
def print_deprecation_warning(msg: str, frame: int = 2) -> None:
|
||||
path, linenum, _, _ = traceback.extract_stack(limit=5)[frame]
|
||||
def print_deprecation_warning(msg: str, frame: int = 1) -> None:
|
||||
# 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)
|
||||
print(f"{path}:{linenum}:{msg}")
|
||||
|
||||
|
||||
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:
|
||||
|
@ -45,7 +66,7 @@ class DeprecatedNamesMixin:
|
|||
# deprecated name -> new name
|
||||
_deprecated_aliases: dict[str, str] = {}
|
||||
# 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
|
||||
# attributes on the consuming class
|
||||
|
@ -53,27 +74,16 @@ class DeprecatedNamesMixin:
|
|||
@no_type_check
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
remapped, replacement = self._get_remapped_and_replacement(name)
|
||||
remapped, replacement = _get_remapped_and_replacement(self, name)
|
||||
out = getattr(self, remapped)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||
) from None
|
||||
|
||||
_print_warning(f"'{name}'", f"please use '{replacement}'")
|
||||
_print_replacement_warning(name, replacement)
|
||||
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
|
||||
def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None:
|
||||
"""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()}
|
||||
|
||||
@no_type_check
|
||||
@classmethod
|
||||
def register_deprecated_attributes(
|
||||
cls,
|
||||
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget],
|
||||
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget | None],
|
||||
) -> None:
|
||||
"""Manually add deprecated attributes without exact substitutes.
|
||||
|
||||
Pass a tuple of (alias, replacement), where alias is the attribute's new
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
It can be invoked like this:
|
||||
|
@ -120,23 +129,40 @@ class DeprecatedNamesMixinForModule(DeprecatedNamesMixin):
|
|||
def __getattr__(name: str) -> Any:
|
||||
return _deprecated_names.__getattr__(name)
|
||||
```
|
||||
See DeprecatedNamesMixin for more documentation.
|
||||
"""
|
||||
|
||||
def __init__(self, module_globals: dict[str, Any]) -> None:
|
||||
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:
|
||||
try:
|
||||
remapped, replacement = self._get_remapped_and_replacement(name)
|
||||
remapped, replacement = _get_remapped_and_replacement(self, name)
|
||||
out = self.module_globals[remapped]
|
||||
except (AttributeError, KeyError):
|
||||
raise AttributeError(
|
||||
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
|
||||
) 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
|
||||
|
||||
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:
|
||||
"""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:
|
||||
@functools.wraps(func)
|
||||
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
||||
if replaced_by:
|
||||
doc = f"please use {replaced_by.__name__} instead."
|
||||
if info:
|
||||
_print_warning(f"{func.__name__}()", info)
|
||||
else:
|
||||
doc = info
|
||||
|
||||
_print_warning(f"{func.__name__}()", doc)
|
||||
_print_replacement_warning(func.__name__, replaced_by.__name__)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_func
|
||||
|
||||
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
|
||||
# MIT: https://github.com/okunishinishi/python-stringcase
|
||||
|
||||
# type: ignore
|
||||
|
||||
"""
|
||||
String convert functions
|
||||
|
|
|
@ -66,9 +66,9 @@ from anki.types import assert_exhaustive
|
|||
from anki.utils import (
|
||||
from_json_bytes,
|
||||
ids2str,
|
||||
intTime,
|
||||
splitFields,
|
||||
stripHTMLMedia,
|
||||
int_time,
|
||||
split_fields,
|
||||
strip_html_media,
|
||||
to_json_bytes,
|
||||
)
|
||||
|
||||
|
@ -292,7 +292,7 @@ class Collection(DeprecatedNamesMixin):
|
|||
self.db.begin()
|
||||
|
||||
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()
|
||||
|
||||
def mod_schema(self, check: bool) -> None:
|
||||
|
@ -578,12 +578,12 @@ class Collection(DeprecatedNamesMixin):
|
|||
for nid, mid, flds in self.db.all(
|
||||
f"select id, mid, flds from notes where id in {ids2str(nids)}"
|
||||
):
|
||||
flds = splitFields(flds)
|
||||
flds = split_fields(flds)
|
||||
ord = ord_for_mid(mid)
|
||||
if ord is None:
|
||||
continue
|
||||
val = flds[ord]
|
||||
val = stripHTMLMedia(val)
|
||||
val = strip_html_media(val)
|
||||
# empty does not count as duplicate
|
||||
if not val:
|
||||
continue
|
||||
|
@ -982,7 +982,7 @@ class Collection(DeprecatedNamesMixin):
|
|||
# restore any siblings
|
||||
self.db.execute(
|
||||
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
|
||||
intTime(),
|
||||
int_time(),
|
||||
self.usn(),
|
||||
card.nid,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
@ -51,9 +53,9 @@ class DBProxy:
|
|||
**kwargs: ValueForDB,
|
||||
) -> list[Row]:
|
||||
# mark modified?
|
||||
s = sql.strip().lower()
|
||||
cananoized = sql.strip().lower()
|
||||
for stmt in "insert", "update", "delete":
|
||||
if s.startswith(stmt):
|
||||
if cananoized.startswith(stmt):
|
||||
self.modified_in_python = True
|
||||
sql, args2 = emulate_named_args(sql, args, kwargs)
|
||||
# fetch rows
|
||||
|
@ -113,11 +115,11 @@ def emulate_named_args(
|
|||
args2 = list(args)
|
||||
for key, val in kwargs.items():
|
||||
args2.append(val)
|
||||
n = len(args2)
|
||||
arg_num[key] = n
|
||||
number = len(args2)
|
||||
arg_num[key] = number
|
||||
# update refs
|
||||
def repl(m: Match) -> str:
|
||||
arg = m.group(1)
|
||||
def repl(match: Match) -> str:
|
||||
arg = match.group(1)
|
||||
return f"?{arg_num[arg]}"
|
||||
|
||||
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.consts import *
|
||||
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
|
||||
DeckTreeNode = decks_pb2.DeckTreeNode
|
||||
|
@ -544,7 +544,7 @@ class DeckManager(DeprecatedNamesMixin):
|
|||
f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}",
|
||||
did,
|
||||
self.col.usn(),
|
||||
intTime(),
|
||||
int_time(),
|
||||
)
|
||||
|
||||
@deprecated(replaced_by=all_names_and_ids)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
|
|
@ -19,7 +19,7 @@ from anki import hooks
|
|||
from anki.cards import CardId
|
||||
from anki.collection import Collection
|
||||
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:
|
||||
|
@ -78,7 +78,7 @@ class Exporter:
|
|||
s = text
|
||||
s = re.sub(r"(?i)<(br ?/?|div|p)>", " ", s)
|
||||
s = re.sub(r"\[sound:[^]]+\]", "", s)
|
||||
s = stripHTML(s)
|
||||
s = strip_html(s)
|
||||
s = re.sub(r"[ \n\t]+", " ", s)
|
||||
s = s.strip()
|
||||
return s
|
||||
|
@ -161,7 +161,7 @@ where cards.id in %s)"""
|
|||
if self.includeID:
|
||||
row.append(str(id))
|
||||
# fields
|
||||
row.extend([self.processText(f) for f in splitFields(flds)])
|
||||
row.extend([self.processText(f) for f in split_fields(flds)])
|
||||
# tags
|
||||
if self.includeTags:
|
||||
row.append(tags.strip())
|
||||
|
@ -280,7 +280,7 @@ class AnkiExporter(Exporter):
|
|||
for row in notedata:
|
||||
flds = row[6]
|
||||
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
|
||||
if file != os.path.basename(file):
|
||||
continue
|
||||
|
@ -310,7 +310,7 @@ class AnkiExporter(Exporter):
|
|||
pass
|
||||
|
||||
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:
|
||||
# First check the styling
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# 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.
|
||||
"""
|
||||
|
@ -14,12 +16,14 @@ from typing import Any, Callable
|
|||
import requests
|
||||
from requests import Response
|
||||
|
||||
from anki._legacy import DeprecatedNamesMixin
|
||||
|
||||
HTTP_BUF_SIZE = 64 * 1024
|
||||
|
||||
ProgressCallback = Callable[[int, int], None]
|
||||
|
||||
|
||||
class HttpClient:
|
||||
class HttpClient(DeprecatedNamesMixin):
|
||||
|
||||
verify = True
|
||||
timeout = 60
|
||||
|
@ -45,7 +49,7 @@ class HttpClient:
|
|||
self.close()
|
||||
|
||||
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(
|
||||
url,
|
||||
data=data,
|
||||
|
@ -58,12 +62,12 @@ class HttpClient:
|
|||
def get(self, url: str, headers: dict[str, str] = None) -> Response:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["User-Agent"] = self._agentName()
|
||||
headers["User-Agent"] = self._agent_name()
|
||||
return self.session.get(
|
||||
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()
|
||||
|
||||
buf = io.BytesIO()
|
||||
|
@ -73,7 +77,7 @@ class HttpClient:
|
|||
buf.write(chunk)
|
||||
return buf.getvalue()
|
||||
|
||||
def _agentName(self) -> str:
|
||||
def _agent_name(self) -> str:
|
||||
from anki.buildinfo import version
|
||||
|
||||
return f"Anki {version}"
|
||||
|
|
|
@ -14,7 +14,7 @@ from anki.decks import DeckId, DeckManager
|
|||
from anki.importing.base import Importer
|
||||
from anki.models import NotetypeId
|
||||
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
|
||||
MID = 2
|
||||
|
@ -82,7 +82,7 @@ class Anki2Importer(Importer):
|
|||
|
||||
def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
|
||||
self.log.append(
|
||||
"[{}] {}".format(action, stripHTMLMedia(noteRow[6].replace("\x1f", ", ")))
|
||||
"[{}] {}".format(action, strip_html_media(noteRow[6].replace("\x1f", ", ")))
|
||||
)
|
||||
|
||||
def _importNotes(self) -> None:
|
||||
|
@ -282,7 +282,7 @@ class Anki2Importer(Importer):
|
|||
# if target is a filtered deck, we'll need a new deck name
|
||||
deck = self.dst.decks.by_name(name)
|
||||
if deck and deck["dyn"]:
|
||||
name = "%s %d" % (name, intTime())
|
||||
name = "%s %d" % (name, int_time())
|
||||
# create in local
|
||||
newid = self.dst.decks.id(name)
|
||||
# pull conf over
|
||||
|
@ -345,7 +345,7 @@ class Anki2Importer(Importer):
|
|||
# update cid, nid, etc
|
||||
card[1] = self._notes[guid][0]
|
||||
card[2] = self._did(card[2])
|
||||
card[4] = intTime()
|
||||
card[4] = int_time()
|
||||
card[5] = usn
|
||||
# review cards have a due date relative to collection
|
||||
if (
|
||||
|
@ -434,7 +434,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
|
|||
pass
|
||||
|
||||
def _mungeMedia(self, mid: NotetypeId, fieldsStr: str) -> str:
|
||||
fields = splitFields(fieldsStr)
|
||||
fields = split_fields(fieldsStr)
|
||||
|
||||
def repl(match):
|
||||
fname = match.group("fname")
|
||||
|
@ -459,8 +459,8 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
|
|||
return match.group(0).replace(fname, lname)
|
||||
|
||||
for idx, field in enumerate(fields):
|
||||
fields[idx] = self.dst.media.transformNames(field, repl)
|
||||
return joinFields(fields)
|
||||
fields[idx] = self.dst.media.transform_names(field, repl)
|
||||
return join_fields(fields)
|
||||
|
||||
# Post-import cleanup
|
||||
######################################################################
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from anki.collection import Collection
|
||||
from anki.utils import maxID
|
||||
from anki.utils import max_id
|
||||
|
||||
# Base importer
|
||||
##########################################################################
|
||||
|
@ -41,7 +41,7 @@ class Importer:
|
|||
# need to make sure our starting point is safe.
|
||||
|
||||
def _prepareTS(self) -> None:
|
||||
self._ts = maxID(self.dst.db)
|
||||
self._ts = max_id(self.dst.db)
|
||||
|
||||
def ts(self) -> Any:
|
||||
self._ts += 1
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import cast
|
|||
|
||||
from anki.db import DB
|
||||
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):
|
||||
|
@ -132,7 +132,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
|||
data.append(n)
|
||||
# add a basic model
|
||||
if not model:
|
||||
model = addBasicModel(self.col)
|
||||
model = _legacy_add_basic_model(self.col)
|
||||
model["name"] = "Mnemosyne-FrontOnly"
|
||||
mm = self.col.models
|
||||
mm.save(model)
|
||||
|
@ -144,7 +144,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
|||
self.importNotes(data)
|
||||
|
||||
def _addFrontBacks(self, notes):
|
||||
m = addBasicModel(self.col)
|
||||
m = _legacy_add_basic_model(self.col)
|
||||
m["name"] = "Mnemosyne-FrontBack"
|
||||
mm = self.col.models
|
||||
t = mm.new_template("Back")
|
||||
|
@ -200,7 +200,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""
|
|||
n.cards = orig.get("cards", {})
|
||||
data.append(n)
|
||||
# add cloze model
|
||||
model = addClozeModel(self.col)
|
||||
model = _legacy_add_cloze_model(self.col)
|
||||
model["name"] = "Mnemosyne-Cloze"
|
||||
mm = self.col.models
|
||||
mm.save(model)
|
||||
|
|
|
@ -16,12 +16,12 @@ from anki.importing.base import Importer
|
|||
from anki.models import NotetypeId
|
||||
from anki.notes import NoteId
|
||||
from anki.utils import (
|
||||
fieldChecksum,
|
||||
field_checksum,
|
||||
guid64,
|
||||
intTime,
|
||||
joinFields,
|
||||
splitFields,
|
||||
timestampID,
|
||||
int_time,
|
||||
join_fields,
|
||||
split_fields,
|
||||
timestamp_id,
|
||||
)
|
||||
|
||||
TagMappedUpdate = tuple[int, int, str, str, NoteId, str, str]
|
||||
|
@ -135,7 +135,7 @@ class NoteImporter(Importer):
|
|||
firsts: dict[str, bool] = {}
|
||||
fld0idx = self.mapping.index(self.model["flds"][0]["name"])
|
||||
self._fmap = self.col.models.field_map(self.model)
|
||||
self._nextID = NoteId(timestampID(self.col.db, "notes"))
|
||||
self._nextID = NoteId(timestamp_id(self.col.db, "notes"))
|
||||
# loop through the notes
|
||||
updates: list[Updates] = []
|
||||
updateLog = []
|
||||
|
@ -158,7 +158,7 @@ class NoteImporter(Importer):
|
|||
self.col.tr.importing_empty_first_field(val=" ".join(n.fields))
|
||||
)
|
||||
continue
|
||||
csum = fieldChecksum(fld0)
|
||||
csum = field_checksum(fld0)
|
||||
# earlier in import?
|
||||
if fld0 in firsts and self.importMode != ADD_MODE:
|
||||
# duplicates in source file; log and ignore
|
||||
|
@ -171,7 +171,7 @@ class NoteImporter(Importer):
|
|||
# csum is not a guarantee; have to check
|
||||
for id in csums[csum]:
|
||||
flds = self.col.db.scalar("select flds from notes where id = ?", id)
|
||||
sflds = splitFields(flds)
|
||||
sflds = split_fields(flds)
|
||||
if fld0 == sflds[0]:
|
||||
# duplicate
|
||||
found = True
|
||||
|
@ -246,7 +246,7 @@ class NoteImporter(Importer):
|
|||
id,
|
||||
guid64(),
|
||||
self.model["id"],
|
||||
intTime(),
|
||||
int_time(),
|
||||
self.col.usn(),
|
||||
self.col.tags.join(n.tags),
|
||||
n.fieldsStr,
|
||||
|
@ -273,14 +273,22 @@ class NoteImporter(Importer):
|
|||
self.processFields(n, sflds)
|
||||
if self._tagsMapped:
|
||||
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:
|
||||
tags = self.col.db.scalar("select tags from notes where id = ?", id)
|
||||
tagList = self.col.tags.split(tags) + self.tagModified.split()
|
||||
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:
|
||||
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:
|
||||
changes = self.col.db.scalar("select total_changes()")
|
||||
|
@ -321,7 +329,7 @@ where id = ? and flds != ?""",
|
|||
else:
|
||||
sidx = self._fmap[f][0]
|
||||
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:
|
||||
# https://forums.ankiweb.net/t/python-checksum-rust-checksum/8195/16
|
||||
if self.col.get_config_bool(Config.Bool.NORMALIZE_NOTE_TEXT):
|
||||
|
|
|
@ -9,7 +9,7 @@ import time
|
|||
import xml.etree.ElementTree as ET
|
||||
|
||||
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
|
||||
|
||||
|
@ -21,7 +21,7 @@ class PaukerImporter(NoteImporter):
|
|||
allowHTML = True
|
||||
|
||||
def run(self):
|
||||
model = addForwardReverse(self.col)
|
||||
model = _legacy_add_forward_reverse(self.col)
|
||||
model["name"] = "Pauker"
|
||||
self.col.models.save(model, updateReqs=False)
|
||||
self.col.models.set_current(model)
|
||||
|
|
|
@ -15,7 +15,7 @@ from xml.dom.minidom import Element, Text
|
|||
|
||||
from anki.collection import Collection
|
||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
||||
from anki.stdmodels import addBasicModel
|
||||
from anki.stdmodels import _legacy_add_basic_model
|
||||
|
||||
|
||||
class SmartDict(dict):
|
||||
|
@ -87,7 +87,7 @@ class SupermemoXmlImporter(NoteImporter):
|
|||
"""Initialize internal varables.
|
||||
Pameters to be exposed to GUI are stored in self.META"""
|
||||
NoteImporter.__init__(self, col, file)
|
||||
m = addBasicModel(self.col)
|
||||
m = _legacy_add_basic_model(self.col)
|
||||
m["name"] = "Supermemo"
|
||||
self.col.models.save(m)
|
||||
self.initMapping()
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import locale
|
||||
import re
|
||||
import weakref
|
||||
from typing import Any, no_type_check
|
||||
|
||||
import anki
|
||||
import anki._backend
|
||||
import anki.i18n_pb2 as _pb
|
||||
from anki._legacy import DeprecatedNamesMixinForModule
|
||||
|
||||
# public exports
|
||||
TR = anki._backend.LegacyTranslationEnum
|
||||
|
@ -138,21 +142,21 @@ def lang_to_disk_lang(lang: str) -> str:
|
|||
):
|
||||
return lang.replace("_", "-")
|
||||
# other languages have the region portion stripped
|
||||
m = re.match("(.*)_", lang)
|
||||
if m:
|
||||
return m.group(1)
|
||||
match = re.match("(.*)_", lang)
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
return lang
|
||||
|
||||
|
||||
# the currently set interface language
|
||||
currentLang = "en"
|
||||
current_lang = "en" # pylint: disable=invalid-name
|
||||
|
||||
# the current Fluent translation instance. Code in pylib/ should
|
||||
# not reference this, and should use col.tr instead. The global
|
||||
# instance exists for legacy reasons, and as a convenience for the
|
||||
# Qt code.
|
||||
current_i18n: anki._backend.RustBackend | None = None
|
||||
current_i18n: anki._backend.RustBackend | None = None # pylint: disable=invalid-name
|
||||
tr_legacyglobal = anki._backend.Translations(None)
|
||||
|
||||
|
||||
|
@ -161,14 +165,14 @@ def _(str: str) -> 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}")
|
||||
return plural
|
||||
|
||||
|
||||
def set_lang(lang: str) -> None:
|
||||
global currentLang, current_i18n
|
||||
currentLang = lang
|
||||
global current_lang, current_i18n # pylint: disable=invalid-name
|
||||
current_lang = lang
|
||||
current_i18n = anki._backend.RustBackend(langs=[lang])
|
||||
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]
|
||||
idx = None
|
||||
lang = None
|
||||
en = None
|
||||
for l in (user_lang, sys_lang):
|
||||
for c, (name, code) in enumerate(langs):
|
||||
en_idx = None
|
||||
for preferred_lang in (user_lang, sys_lang):
|
||||
for lang_idx, (name, code) in enumerate(langs):
|
||||
if code == "en_US":
|
||||
en = c
|
||||
if code == l:
|
||||
idx = c
|
||||
lang = l
|
||||
en_idx = lang_idx
|
||||
if code == preferred_lang:
|
||||
idx = lang_idx
|
||||
lang = preferred_lang
|
||||
if idx is not None:
|
||||
break
|
||||
# if the specified language and the system language aren't available, revert to english
|
||||
if idx is None:
|
||||
idx = en
|
||||
idx = en_idx
|
||||
lang = "en_US"
|
||||
return (idx, lang)
|
||||
|
||||
|
@ -209,9 +213,17 @@ def is_rtl(lang: str) -> bool:
|
|||
|
||||
# strip off unicode isolation markers from a translated string
|
||||
# for testing purposes
|
||||
def without_unicode_isolation(s: str) -> str:
|
||||
return s.replace("\u2068", "").replace("\u2069", "")
|
||||
def without_unicode_isolation(string: str) -> str:
|
||||
return string.replace("\u2068", "").replace("\u2069", "")
|
||||
|
||||
|
||||
def with_collapsed_whitespace(s: str) -> str:
|
||||
return re.sub(r"\s+", " ", s)
|
||||
def with_collapsed_whitespace(string: str) -> str:
|
||||
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
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
@ -25,7 +27,8 @@ svgCommands = [
|
|||
["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
|
||||
if isMac:
|
||||
|
@ -135,10 +138,10 @@ def _save_latex_image(
|
|||
|
||||
# commands to use
|
||||
if svg:
|
||||
latexCmds = svgCommands
|
||||
latex_cmds = svgCommands
|
||||
ext = "svg"
|
||||
else:
|
||||
latexCmds = pngCommands
|
||||
latex_cmds = pngCommands
|
||||
ext = "png"
|
||||
|
||||
# write into a temp file
|
||||
|
@ -152,9 +155,9 @@ def _save_latex_image(
|
|||
try:
|
||||
# generate png/svg
|
||||
os.chdir(tmpdir())
|
||||
for latexCmd in latexCmds:
|
||||
if call(latexCmd, stdout=log, stderr=log):
|
||||
return _errMsg(col, latexCmd[0], texpath)
|
||||
for latex_cmd in latex_cmds:
|
||||
if call(latex_cmd, stdout=log, stderr=log):
|
||||
return _err_msg(col, latex_cmd[0], texpath)
|
||||
# add to media
|
||||
with open(png_or_svg, "rb") as file:
|
||||
data = file.read()
|
||||
|
@ -166,12 +169,12 @@ def _save_latex_image(
|
|||
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_generated_file(val=texpath)}<br>"
|
||||
try:
|
||||
with open(namedtmp("latex_log.txt", rm=False), encoding="utf8") as f:
|
||||
log = f.read()
|
||||
with open(namedtmp("latex_log.txt", remove=False), encoding="utf8") as file:
|
||||
log = file.read()
|
||||
if not log:
|
||||
raise Exception()
|
||||
msg += f"<small><pre>{html.escape(log)}</pre></small>"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
@ -11,12 +13,13 @@ import time
|
|||
from typing import Any, Callable
|
||||
|
||||
from anki import media_pb2
|
||||
from anki._legacy import DeprecatedNamesMixin, deprecated_keywords
|
||||
from anki.consts import *
|
||||
from anki.latex import render_latex, render_latex_returning_errors
|
||||
from anki.models import NotetypeId
|
||||
from anki.sound import SoundOrVideoTag
|
||||
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]:
|
||||
|
@ -33,7 +36,7 @@ CheckMediaResponse = media_pb2.CheckMediaResponse
|
|||
# - and audio handling code
|
||||
|
||||
|
||||
class MediaManager:
|
||||
class MediaManager(DeprecatedNamesMixin):
|
||||
|
||||
sound_regexps = [r"(?i)(\[sound:(?P<fname>[^]]+)\])"]
|
||||
html_media_regexps = [
|
||||
|
@ -68,9 +71,9 @@ class MediaManager:
|
|||
raise Exception("invalidTempFolder") from exc
|
||||
|
||||
def __repr__(self) -> str:
|
||||
d = dict(self.__dict__)
|
||||
del d["col"]
|
||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
||||
dict_ = dict(self.__dict__)
|
||||
del dict_["col"]
|
||||
return f"{super().__repr__()} {pprint.pformat(dict_, width=300)}"
|
||||
|
||||
def connect(self) -> None:
|
||||
if self.col.server:
|
||||
|
@ -122,8 +125,8 @@ class MediaManager:
|
|||
"""Add basename of path to the media folder, renaming if not unique.
|
||||
|
||||
Returns possibly-renamed filename."""
|
||||
with open(path, "rb") as f:
|
||||
return self.write_data(os.path.basename(path), f.read())
|
||||
with open(path, "rb") as file:
|
||||
return self.write_data(os.path.basename(path), file.read())
|
||||
|
||||
def write_data(self, desired_fname: str, data: bytes) -> str:
|
||||
"""Write the file to the media folder, renaming if not unique.
|
||||
|
@ -155,10 +158,11 @@ class MediaManager:
|
|||
# String manipulation
|
||||
##########################################################################
|
||||
|
||||
def filesInStr(
|
||||
self, mid: NotetypeId, string: str, includeRemote: bool = False
|
||||
@deprecated_keywords(includeRemote="include_remote")
|
||||
def files_in_str(
|
||||
self, mid: NotetypeId, string: str, include_remote: bool = False
|
||||
) -> list[str]:
|
||||
l = []
|
||||
files = []
|
||||
model = self.col.models.get(mid)
|
||||
# handle latex
|
||||
string = render_latex(string, model, self.col)
|
||||
|
@ -166,12 +170,12 @@ class MediaManager:
|
|||
for reg in self.regexps:
|
||||
for match in re.finditer(reg, string):
|
||||
fname = match.group("fname")
|
||||
isLocal = not re.match("(https?|ftp)://", fname.lower())
|
||||
if isLocal or includeRemote:
|
||||
l.append(fname)
|
||||
return l
|
||||
is_local = not re.match("(https?|ftp)://", fname.lower())
|
||||
if is_local or include_remote:
|
||||
files.append(fname)
|
||||
return files
|
||||
|
||||
def transformNames(self, txt: str, func: Callable) -> Any:
|
||||
def transform_names(self, txt: str, func: Callable) -> Any:
|
||||
for reg in self.regexps:
|
||||
txt = re.sub(reg, func, txt)
|
||||
return txt
|
||||
|
@ -182,7 +186,7 @@ class MediaManager:
|
|||
txt = re.sub(reg, "", 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."
|
||||
return self.escape_media_filenames(string, unescape)
|
||||
|
||||
|
@ -229,7 +233,7 @@ class MediaManager:
|
|||
checked += 1
|
||||
elap = time.time() - last_progress
|
||||
if elap >= 0.3 and progress_cb is not None:
|
||||
last_progress = intTime()
|
||||
last_progress = int_time()
|
||||
if not progress_cb(checked):
|
||||
return None
|
||||
|
||||
|
@ -240,28 +244,35 @@ class MediaManager:
|
|||
|
||||
_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
|
||||
print("stripIllegal() will go away")
|
||||
return re.sub(self._illegalCharReg, "", str)
|
||||
|
||||
def hasIllegal(self, s: str) -> bool:
|
||||
print("hasIllegal() will go away")
|
||||
if re.search(self._illegalCharReg, s):
|
||||
def _legacy_has_illegal(self, string: str) -> bool:
|
||||
if re.search(self._illegalCharReg, string):
|
||||
return True
|
||||
try:
|
||||
s.encode(sys.getfilesystemencoding())
|
||||
string.encode(sys.getfilesystemencoding())
|
||||
except UnicodeEncodeError:
|
||||
return True
|
||||
return False
|
||||
|
||||
def findChanges(self) -> None:
|
||||
def _legacy_find_changes(self) -> None:
|
||||
pass
|
||||
|
||||
addFile = add_file
|
||||
|
||||
def writeData(self, opath: str, data: bytes, typeHint: str | None = None) -> str:
|
||||
@deprecated_keywords(typeHint="type_hint")
|
||||
def _legacy_write_data(
|
||||
self, opath: str, data: bytes, type_hint: str | None = None
|
||||
) -> str:
|
||||
fname = os.path.basename(opath)
|
||||
if typeHint:
|
||||
fname = self.add_extension_based_on_mime(fname, typeHint)
|
||||
if type_hint:
|
||||
fname = self.add_extension_based_on_mime(fname, type_hint)
|
||||
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.consts import MODEL_STD
|
||||
from anki.models import NotetypeDict, NotetypeId, TemplateDict
|
||||
from anki.utils import joinFields
|
||||
from anki.utils import join_fields
|
||||
|
||||
DuplicateOrEmptyResult = notes_pb2.NoteFieldsCheckResponse.State
|
||||
NoteFieldsCheckResult = notes_pb2.NoteFieldsCheckResponse.State
|
||||
|
@ -84,7 +84,7 @@ class Note(DeprecatedNamesMixin):
|
|||
)
|
||||
|
||||
def joined_fields(self) -> str:
|
||||
return joinFields(self.fields)
|
||||
return join_fields(self.fields)
|
||||
|
||||
def ephemeral_card(
|
||||
self,
|
||||
|
@ -164,7 +164,7 @@ class Note(DeprecatedNamesMixin):
|
|||
##################################################
|
||||
|
||||
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:
|
||||
rem = []
|
||||
|
@ -179,7 +179,7 @@ class Note(DeprecatedNamesMixin):
|
|||
self.tags.append(tag)
|
||||
|
||||
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:
|
||||
self.tags = self.col.tags.split(tags)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# future.
|
||||
#
|
||||
# pylint: disable=unused-import
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from anki.decks import DeckTreeNode
|
||||
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.decks import DeckConfigDict, DeckId, DeckTreeNode
|
||||
from anki.notes import NoteId
|
||||
from anki.utils import ids2str, intTime
|
||||
from anki.utils import ids2str, int_time
|
||||
|
||||
|
||||
class SchedulerBase(DeprecatedNamesMixin):
|
||||
|
@ -52,7 +52,7 @@ class SchedulerBase(DeprecatedNamesMixin):
|
|||
def deck_due_tree(self, top_deck_id: int = 0) -> DeckTreeNode:
|
||||
"""Returns a tree of decks with counts.
|
||||
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
|
||||
##########################################################################
|
||||
|
|
|
@ -14,7 +14,7 @@ from anki import hooks
|
|||
from anki.cards import Card
|
||||
from anki.consts import *
|
||||
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 Scheduler as V2
|
||||
|
@ -88,7 +88,7 @@ class Scheduler(V2):
|
|||
milliseconds_delta=+card.time_taken(),
|
||||
)
|
||||
|
||||
card.mod = intTime()
|
||||
card.mod = int_time()
|
||||
card.usn = self.col.usn()
|
||||
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}
|
||||
%s
|
||||
"""
|
||||
% (intTime(), self.col.usn(), extra)
|
||||
% (int_time(), self.col.usn(), extra)
|
||||
)
|
||||
# new cards in learning
|
||||
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 left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
|
||||
did,
|
||||
intTime() + self.col.conf["collapseTime"],
|
||||
int_time() + self.col.conf["collapseTime"],
|
||||
self.reportLimit,
|
||||
)
|
||||
or 0
|
||||
|
|
|
@ -17,7 +17,7 @@ from anki.consts import *
|
|||
from anki.decks import DeckConfigDict, DeckDict, DeckId
|
||||
from anki.lang import FormatTimeSpan
|
||||
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||
from anki.utils import ids2str, intTime
|
||||
from anki.utils import ids2str, int_time
|
||||
|
||||
CountsForDeckToday = scheduler_pb2.CountsForDeckTodayResponse
|
||||
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
|
||||
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:
|
||||
self._lrnCutoff = nextCutoff
|
||||
return True
|
||||
|
@ -320,7 +320,7 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
|
|||
return False
|
||||
if self._lrnQueue:
|
||||
return True
|
||||
cutoff = intTime() + self.col.conf["collapseTime"]
|
||||
cutoff = int_time() + self.col.conf["collapseTime"]
|
||||
self._lrnQueue = self.col.db.all( # type: ignore
|
||||
f"""
|
||||
select due, id from cards where
|
||||
|
@ -457,7 +457,7 @@ limit ?"""
|
|||
|
||||
self._answerCard(card, ease)
|
||||
|
||||
card.mod = intTime()
|
||||
card.mod = int_time()
|
||||
card.usn = self.col.usn()
|
||||
card.flush()
|
||||
|
||||
|
@ -615,7 +615,7 @@ limit ?"""
|
|||
fuzz = random.randrange(0, max(1, maxExtra))
|
||||
card.due = min(self.day_cutoff - 1, card.due + fuzz)
|
||||
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
|
||||
# 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
|
||||
|
@ -696,7 +696,7 @@ limit ?"""
|
|||
) -> int:
|
||||
"The number of steps that can be completed by the day cutoff."
|
||||
if not now:
|
||||
now = intTime()
|
||||
now = int_time()
|
||||
delays = delays[-left:]
|
||||
ok = 0
|
||||
for idx, delay in enumerate(delays):
|
||||
|
@ -777,7 +777,7 @@ limit ?"""
|
|||
if ease == BUTTON_ONE:
|
||||
# repeat after delay
|
||||
card.queue = QUEUE_TYPE_PREVIEW
|
||||
card.due = intTime() + self._previewDelay(card)
|
||||
card.due = int_time() + self._previewDelay(card)
|
||||
self.lrnCount += 1
|
||||
else:
|
||||
# BUTTON_TWO
|
||||
|
|
|
@ -22,7 +22,7 @@ from anki.decks import DeckId
|
|||
from anki.errors import DBError
|
||||
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||
from anki.types import assert_exhaustive
|
||||
from anki.utils import intTime
|
||||
from anki.utils import int_time
|
||||
|
||||
QueuedCards = scheduler_pb2.QueuedCards
|
||||
SchedulingState = scheduler_pb2.SchedulingState
|
||||
|
@ -77,7 +77,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
|||
current_state=states.current,
|
||||
new_state=new_state,
|
||||
rating=rating,
|
||||
answered_at_millis=intTime(1000),
|
||||
answered_at_millis=int_time(1000),
|
||||
milliseconds_taken=card.time_taken(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# 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.
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, no_type_check
|
||||
|
||||
import anki.collection
|
||||
import anki.models
|
||||
from anki import notetypes_pb2
|
||||
from anki._legacy import DeprecatedNamesMixinForModule
|
||||
from anki.utils import from_json_bytes
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
@ -38,14 +41,14 @@ def get_stock_notetypes(
|
|||
StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
|
||||
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(
|
||||
model: Any,
|
||||
) -> Callable[[anki.collection.Collection], anki.models.NotetypeDict]:
|
||||
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
|
||||
for (name_or_func, func) in models:
|
||||
if not isinstance(name_or_func, str):
|
||||
|
@ -61,33 +64,59 @@ def get_stock_notetypes(
|
|||
#
|
||||
|
||||
|
||||
def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
||||
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(
|
||||
def _legacy_add_basic_model(
|
||||
col: anki.collection.Collection,
|
||||
) -> anki.models.NotetypeDict:
|
||||
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED)
|
||||
col.models.add(nt)
|
||||
return nt
|
||||
note_type = _get_stock_notetype(col, StockNotetypeKind.BASIC)
|
||||
col.models.add(note_type)
|
||||
return note_type
|
||||
|
||||
|
||||
def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
|
||||
nt = _get_stock_notetype(col, StockNotetypeKind.CLOZE)
|
||||
col.models.add(nt)
|
||||
return nt
|
||||
def _legacy_add_basic_typing_model(
|
||||
col: anki.collection.Collection,
|
||||
) -> anki.models.NotetypeDict:
|
||||
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
|
||||
# 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.
|
||||
|
||||
from anki.collection import Collection
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from anki import sync_pb2
|
||||
|
||||
# public exports
|
||||
|
|
|
@ -171,7 +171,7 @@ def col_path() -> str:
|
|||
|
||||
|
||||
def serve() -> None:
|
||||
global col # pylint: disable=C0103
|
||||
global col # pylint: disable=invalid-name
|
||||
|
||||
col = Collection(col_path(), server=True)
|
||||
# don't hold an outer transaction open
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# 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
|
||||
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.collection
|
||||
from anki import tags_pb2
|
||||
from anki._legacy import DeprecatedNamesMixin, deprecated
|
||||
from anki.collection import OpChanges, OpChangesWithCount
|
||||
from anki.decks import DeckId
|
||||
from anki.notes import NoteId
|
||||
|
@ -28,7 +31,7 @@ TagTreeNode = tags_pb2.TagTreeNode
|
|||
MARKED_TAG = "marked"
|
||||
|
||||
|
||||
class TagManager:
|
||||
class TagManager(DeprecatedNamesMixin):
|
||||
def __init__(self, col: anki.collection.Collection) -> None:
|
||||
self.col = col.weakref()
|
||||
|
||||
|
@ -37,9 +40,9 @@ class TagManager:
|
|||
return list(self.col._backend.all_tags())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
d = dict(self.__dict__)
|
||||
del d["col"]
|
||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
||||
dict_ = dict(self.__dict__)
|
||||
del dict_["col"]
|
||||
return f"{super().__repr__()} {pprint.pformat(dict_, width=300)}"
|
||||
|
||||
def tree(self) -> TagTreeNode:
|
||||
return self.col._backend.tag_tree()
|
||||
|
@ -50,7 +53,7 @@ class TagManager:
|
|||
def clear_unused_tags(self) -> OpChangesWithCount:
|
||||
return self.col._backend.clear_unused_tags()
|
||||
|
||||
def byDeck(self, did: DeckId, children: bool = False) -> list[str]:
|
||||
def by_deck(self, did: DeckId, children: bool = False) -> list[str]:
|
||||
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
||||
if not children:
|
||||
query = f"{basequery} AND c.did=?"
|
||||
|
@ -133,48 +136,40 @@ class TagManager:
|
|||
return ""
|
||||
return f" {' '.join(tags)} "
|
||||
|
||||
def addToStr(self, addtags: 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:
|
||||
def rem_from_str(self, deltags: str, tags: str) -> str:
|
||||
"Delete tags if they exist."
|
||||
|
||||
def wildcard(pat: str, repl: str) -> Match:
|
||||
pat = re.escape(pat).replace("\\*", ".*")
|
||||
return re.match(f"^{pat}$", repl, re.IGNORECASE)
|
||||
|
||||
currentTags = self.split(tags)
|
||||
for tag in self.split(deltags):
|
||||
current_tags = self.split(tags)
|
||||
for del_tag in self.split(deltags):
|
||||
# find tags, ignoring case
|
||||
remove = []
|
||||
for tx in currentTags:
|
||||
if (tag.lower() == tx.lower()) or wildcard(tag, tx):
|
||||
remove.append(tx)
|
||||
for cur_tag in current_tags:
|
||||
if (del_tag.lower() == cur_tag.lower()) or wildcard(del_tag, cur_tag):
|
||||
remove.append(cur_tag)
|
||||
# remove them
|
||||
for r in remove:
|
||||
currentTags.remove(r)
|
||||
return self.join(currentTags)
|
||||
for rem in remove:
|
||||
current_tags.remove(rem)
|
||||
return self.join(current_tags)
|
||||
|
||||
# List-based utilities
|
||||
##########################################################################
|
||||
|
||||
# this is now a no-op - the tags are canonified when the note is saved
|
||||
def canonify(self, tagList: list[str]) -> list[str]:
|
||||
return tagList
|
||||
@deprecated(info="no-op - tags are now canonified when note is saved")
|
||||
def canonify(self, tag_list: list[str]) -> list[str]:
|
||||
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."
|
||||
return tag.lower() in [t.lower() for t in tags]
|
||||
|
||||
# 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()
|
||||
|
||||
def register(
|
||||
|
@ -182,12 +177,19 @@ class TagManager:
|
|||
) -> None:
|
||||
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."
|
||||
if add:
|
||||
self.bulk_add(ids, tags)
|
||||
else:
|
||||
self.bulk_remove(ids, tags)
|
||||
|
||||
def bulkRem(self, ids: list[NoteId], tags: str) -> None:
|
||||
self.bulkAdd(ids, tags, False)
|
||||
def _legacy_bulk_rem(self, ids: list[NoteId], tags: str) -> None:
|
||||
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
|
||||
# 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.
|
||||
|
||||
|
@ -215,10 +217,10 @@ class TemplateRenderContext:
|
|||
def render(self) -> TemplateRenderOutput:
|
||||
try:
|
||||
partial = self._partially_render()
|
||||
except TemplateError as e:
|
||||
except TemplateError as error:
|
||||
return TemplateRenderOutput(
|
||||
question_text=str(e),
|
||||
answer_text=str(e),
|
||||
question_text=str(error),
|
||||
answer_text=str(error),
|
||||
question_av_tags=[],
|
||||
answer_av_tags=[],
|
||||
)
|
||||
|
@ -284,12 +286,12 @@ class TemplateRenderOutput:
|
|||
def templates_for_card(card: Card, browser: bool) -> tuple[str, str]:
|
||||
template = card.template()
|
||||
if browser:
|
||||
q, a = template.get("bqfmt"), template.get("bafmt")
|
||||
question, answer = template.get("bqfmt"), template.get("bafmt")
|
||||
else:
|
||||
q, a = None, None
|
||||
q = q or template.get("qfmt")
|
||||
a = a or template.get("afmt")
|
||||
return q, a # type: ignore
|
||||
question, answer = None, None
|
||||
question = question or template.get("qfmt")
|
||||
answer = answer or template.get("afmt")
|
||||
return question, answer # type: ignore
|
||||
|
||||
|
||||
def apply_custom_filters(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json as _json
|
||||
|
@ -14,11 +16,11 @@ import subprocess
|
|||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from contextlib import contextmanager
|
||||
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
|
||||
|
||||
_tmpdir: str | None
|
||||
|
@ -35,19 +37,11 @@ except:
|
|||
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
|
||||
##############################################################################
|
||||
|
||||
|
||||
def intTime(scale: int = 1) -> int:
|
||||
def int_time(scale: int = 1) -> int:
|
||||
"The time in integer seconds. Pass scale=1000 to get milliseconds."
|
||||
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
|
||||
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"
|
||||
import anki.lang
|
||||
from anki.collection import StripHtmlMode
|
||||
|
||||
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:
|
||||
s = s.replace("<br>", " ")
|
||||
s = s.replace("<br />", " ")
|
||||
s = s.replace("<div>", " ")
|
||||
s = s.replace("\n", " ")
|
||||
s = re.sub(r"\[sound:[^]]+\]", "", s)
|
||||
s = re.sub(r"\[\[type:[^]]+\]\]", "", s)
|
||||
s = stripHTMLMedia(s)
|
||||
s = s.strip()
|
||||
return s
|
||||
def html_to_text_line(txt: str) -> str:
|
||||
txt = txt.replace("<br>", " ")
|
||||
txt = txt.replace("<br />", " ")
|
||||
txt = txt.replace("<div>", " ")
|
||||
txt = txt.replace("\n", " ")
|
||||
txt = re.sub(r"\[sound:[^]]+\]", "", txt)
|
||||
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
|
||||
txt = strip_html_media(txt)
|
||||
txt = txt.strip()
|
||||
return txt
|
||||
|
||||
|
||||
# IDs
|
||||
|
@ -94,19 +88,19 @@ def ids2str(ids: Iterable[int | str]) -> str:
|
|||
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."
|
||||
# be careful not to create multiple objects without flushing them, or they
|
||||
# may share an ID.
|
||||
t = intTime(1000)
|
||||
while db.scalar(f"select id from {table} where id = ?", t):
|
||||
t += 1
|
||||
return t
|
||||
timestamp = int_time(1000)
|
||||
while db.scalar(f"select id from {table} where id = ?", timestamp):
|
||||
timestamp += 1
|
||||
return timestamp
|
||||
|
||||
|
||||
def maxID(db: DBProxy) -> int:
|
||||
def max_id(db: DBProxy) -> int:
|
||||
"Return the first safe ID to use."
|
||||
now = intTime(1000)
|
||||
now = int_time(1000)
|
||||
for tbl in "cards", "notes":
|
||||
now = max(now, db.scalar(f"select max(id) from {tbl}") or 0)
|
||||
return now + 1
|
||||
|
@ -114,21 +108,20 @@ def maxID(db: DBProxy) -> int:
|
|||
|
||||
# used in ankiweb
|
||||
def base62(num: int, extra: str = "") -> str:
|
||||
s = string
|
||||
table = s.ascii_letters + s.digits + extra
|
||||
table = string.ascii_letters + string.digits + extra
|
||||
buf = ""
|
||||
while num:
|
||||
num, i = divmod(num, len(table))
|
||||
buf = table[i] + buf
|
||||
num, mod = divmod(num, len(table))
|
||||
buf = table[mod] + buf
|
||||
return buf
|
||||
|
||||
|
||||
_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
||||
_BASE91_EXTRA_CHARS = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
||||
|
||||
|
||||
def base91(num: int) -> str:
|
||||
# all printable characters minus quotes, backslash and separators
|
||||
return base62(num, _base91_extra_chars)
|
||||
return base62(num, _BASE91_EXTRA_CHARS)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def splitFields(string: str) -> list[str]:
|
||||
def split_fields(string: str) -> list[str]:
|
||||
return string.split("\x1f")
|
||||
|
||||
|
||||
|
@ -158,20 +151,20 @@ def checksum(data: bytes | str) -> str:
|
|||
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
|
||||
return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16)
|
||||
return int(checksum(strip_html_media(data).encode("utf-8"))[:8], 16)
|
||||
|
||||
|
||||
# Temp files
|
||||
##############################################################################
|
||||
|
||||
_tmpdir = None
|
||||
_tmpdir = None # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def tmpdir() -> str:
|
||||
"A reusable temp folder which we clean out on each program invocation."
|
||||
global _tmpdir
|
||||
global _tmpdir # pylint: disable=invalid-name
|
||||
if not _tmpdir:
|
||||
|
||||
def cleanup() -> None:
|
||||
|
@ -190,15 +183,15 @@ def tmpdir() -> str:
|
|||
|
||||
|
||||
def tmpfile(prefix: str = "", suffix: str = "") -> str:
|
||||
(fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
|
||||
os.close(fd)
|
||||
(descriptor, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix)
|
||||
os.close(descriptor)
|
||||
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."
|
||||
path = os.path.join(tmpdir(), name)
|
||||
if rm:
|
||||
if remove:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
|
@ -211,7 +204,7 @@ def namedtmp(name: str, rm: bool = True) -> str:
|
|||
|
||||
|
||||
@contextmanager
|
||||
def noBundledLibs() -> Iterator[None]:
|
||||
def no_bundled_libs() -> Iterator[None]:
|
||||
oldlpath = os.environ.pop("LD_LIBRARY_PATH", None)
|
||||
yield
|
||||
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."
|
||||
# ensure we don't open a separate window for forking process on windows
|
||||
if isWin:
|
||||
si = subprocess.STARTUPINFO() # type: ignore
|
||||
info = subprocess.STARTUPINFO() # type: ignore
|
||||
try:
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
except:
|
||||
# pylint: disable=no-member
|
||||
si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||
else:
|
||||
si = None
|
||||
info = None
|
||||
# run
|
||||
try:
|
||||
with noBundledLibs():
|
||||
o = subprocess.Popen(argv, startupinfo=si, **kwargs)
|
||||
with no_bundled_libs():
|
||||
process = subprocess.Popen(argv, startupinfo=info, **kwargs)
|
||||
except OSError:
|
||||
# command not found
|
||||
return -1
|
||||
|
@ -241,7 +234,7 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
|
|||
if wait:
|
||||
while 1:
|
||||
try:
|
||||
ret = o.wait()
|
||||
ret = process.wait()
|
||||
except OSError:
|
||||
# interrupted system call
|
||||
continue
|
||||
|
@ -259,13 +252,13 @@ isWin = sys.platform.startswith("win32")
|
|||
isLin = not isMac and not isWin
|
||||
devMode = os.getenv("ANKIDEV", "")
|
||||
|
||||
invalidFilenameChars = ':*?"<>|'
|
||||
INVALID_FILENAME_CHARS = ':*?"<>|'
|
||||
|
||||
|
||||
def invalidFilename(str: str, dirsep: bool = True) -> str | None:
|
||||
for c in invalidFilenameChars:
|
||||
if c in str:
|
||||
return c
|
||||
def invalid_filename(str: str, dirsep: bool = True) -> str | None:
|
||||
for char in INVALID_FILENAME_CHARS:
|
||||
if char in str:
|
||||
return char
|
||||
if (dirsep or isWin) and "/" in str:
|
||||
return "/"
|
||||
elif (dirsep or not isWin) and "\\" in str:
|
||||
|
@ -275,12 +268,10 @@ def invalidFilename(str: str, dirsep: bool = True) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def platDesc() -> str:
|
||||
def plat_desc() -> str:
|
||||
# we may get an interrupted system call, so try this in a loop
|
||||
n = 0
|
||||
theos = "unknown"
|
||||
while n < 100:
|
||||
n += 1
|
||||
for _ in range(100):
|
||||
try:
|
||||
system = platform.system()
|
||||
if isMac:
|
||||
|
@ -301,33 +292,33 @@ def platDesc() -> str:
|
|||
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
|
||||
##############################################################################
|
||||
|
||||
|
||||
def versionWithBuild() -> str:
|
||||
def version_with_build() -> str:
|
||||
from anki.buildinfo import buildhash, version
|
||||
|
||||
return f"{version} ({buildhash})"
|
||||
|
||||
|
||||
def pointVersion() -> int:
|
||||
def point_version() -> int:
|
||||
from anki.buildinfo import version
|
||||
|
||||
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
|
||||
[mypy-anki._backend.rsbridge]
|
||||
ignore_missing_imports = True
|
||||
[mypy-stringcase]
|
||||
ignore_missing_imports = True
|
||||
[mypy-anki._vendor.stringcase]
|
||||
disallow_untyped_defs = False
|
||||
|
|
|
@ -9,7 +9,7 @@ import tempfile
|
|||
from anki.collection import Collection as aopen
|
||||
from anki.dbproxy import emulate_named_args
|
||||
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 tests.shared import assertException, getEmptyCol
|
||||
|
||||
|
@ -123,7 +123,7 @@ def test_timestamps():
|
|||
col = getEmptyCol()
|
||||
assert len(col.models.all_names_and_ids()) == len(get_stock_notetypes(col))
|
||||
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))
|
||||
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ def test_find_cards():
|
|||
assert len(col.find_cards("-tag:sheep")) == 4
|
||||
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
|
||||
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:bar")) == 5
|
||||
# text searches
|
||||
|
|
|
@ -17,18 +17,20 @@ def test_add():
|
|||
with open(path, "w") as note:
|
||||
note.write("hello")
|
||||
# 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
|
||||
assert col.media.addFile(path) == "foo.jpg"
|
||||
assert col.media.add_file(path) == "foo.jpg"
|
||||
# but if it has a different sha1, it should
|
||||
with open(path, "w") as note:
|
||||
note.write("world")
|
||||
assert col.media.addFile(path) == "foo-7c211433f02071597741e6ff5a8ea34789abbf43.jpg"
|
||||
assert (
|
||||
col.media.add_file(path) == "foo-7c211433f02071597741e6ff5a8ea34789abbf43.jpg"
|
||||
)
|
||||
|
||||
|
||||
def test_strings():
|
||||
col = getEmptyCol()
|
||||
mf = col.media.filesInStr
|
||||
mf = col.media.files_in_str
|
||||
mid = col.models.current()["id"]
|
||||
assert mf(mid, "aoeu") == []
|
||||
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
||||
|
@ -61,7 +63,7 @@ def test_deckIntegration():
|
|||
col.media.dir()
|
||||
# put a file into it
|
||||
file = str(os.path.join(testDir, "support", "fake.png"))
|
||||
col.media.addFile(file)
|
||||
col.media.add_file(file)
|
||||
# add a note which references it
|
||||
note = col.newNote()
|
||||
note["Front"] = "one"
|
||||
|
|
|
@ -6,7 +6,7 @@ import time
|
|||
|
||||
from anki.consts import MODEL_CLOZE
|
||||
from anki.errors import NotFoundError
|
||||
from anki.utils import isWin, stripHTML
|
||||
from anki.utils import isWin, strip_html
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
|
@ -114,7 +114,7 @@ def test_templates():
|
|||
# and should have updated the other cards' ordinals
|
||||
c = note.cards()[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
|
||||
t = mm.new_template("template name")
|
||||
t["qfmt"] = "{{Front}}2"
|
||||
|
|
|
@ -7,7 +7,7 @@ import time
|
|||
from anki.collection import Collection
|
||||
from anki.consts import *
|
||||
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
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ def getEmptyCol() -> Collection:
|
|||
|
||||
def test_clock():
|
||||
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.")
|
||||
|
||||
|
||||
|
@ -53,7 +53,7 @@ def test_new():
|
|||
assert c.queue == QUEUE_TYPE_NEW
|
||||
assert c.type == CARD_TYPE_NEW
|
||||
# if we answer it, it should become a learn card
|
||||
t = intTime()
|
||||
t = int_time()
|
||||
col.sched.answerCard(c, 1)
|
||||
assert c.queue == QUEUE_TYPE_LRN
|
||||
assert c.type == CARD_TYPE_LRN
|
||||
|
|
|
@ -12,7 +12,7 @@ from anki import hooks
|
|||
from anki.consts import *
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.scheduler import UnburyDeck
|
||||
from anki.utils import intTime
|
||||
from anki.utils import int_time
|
||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||
|
||||
|
||||
|
@ -33,7 +33,7 @@ def getEmptyCol():
|
|||
|
||||
def test_clock():
|
||||
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.")
|
||||
|
||||
|
||||
|
@ -65,7 +65,7 @@ def test_new():
|
|||
assert c.queue == QUEUE_TYPE_NEW
|
||||
assert c.type == CARD_TYPE_NEW
|
||||
# if we answer it, it should become a learn card
|
||||
t = intTime()
|
||||
t = int_time()
|
||||
col.sched.answerCard(c, 1)
|
||||
assert c.queue == QUEUE_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
|
||||
col.sched.answerCard(c, 3)
|
||||
# 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
|
||||
col.sched.empty_filtered_deck(did)
|
||||
c.load()
|
||||
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
|
||||
assert c.left % 1000 == 1
|
||||
assert c.due - intTime() > 60 * 60
|
||||
assert c.due - int_time() > 60 * 60
|
||||
|
||||
|
||||
def test_preview():
|
||||
|
@ -1034,7 +1034,7 @@ def test_timing():
|
|||
c2 = col.sched.getCard()
|
||||
assert c2.queue == QUEUE_TYPE_REV
|
||||
# if the failed card becomes due, it should show first
|
||||
c.due = intTime() - 1
|
||||
c.due = int_time() - 1
|
||||
c.flush()
|
||||
col.reset()
|
||||
c = col.sched.getCard()
|
||||
|
@ -1061,7 +1061,7 @@ def test_collapse():
|
|||
col.sched.answerCard(c2, 1)
|
||||
# first should become available again, despite it being due in the future
|
||||
c3 = col.sched.getCard()
|
||||
assert c3.due > intTime()
|
||||
assert c3.due > int_time()
|
||||
col.sched.answerCard(c3, 4)
|
||||
# answer other
|
||||
c4 = col.sched.getCard()
|
||||
|
|
|
@ -6,7 +6,7 @@ import time
|
|||
|
||||
import aqt.forms
|
||||
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.qt import *
|
||||
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 += 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_version(val=versionWithBuild())}<br>"
|
||||
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
|
||||
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
|
||||
platform.python_version(),
|
||||
QT_VERSION_STR,
|
||||
|
|
|
@ -10,7 +10,7 @@ from anki.collection import OpChanges, SearchNode
|
|||
from anki.decks import DeckId
|
||||
from anki.models import NotetypeId
|
||||
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.deckchooser import DeckChooser
|
||||
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))):
|
||||
note = self.col.get_note(nid)
|
||||
fields = note.fields
|
||||
txt = htmlToTextLine(", ".join(fields))
|
||||
txt = html_to_text_line(", ".join(fields))
|
||||
if len(txt) > 30:
|
||||
txt = f"{txt[:30]}..."
|
||||
line = tr.adding_edit(val=txt)
|
||||
|
|
|
@ -95,7 +95,7 @@ class UpdateInfo:
|
|||
|
||||
ANKIWEB_ID_RE = re.compile(r"^\d+$")
|
||||
|
||||
current_point_version = anki.utils.pointVersion()
|
||||
current_point_version = anki.utils.point_version()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -983,7 +983,7 @@ def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
|
|||
if resp.status_code != 200:
|
||||
return DownloadError(status_code=resp.status_code)
|
||||
|
||||
data = client.streamContent(resp)
|
||||
data = client.stream_content(resp)
|
||||
|
||||
fname = re.match(
|
||||
"attachment; filename=(.+)", resp.headers["content-disposition"]
|
||||
|
|
|
@ -484,7 +484,7 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.then((toolbar) => toolba
|
|||
json.dumps(focusTo),
|
||||
json.dumps(self.note.id),
|
||||
json.dumps([text_color, highlight_color]),
|
||||
json.dumps(self.mw.col.tags.canonify(self.note.tags)),
|
||||
json.dumps(self.note.tags),
|
||||
)
|
||||
|
||||
if self.addMode:
|
||||
|
@ -674,12 +674,12 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.then((toolbar) => toolba
|
|||
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
||||
"""Add to media folder and return local img or sound tag."""
|
||||
# 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 self.fnameToLink(fname)
|
||||
|
||||
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:
|
||||
aqt.sound.record_audio(
|
||||
|
|
|
@ -31,7 +31,7 @@ from anki.decks import DeckDict, DeckId
|
|||
from anki.hooks import runHook
|
||||
from anki.notes import NoteId
|
||||
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.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
|
||||
from aqt.dbcheck import check_db
|
||||
|
@ -633,11 +633,11 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
def maybeOptimize(self) -> None:
|
||||
# have two weeks passed?
|
||||
if (intTime() - self.pm.profile["lastOptimize"]) < 86400 * 14:
|
||||
if (int_time() - self.pm.profile["lastOptimize"]) < 86400 * 14:
|
||||
return
|
||||
self.progress.start(label=tr.qt_misc_optimizing())
|
||||
self.col.optimize()
|
||||
self.pm.profile["lastOptimize"] = intTime()
|
||||
self.pm.profile["lastOptimize"] = int_time()
|
||||
self.pm.save()
|
||||
self.progress.finish()
|
||||
|
||||
|
@ -877,7 +877,7 @@ title="{}" {}>{}</button>""".format(
|
|||
|
||||
def maybe_check_for_addon_updates(self) -> None:
|
||||
last_check = self.pm.last_addon_update_check()
|
||||
elap = intTime() - last_check
|
||||
elap = int_time() - last_check
|
||||
|
||||
if elap > 86_400:
|
||||
check_and_prompt_for_updates(
|
||||
|
@ -886,7 +886,7 @@ title="{}" {}>{}</button>""".format(
|
|||
self.on_updates_installed,
|
||||
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:
|
||||
if log:
|
||||
|
@ -1321,7 +1321,7 @@ title="{}" {}>{}</button>""".format(
|
|||
for id, mid, flds in col.db.execute(
|
||||
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(b"\n")
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import aqt
|
|||
from anki.collection import Progress
|
||||
from anki.errors import Interrupted, NetworkError
|
||||
from anki.types import assert_exhaustive
|
||||
from anki.utils import intTime
|
||||
from anki.utils import int_time
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import QDialog, QDialogButtonBox, QPushButton, QTextCursor, QTimer, qconnect
|
||||
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)
|
||||
|
||||
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.mw.taskman.run_on_main(
|
||||
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
||||
|
@ -142,7 +142,7 @@ class MediaSyncer:
|
|||
last = self._log[-1].time
|
||||
else:
|
||||
last = 0
|
||||
return intTime() - last
|
||||
return int_time() - last
|
||||
|
||||
|
||||
class MediaSyncDialog(QDialog):
|
||||
|
|
|
@ -244,7 +244,7 @@ class Preferences(QDialog):
|
|||
|
||||
def current_lang_index(self) -> int:
|
||||
codes = [x[1] for x in anki.lang.langs]
|
||||
lang = anki.lang.currentLang
|
||||
lang = anki.lang.current_lang
|
||||
if lang in anki.lang.compatMap:
|
||||
lang = anki.lang.compatMap[lang]
|
||||
else:
|
||||
|
|
|
@ -20,7 +20,7 @@ from anki.collection import Collection
|
|||
from anki.db import DB
|
||||
from anki.lang import without_unicode_isolation
|
||||
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.qt import *
|
||||
from aqt.utils import disable_help_button, showWarning, tr
|
||||
|
@ -68,7 +68,7 @@ class VideoDriver(Enum):
|
|||
metaConf = dict(
|
||||
ver=0,
|
||||
updates=True,
|
||||
created=intTime(),
|
||||
created=int_time(),
|
||||
id=random.randrange(0, 2 ** 63),
|
||||
lastMsg=-1,
|
||||
suppressUpdate=False,
|
||||
|
@ -81,7 +81,7 @@ profileConf: dict[str, Any] = dict(
|
|||
mainWindowGeom=None,
|
||||
mainWindowState=None,
|
||||
numBackups=50,
|
||||
lastOptimize=intTime(),
|
||||
lastOptimize=int_time(),
|
||||
# editing
|
||||
searchHistory=[],
|
||||
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 Scheduler as V3Scheduler
|
||||
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.browser.card_info import PreviousReviewerCardInfo, ReviewerCardInfo
|
||||
from aqt.deckoptions import confirm_deck_then_display_options
|
||||
|
@ -573,7 +573,7 @@ class Reviewer:
|
|||
# munge correct value
|
||||
cor = self.mw.col.media.strip(self.typeCorrect)
|
||||
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
||||
cor = stripHTML(cor)
|
||||
cor = strip_html(cor)
|
||||
# ensure we don't chomp multiple whitespace
|
||||
cor = cor.replace(" ", " ")
|
||||
cor = html.unescape(cor)
|
||||
|
|
|
@ -12,7 +12,7 @@ import aqt
|
|||
from anki.errors import Interrupted, SyncError, SyncErrorKind
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.sync import SyncOutput, SyncStatus
|
||||
from anki.utils import platDesc
|
||||
from anki.utils import plat_desc
|
||||
from aqt.qt import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
|
@ -331,4 +331,4 @@ def get_id_and_pass_from_user(
|
|||
|
||||
|
||||
# export platform version to syncing code
|
||||
os.environ["PLATFORM"] = platDesc()
|
||||
os.environ["PLATFORM"] = plat_desc()
|
||||
|
|
|
@ -39,7 +39,7 @@ class TagLimit(QDialog):
|
|||
self.exec()
|
||||
|
||||
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", [])
|
||||
no = self.deck.get("inactiveTags", [])
|
||||
yesHash = {}
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Any
|
|||
import requests
|
||||
|
||||
import aqt
|
||||
from anki.utils import platDesc, versionWithBuild
|
||||
from anki.utils import plat_desc, version_with_build
|
||||
from aqt.main import AnkiQt
|
||||
from aqt.qt import *
|
||||
from aqt.utils import openLink, showText, tr
|
||||
|
@ -26,8 +26,8 @@ class LatestVersionFinder(QThread):
|
|||
|
||||
def _data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"ver": versionWithBuild(),
|
||||
"os": platDesc(),
|
||||
"ver": version_with_build(),
|
||||
"os": plat_desc(),
|
||||
"id": self.config["id"],
|
||||
"lm": self.config["lastMsg"],
|
||||
"crt": self.config["created"],
|
||||
|
|
|
@ -12,7 +12,13 @@ from typing import TYPE_CHECKING, Any, Literal, Sequence
|
|||
import aqt
|
||||
from anki.collection import Collection, HelpPage
|
||||
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.theme import theme_manager
|
||||
|
||||
|
@ -57,7 +63,7 @@ def openHelp(section: HelpPageArgument) -> None:
|
|||
|
||||
def openLink(link: str | QUrl) -> None:
|
||||
tooltip(tr.qt_misc_loading(), period=1000)
|
||||
with noBundledLibs():
|
||||
with no_bundled_libs():
|
||||
QDesktopServices.openUrl(QUrl(link))
|
||||
|
||||
|
||||
|
@ -658,7 +664,7 @@ def openFolder(path: str) -> None:
|
|||
if isWin:
|
||||
subprocess.run(["explorer", f"file://{path}"], check=False)
|
||||
else:
|
||||
with noBundledLibs():
|
||||
with no_bundled_libs():
|
||||
QDesktopServices.openUrl(QUrl(f"file://{path}"))
|
||||
|
||||
|
||||
|
@ -762,7 +768,7 @@ def closeTooltip() -> None:
|
|||
|
||||
# true if invalid; print warning
|
||||
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
||||
bad = invalidFilename(str, dirsep)
|
||||
bad = invalid_filename(str, dirsep)
|
||||
if bad:
|
||||
showWarning(tr.qt_misc_the_following_character_can_not_be(val=bad))
|
||||
return True
|
||||
|
@ -874,7 +880,7 @@ Platform: {}
|
|||
Flags: frz={} ao={} sv={}
|
||||
Add-ons, last update check: {}
|
||||
""".format(
|
||||
versionWithBuild(),
|
||||
version_with_build(),
|
||||
platform.python_version(),
|
||||
QT_VERSION_STR,
|
||||
PYQT_VERSION_STR,
|
||||
|
|
|
@ -439,7 +439,7 @@ div[contenteditable="true"]:focus {{
|
|||
window_bg_night = self.get_window_bg_color(True).name()
|
||||
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"
|
||||
else:
|
||||
lang_dir = "ltr"
|
||||
|
|
Loading…
Reference in a new issue