diff --git a/pylib/.pylintrc b/pylib/.pylintrc index 528fabf59..06663f865 100644 --- a/pylib/.pylintrc +++ b/pylib/.pylintrc @@ -57,4 +57,4 @@ good-names = tr, db, ok, - ip, \ No newline at end of file + ip, diff --git a/pylib/anki/BUILD.bazel b/pylib/anki/BUILD.bazel index 4ca46641e..90405ec9e 100644 --- a/pylib/anki/BUILD.bazel +++ b/pylib/anki/BUILD.bazel @@ -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"', diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index 433463f76..2f29831bb 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -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, diff --git a/pylib/anki/_legacy.py b/pylib/anki/_legacy.py index cd9ed1330..80d481875 100644 --- a/pylib/anki/_legacy.py +++ b/pylib/anki/_legacy.py @@ -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 diff --git a/pylib/anki/_vendor/stringcase.py b/pylib/anki/_vendor/stringcase.py index 517e43dbc..01007129c 100644 --- a/pylib/anki/_vendor/stringcase.py +++ b/pylib/anki/_vendor/stringcase.py @@ -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 diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 9c838c511..4df8328bf 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -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, ) diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 9d0b25b89..c5aa908ce 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -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) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 724de45fb..3217f1f39 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -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) diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index 1514916d4..725dceb0c 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -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 diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index cf2fc587a..4b7ecb0c8 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -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 diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py index 6643deb4d..ebcc5b97c 100644 --- a/pylib/anki/httpclient.py +++ b/pylib/anki/httpclient.py @@ -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}" diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 8de564508..dd9ad5052 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -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 ###################################################################### diff --git a/pylib/anki/importing/base.py b/pylib/anki/importing/base.py index 7a4ea1aff..6d53d0d24 100644 --- a/pylib/anki/importing/base.py +++ b/pylib/anki/importing/base.py @@ -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 diff --git a/pylib/anki/importing/mnemo.py b/pylib/anki/importing/mnemo.py index fb1305da7..c958a2059 100644 --- a/pylib/anki/importing/mnemo.py +++ b/pylib/anki/importing/mnemo.py @@ -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) diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index bc07b423c..7792de96e 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -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): diff --git a/pylib/anki/importing/pauker.py b/pylib/anki/importing/pauker.py index 01dd190bd..99a1bfbfb 100644 --- a/pylib/anki/importing/pauker.py +++ b/pylib/anki/importing/pauker.py @@ -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) diff --git a/pylib/anki/importing/supermemo_xml.py b/pylib/anki/importing/supermemo_xml.py index b3f1ad539..4064f8aa5 100644 --- a/pylib/anki/importing/supermemo_xml.py +++ b/pylib/anki/importing/supermemo_xml.py @@ -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() diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index d159eaf44..827dc40c4 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -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) diff --git a/pylib/anki/latex.py b/pylib/anki/latex.py index 98dc90bcd..a73003b6a 100644 --- a/pylib/anki/latex.py +++ b/pylib/anki/latex.py @@ -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)}
" msg += f"{col.tr.media_generated_file(val=texpath)}
" 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"
{html.escape(log)}
" diff --git a/pylib/anki/media.py b/pylib/anki/media.py index a491f2436..15c03c070 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -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[^]]+)\])"] 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), +) diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 04cb0637f..510a7df59 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -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) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index fcec17d02..297c1b6d2 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -6,6 +6,7 @@ # future. # # pylint: disable=unused-import +# pylint: enable=invalid-name from anki.decks import DeckTreeNode from anki.errors import InvalidInput, NotFoundError diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 0c5050b53..f9e4b2cb7 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -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 ########################################################################## diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 98bc6e0c5..426f3006b 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -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 diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py index 8088f53ca..aa330c5ab 100644 --- a/pylib/anki/scheduler/v2.py +++ b/pylib/anki/scheduler/v2.py @@ -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 diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 2850a2b84..d28d6eb9f 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -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(), ) diff --git a/pylib/anki/sound.py b/pylib/anki/sound.py index 3d375f716..a21e8207c 100644 --- a/pylib/anki/sound.py +++ b/pylib/anki/sound.py @@ -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. diff --git a/pylib/anki/stdmodels.py b/pylib/anki/stdmodels.py index ff93f5bb4..83d79ce80 100644 --- a/pylib/anki/stdmodels.py +++ b/pylib/anki/stdmodels.py @@ -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) diff --git a/pylib/anki/storage.py b/pylib/anki/storage.py index bf71689cd..ad2a1b156 100644 --- a/pylib/anki/storage.py +++ b/pylib/anki/storage.py @@ -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 diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index e52611d30..7d9218832 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -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 diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py index 3428bf1d7..0d21cb87a 100644 --- a/pylib/anki/syncserver/__init__.py +++ b/pylib/anki/syncserver/__init__.py @@ -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 diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 425987ebc..cf3d9569f 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -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), +) diff --git a/pylib/anki/template.py b/pylib/anki/template.py index b4d4fb206..d1654485c 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -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( diff --git a/pylib/anki/types.py b/pylib/anki/types.py index d6f1406a2..6ff8f1bdc 100644 --- a/pylib/anki/types.py +++ b/pylib/anki/types.py @@ -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 diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 61995ffec..dd2e9a383 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -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("
", " ") - s = s.replace("
", " ") - s = s.replace("
", " ") - 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("
", " ") + txt = txt.replace("
", " ") + txt = txt.replace("
", " ") + 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) diff --git a/pylib/mypy.ini b/pylib/mypy.ini index 756045bdc..b3103e76c 100644 --- a/pylib/mypy.ini +++ b/pylib/mypy.ini @@ -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 diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index 6588bc592..921e1ecae 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -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)) diff --git a/pylib/tests/test_find.py b/pylib/tests/test_find.py index 0a26620b9..5c45c05fb 100644 --- a/pylib/tests/test_find.py +++ b/pylib/tests/test_find.py @@ -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 diff --git a/pylib/tests/test_media.py b/pylib/tests/test_media.py index 30e2b8056..546e13358 100644 --- a/pylib/tests/test_media.py +++ b/pylib/tests/test_media.py @@ -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, "aoeuao") == ["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" diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index dbad433b5..3449e549e 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -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" diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index d0afd3e7f..690cc0fd2 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -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 diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 53a8d02f9..9f0ee0d23 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -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() diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 3f5d47164..33168940e 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -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 = "
" abouttext += f"

