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:
RumovZ 2021-10-25 06:50:13 +02:00 committed by GitHub
parent 1fab547d46
commit 9dc3cf216a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 502 additions and 377 deletions

View file

@ -57,4 +57,4 @@ good-names =
tr,
db,
ok,
ip,
ip,

View file

@ -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"',

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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}"

View file

@ -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
######################################################################

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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>"

View file

@ -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),
)

View file

@ -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)

View file

@ -6,6 +6,7 @@
# future.
#
# pylint: disable=unused-import
# pylint: enable=invalid-name
from anki.decks import DeckTreeNode
from anki.errors import InvalidInput, NotFoundError

View file

@ -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
##########################################################################

View file

@ -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

View file

@ -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

View file

@ -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(),
)

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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),
)

View file

@ -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(

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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)

View file

@ -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"]

View file

@ -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(

View file

@ -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")

View file

@ -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):

View file

@ -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:

View file

@ -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",

View file

@ -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(" ", "&nbsp;")
cor = html.unescape(cor)

View file

@ -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()

View file

@ -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 = {}

View file

@ -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"],

View file

@ -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,

View file

@ -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"