{tr.about_anki_is_a_friendly_intelligent_spaced()}" abouttext += f"

{tr.about_anki_is_licensed_under_the_agpl3()}" - abouttext += f"

{tr.about_version(val=versionWithBuild())}
" + abouttext += f"

{tr.about_version(val=version_with_build())}
" abouttext += ("Python %s Qt %s PyQt %s
") % ( platform.python_version(), QT_VERSION_STR, diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 8057cf586..9f80d1d73 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -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) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index c8c69b6de..91d019db1 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -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"] diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index bea54332e..31fe1be98 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -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( diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 1249e764f..2e3c08843 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -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="{}" {}>{}""".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="{}" {}>{}""".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="{}" {}>{}""".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") diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 1c5a366ff..5961ceecf 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -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): diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index ba48cf7b8..134507865 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -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: diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 59ba16572..5ee1533b4 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -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", diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 192ed11b2..e75cd5cf8 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -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|
|)+", " ", cor) - cor = stripHTML(cor) + cor = strip_html(cor) # ensure we don't chomp multiple whitespace cor = cor.replace(" ", " ") cor = html.unescape(cor) diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 2da20d933..3dcd3a9c2 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -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() diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index 0a04ef3ad..a9db514a1 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -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 = {} diff --git a/qt/aqt/update.py b/qt/aqt/update.py index 4b5a00fbd..fbcd5fcbd 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -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"], diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 28f1242dc..ed5699059 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -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, diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 72c0cb976..21e1e79df 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -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"