From f0f2da0f56bdaaa80169113c83473a83bbd2f41c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 31 Jan 2021 20:54:43 +1000 Subject: [PATCH 01/22] doc tweaks --- docs/development.md | 6 +++--- docs/mac.md | 5 +++-- docs/windows.md | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/development.md b/docs/development.md index 5ddb825f0..25103a518 100644 --- a/docs/development.md +++ b/docs/development.md @@ -15,9 +15,9 @@ Pre-built Python packages are available on PyPI. They are useful if you wish to: - Get code completion when developing add-ons - Make command line scripts that modify .anki2 files via Anki's Python libraries -You will need Python 3.8 or 3.9 installed. If you do not have Python yet, please -see the platform-specific instructions in the "Building from source" section below -for more info. +You will need the 64 bit version of Python 3.8 or 3.9 installed. If you do not +have Python yet, please see the platform-specific instructions in the "Building +from source" section below for more info. **Mac/Linux**: diff --git a/docs/mac.md b/docs/mac.md index 2a8c84cfb..6551eb1cf 100644 --- a/docs/mac.md +++ b/docs/mac.md @@ -19,8 +19,9 @@ $ brew install rsync bazelisk **Install Python 3.8**: -Install Python 3.8 from . You may be able to use -the Homebrew version instead, but this is untested. +Install Python 3.8 from . We have heard reports +of issues with pyenv and homebrew, so the package from python.org is +the only recommended approach. Python 3.9 is not currently recommended, as pylint does not support it yet. diff --git a/docs/windows.md b/docs/windows.md index 6502475d4..afa9d0521 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -17,8 +17,8 @@ components enabled on the right. **Python 3.8**: -Download Python 3.8 from . Run the installer, and -customize the installation. Select "install for all users", and choose +Download the 64 bit Python 3.8 from . Run the installer, +and customize the installation. Select "install for all users", and choose the install path as c:\python. Currently the build scripts require Python to be installed in that location. From 7fda601aef4a286650449cadc4633364283db82b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 31 Jan 2021 20:56:21 +1000 Subject: [PATCH 02/22] add some typehints, and remove some unused code --- pylib/anki/_backend/rsbridge.pyi | 11 +++---- pylib/anki/consts.py | 2 +- pylib/anki/db.py | 14 ++++----- pylib/anki/dbproxy.py | 23 +++++++++----- pylib/anki/errors.py | 16 ++++------ pylib/anki/httpclient.py | 33 +++++++------------ pylib/anki/lang.py | 4 +-- pylib/anki/tags.py | 12 +++---- pylib/anki/utils.py | 54 ++++---------------------------- 9 files changed, 58 insertions(+), 111 deletions(-) diff --git a/pylib/anki/_backend/rsbridge.pyi b/pylib/anki/_backend/rsbridge.pyi index 57068a376..126678bc9 100644 --- a/pylib/anki/_backend/rsbridge.pyi +++ b/pylib/anki/_backend/rsbridge.pyi @@ -1,10 +1,7 @@ -from typing import Any - -def buildhash(*args, **kwargs) -> Any: ... -def open_backend(*args, **kwargs) -> Any: ... +def buildhash() -> str: ... +def open_backend(data: bytes) -> Backend: ... class Backend: @classmethod - def __init__(self, *args, **kwargs) -> None: ... - def command(self, *args, **kwargs) -> Any: ... - def db_command(self, *args, **kwargs) -> Any: ... + def command(self, method: int, data: bytes) -> bytes: ... + def db_command(self, data: bytes) -> bytes: ... diff --git a/pylib/anki/consts.py b/pylib/anki/consts.py index 18c134ea2..1e472bdf4 100644 --- a/pylib/anki/consts.py +++ b/pylib/anki/consts.py @@ -92,7 +92,7 @@ REVLOG_RESCHED = 4 ########################################################################## -def _tr(col: Optional[anki.collection.Collection]): +def _tr(col: Optional[anki.collection.Collection]) -> Any: if col: return col.tr else: diff --git a/pylib/anki/db.py b/pylib/anki/db.py index 348a52d7f..eebc5be67 100644 --- a/pylib/anki/db.py +++ b/pylib/anki/db.py @@ -32,7 +32,7 @@ class DB: del d["_db"] return f"{super().__repr__()} {pprint.pformat(d, width=300)}" - def execute(self, sql: str, *a, **ka) -> Cursor: + def execute(self, sql: str, *a: Any, **ka: Any) -> Cursor: s = sql.strip().lower() # mark modified? for stmt in "insert", "update", "delete": @@ -76,36 +76,36 @@ class DB: def rollback(self) -> None: self._db.rollback() - def scalar(self, *a, **kw) -> Any: + def scalar(self, *a: Any, **kw: Any) -> Any: res = self.execute(*a, **kw).fetchone() if res: return res[0] return None - def all(self, *a, **kw) -> List: + def all(self, *a: Any, **kw: Any) -> List: return self.execute(*a, **kw).fetchall() - def first(self, *a, **kw) -> Any: + def first(self, *a: Any, **kw: Any) -> Any: c = self.execute(*a, **kw) res = c.fetchone() c.close() return res - def list(self, *a, **kw) -> List: + def list(self, *a: Any, **kw: Any) -> List: return [x[0] for x in self.execute(*a, **kw)] def close(self) -> None: self._db.text_factory = None self._db.close() - def set_progress_handler(self, *args) -> None: + def set_progress_handler(self, *args: Any) -> None: self._db.set_progress_handler(*args) def __enter__(self) -> "DB": self._db.execute("begin") return self - def __exit__(self, exc_type, *args) -> None: + def __exit__(self, *args: Any) -> None: self._db.close() def totalChanges(self) -> Any: diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index 7ec126e04..1bb4f9d95 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -4,6 +4,7 @@ from __future__ import annotations import re +from re import Match from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union import anki @@ -43,7 +44,11 @@ class DBProxy: ################ def _query( - self, sql: str, *args: ValueForDB, first_row_only: bool = False, **kwargs + self, + sql: str, + *args: ValueForDB, + first_row_only: bool = False, + **kwargs: ValueForDB, ) -> List[Row]: # mark modified? s = sql.strip().lower() @@ -57,20 +62,22 @@ class DBProxy: # Query shortcuts ################### - def all(self, sql: str, *args: ValueForDB, **kwargs) -> List[Row]: - return self._query(sql, *args, **kwargs) + def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]: + return self._query(sql, *args, first_row_only=False, **kwargs) - def list(self, sql: str, *args: ValueForDB, **kwargs) -> List[ValueFromDB]: - return [x[0] for x in self._query(sql, *args, **kwargs)] + def list( + self, sql: str, *args: ValueForDB, **kwargs: ValueForDB + ) -> List[ValueFromDB]: + return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)] - def first(self, sql: str, *args: ValueForDB, **kwargs) -> Optional[Row]: + def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]: rows = self._query(sql, *args, first_row_only=True, **kwargs) if rows: return rows[0] else: return None - def scalar(self, sql: str, *args: ValueForDB, **kwargs) -> ValueFromDB: + def scalar(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> ValueFromDB: rows = self._query(sql, *args, first_row_only=True, **kwargs) if rows: return rows[0][0] @@ -109,7 +116,7 @@ def emulate_named_args( n = len(args2) arg_num[key] = n # update refs - def repl(m): + def repl(m: Match) -> str: arg = m.group(1) return f"?{arg_num[arg]}" diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index bed39fab0..955d6009f 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -3,8 +3,6 @@ from __future__ import annotations -from typing import Any - import anki._backend.backend_pb2 as _pb # fixme: notfounderror etc need to be in rsbackend.py @@ -88,17 +86,15 @@ def backend_exception_to_pylib(err: _pb.BackendError) -> Exception: return StringError(err.localized) +# FIXME: this is only used with "abortSchemaMod", but currently some +# add-ons depend on it class AnkiError(Exception): - def __init__(self, type, **data) -> None: + def __init__(self, type: str) -> None: super().__init__() self.type = type - self.data = data - def __str__(self) -> Any: - m = self.type - if self.data: - m += ": %s" % repr(self.data) - return m + def __str__(self) -> str: + return self.type class DeckRenameError(Exception): @@ -106,5 +102,5 @@ class DeckRenameError(Exception): super().__init__() self.description = description - def __str__(self): + def __str__(self) -> str: return "Couldn't rename deck: " + self.description diff --git a/pylib/anki/httpclient.py b/pylib/anki/httpclient.py index 55842918a..b169fc2d2 100644 --- a/pylib/anki/httpclient.py +++ b/pylib/anki/httpclient.py @@ -5,6 +5,8 @@ Wrapper for requests that adds a callback for tracking upload/download progress. """ +from __future__ import annotations + import io import os from typing import Any, Callable, Dict, Optional @@ -28,24 +30,23 @@ class HttpClient: self.progress_hook = progress_hook self.session = requests.Session() - def __enter__(self): + def __enter__(self) -> HttpClient: return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: self.close() - def close(self): + def close(self) -> None: if self.session: self.session.close() self.session = None - def __del__(self): + def __del__(self) -> None: self.close() - def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response: - data = _MonitoringFile( - data, hook=self.progress_hook - ) # pytype: disable=wrong-arg-types + def post( + self, url: str, data: bytes, headers: Optional[Dict[str, str]] + ) -> Response: headers["User-Agent"] = self._agentName() return self.session.post( url, @@ -56,7 +57,7 @@ class HttpClient: verify=self.verify, ) # pytype: disable=wrong-arg-types - def get(self, url, headers=None) -> Response: + def get(self, url: str, headers: Dict[str, str] = None) -> Response: if headers is None: headers = {} headers["User-Agent"] = self._agentName() @@ -64,7 +65,7 @@ class HttpClient: url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify ) - def streamContent(self, resp) -> bytes: + def streamContent(self, resp: Response) -> bytes: resp.raise_for_status() buf = io.BytesIO() @@ -87,15 +88,3 @@ if os.environ.get("ANKI_NOVERIFYSSL"): import warnings warnings.filterwarnings("ignore") - - -class _MonitoringFile(io.BufferedReader): - def __init__(self, raw: io.RawIOBase, hook: Optional[ProgressCallback]): - io.BufferedReader.__init__(self, raw) - self.hook = hook - - def read(self, size=-1) -> bytes: - data = io.BufferedReader.read(self, HTTP_BUF_SIZE) - if self.hook: - self.hook(len(data), 0) - return data diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index 2cc2eb8ed..6be31bbfd 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -5,7 +5,7 @@ from __future__ import annotations import locale import re -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Any, Optional, Tuple import anki import anki._backend.backend_pb2 as _pb @@ -169,7 +169,7 @@ def ngettext(single: str, plural: str, n: int) -> str: return plural -def tr_legacyglobal(*args, **kwargs) -> str: +def tr_legacyglobal(*args: Any, **kwargs: Any) -> str: "Should use col.tr() instead." if current_i18n: return current_i18n.translate(*args, **kwargs) diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 2699cfdab..e64e9d434 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -48,7 +48,7 @@ class TagManager: ############################################################# def register( - self, tags: Collection[str], usn: Optional[int] = None, clear=False + self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False ) -> None: print("tags.register() is deprecated and no longer works") @@ -56,10 +56,10 @@ class TagManager: "Clear unused tags and add any missing tags from notes to the tag list." self.clear_unused_tags() - def clear_unused_tags(self): + def clear_unused_tags(self) -> None: self.col._backend.clear_unused_tags() - def byDeck(self, did, children=False) -> List[str]: + def byDeck(self, did: int, children: bool = False) -> List[str]: basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" if not children: query = basequery + " AND c.did=?" @@ -72,7 +72,7 @@ class TagManager: res = self.col.db.list(query) return list(set(self.split(" ".join(res)))) - def set_collapsed(self, tag: str, collapsed: bool): + def set_collapsed(self, tag: str, collapsed: bool) -> None: "Set browser collapse state for tag, registering the tag if missing." self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed) @@ -139,9 +139,9 @@ class TagManager: def remFromStr(self, deltags: str, tags: str) -> str: "Delete tags if they exist." - def wildcard(pat, str): + def wildcard(pat: str, repl: str): pat = re.escape(pat).replace("\\*", ".*") - return re.match("^" + pat + "$", str, re.IGNORECASE) + return re.match("^" + pat + "$", repl, re.IGNORECASE) currentTags = self.split(tags) for tag in self.split(deltags): diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index d8d3975ab..fa3e21c0d 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -5,7 +5,6 @@ from __future__ import annotations # some add-ons expect json to be in the utils module import json # pylint: disable=unused-import -import locale import os import platform import random @@ -20,7 +19,7 @@ import traceback from contextlib import contextmanager from hashlib import sha1 from html.entities import name2codepoint -from typing import Iterable, Iterator, List, Optional, Union +from typing import Any, Iterable, Iterator, List, Match, Optional, Union from anki.dbproxy import DBProxy @@ -46,22 +45,6 @@ def intTime(scale: int = 1) -> int: return int(time.time() * scale) -# Locale -############################################################################## - - -def fmtPercentage(float_value, point=1) -> str: - "Return float with percentage sign" - fmt = "%" + "0.%(b)df" % {"b": point} - return locale.format_string(fmt, float_value) + "%" - - -def fmtFloat(float_value, point=1) -> str: - "Return a string with decimal separator according to current locale" - fmt = "%" + "0.%(b)df" % {"b": point} - return locale.format_string(fmt, float_value) - - # HTML ############################################################################## reComment = re.compile("(?s)") @@ -114,7 +97,7 @@ def entsToTxt(html: str) -> str: # replace it first html = html.replace(" ", " ") - def fixup(m): + def fixup(m: Match) -> str: text = m.group(0) if text[:2] == "&#": # character reference @@ -140,14 +123,6 @@ def entsToTxt(html: str) -> str: ############################################################################## -def hexifyID(id) -> str: - return "%x" % int(id) - - -def dehexifyID(id) -> int: - return int(id, 16) - - def ids2str(ids: Iterable[Union[int, str]]) -> str: """Given a list of integers, return a string '(int1,int2,...)'.""" return "(%s)" % ",".join(str(i) for i in ids) @@ -195,23 +170,6 @@ def guid64() -> str: return base91(random.randint(0, 2 ** 64 - 1)) -# increment a guid by one, for note type conflicts -def incGuid(guid) -> str: - return _incGuid(guid[::-1])[::-1] - - -def _incGuid(guid) -> str: - s = string - table = s.ascii_letters + s.digits + _base91_extra_chars - idx = table.index(guid[0]) - if idx + 1 == len(table): - # overflow - guid = table[0] + _incGuid(guid[1:]) - else: - guid = table[idx + 1] + guid[1:] - return guid - - # Fields ############################################################################## @@ -250,7 +208,7 @@ def tmpdir() -> str: global _tmpdir if not _tmpdir: - def cleanup(): + def cleanup() -> None: if os.path.exists(_tmpdir): shutil.rmtree(_tmpdir) @@ -294,7 +252,7 @@ def noBundledLibs() -> Iterator[None]: os.environ["LD_LIBRARY_PATH"] = oldlpath -def call(argv: List[str], wait: bool = True, **kwargs) -> int: +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: @@ -338,7 +296,7 @@ devMode = os.getenv("ANKIDEV", "") invalidFilenameChars = ':*?"<>|' -def invalidFilename(str, dirsep=True) -> Optional[str]: +def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]: for c in invalidFilenameChars: if c in str: return c @@ -384,7 +342,7 @@ class TimedLog: def __init__(self) -> None: self._last = time.time() - def log(self, s) -> None: + 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) From bb92dde2d71861575e87daecaff57e6bf0e38010 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 31 Jan 2021 21:05:46 +1000 Subject: [PATCH 03/22] warn add-ons importing json from anki.utils; use stdout not stderr --- pylib/anki/collection.py | 3 ++- pylib/anki/utils.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index c21fc57ac..9a057e23d 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -8,6 +8,7 @@ import enum import os import pprint import re +import sys import time import traceback import weakref @@ -107,7 +108,7 @@ class Collection: @property def backend(self) -> RustBackend: - traceback.print_stack() + traceback.print_stack(file=sys.stdout) print() print( "Accessing the backend directly will break in the future. Please use the public methods on Collection instead." diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index fa3e21c0d..3e7e5feda 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -3,8 +3,7 @@ from __future__ import annotations -# some add-ons expect json to be in the utils module -import json # pylint: disable=unused-import +import json as _json import os import platform import random @@ -33,8 +32,17 @@ try: from_json_bytes = orjson.loads except: print("orjson is missing; DB operations will be slower") - to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore - from_json_bytes = json.loads + to_json_bytes = lambda obj: _json.dumps(obj).encode("utf8") # type: ignore + 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 ############################################################################## From 1741ce1ed86c4bc7753d499821fc0b7ece4da08d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 31 Jan 2021 21:38:36 +1000 Subject: [PATCH 04/22] add more typing, and enable checks for missing types for most of pylib --- pylib/anki/collection.py | 21 ++++++++------- pylib/anki/config.py | 21 ++++++++------- pylib/anki/decks.py | 45 ++++++++++++++++--------------- pylib/anki/find.py | 6 ++--- pylib/anki/hooks.py | 12 ++++----- pylib/anki/media.py | 4 +-- pylib/anki/models.py | 27 ++++++++++--------- pylib/anki/notes.py | 2 +- pylib/anki/sched.py | 4 +-- pylib/anki/schedv2.py | 22 ++++++++------- pylib/anki/stats.py | 6 ++--- pylib/anki/syncserver/__init__.py | 17 +++++++----- pylib/anki/tags.py | 4 +-- pylib/mypy.ini | 8 ++++++ 14 files changed, 112 insertions(+), 87 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 9a057e23d..d227391f2 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -281,7 +281,7 @@ class Collection: self.db.rollback() self.db.begin() - def reopen(self, after_full_sync=False) -> None: + def reopen(self, after_full_sync: bool = False) -> None: assert not self.db assert self.path.endswith(".anki2") @@ -410,7 +410,7 @@ class Collection: def cardCount(self) -> Any: return self.db.scalar("select count() from cards") - def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]): + def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]) -> None: "You probably want .remove_notes_by_card() instead." self._backend.remove_cards(card_ids=card_ids) @@ -506,7 +506,7 @@ class Collection: dupes = [] fields: Dict[int, int] = {} - def ordForMid(mid): + def ordForMid(mid: int) -> int: if mid not in fields: model = self.models.get(mid) for c, f in enumerate(model["flds"]): @@ -540,7 +540,10 @@ class Collection: ########################################################################## def build_search_string( - self, *terms: Union[str, SearchTerm], negate=False, match_any=False + self, + *terms: Union[str, SearchTerm], + negate: bool = False, + match_any: bool = False, ) -> str: """Helper function for the backend's search string operations. @@ -577,11 +580,11 @@ class Collection: except KeyError: return default - def set_config(self, key: str, val: Any): + def set_config(self, key: str, val: Any) -> None: self.setMod() self.conf.set(key, val) - def remove_config(self, key): + def remove_config(self, key: str) -> None: self.setMod() self.conf.remove(key) @@ -780,11 +783,11 @@ table.review-log {{ {revlog_style} }} # Logging ########################################################################## - def log(self, *args, **kwargs) -> None: + def log(self, *args: Any, **kwargs: Any) -> None: if not self._should_log: return - def customRepr(x): + def customRepr(x: Any) -> str: if isinstance(x, str): return x return pprint.pformat(x) @@ -866,7 +869,7 @@ table.review-log {{ {revlog_style} }} def get_preferences(self) -> Preferences: return self._backend.get_preferences() - def set_preferences(self, prefs: Preferences): + def set_preferences(self, prefs: Preferences) -> None: self._backend.set_preferences(prefs) diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 46bb1e1b7..e77e3e7a8 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -21,6 +21,7 @@ from __future__ import annotations import copy import weakref from typing import Any +from weakref import ref import anki from anki.errors import NotFoundError @@ -46,7 +47,7 @@ class ConfigManager: # Legacy dict interface ######################### - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: val = self.get_immutable(key) if isinstance(val, list): print( @@ -61,28 +62,28 @@ class ConfigManager: else: return val - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) - def get(self, key, default=None): + def get(self, key: str, default: Any = None) -> Any: try: return self[key] except KeyError: return default - def setdefault(self, key, default): + def setdefault(self, key: str, default: Any) -> Any: if key not in self: self[key] = default return self[key] - def __contains__(self, key): + def __contains__(self, key: str) -> bool: try: self.get_immutable(key) return True except KeyError: return False - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: self.remove(key) @@ -95,13 +96,13 @@ class ConfigManager: class WrappedList(list): - def __init__(self, conf, key, val): + def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None: self.key = key self.conf = conf self.orig = copy.deepcopy(val) super().__init__(val) - def __del__(self): + def __del__(self) -> None: cur = list(self) conf = self.conf() if conf and self.orig != cur: @@ -109,13 +110,13 @@ class WrappedList(list): class WrappedDict(dict): - def __init__(self, conf, key, val): + def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None: self.key = key self.conf = conf self.orig = copy.deepcopy(val) super().__init__(val) - def __del__(self): + def __del__(self) -> None: cur = dict(self) conf = self.conf() if conf and self.orig != cur: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 9c0aec102..5242b23ab 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -5,6 +5,8 @@ from __future__ import annotations import copy import pprint +import sys +import traceback from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import @@ -43,36 +45,37 @@ class DecksDictProxy: def __init__(self, col: anki.collection.Collection): self._col = col.weakref() - def _warn(self): + def _warn(self) -> None: + traceback.print_stack(file=sys.stdout) print("add-on should use methods on col.decks, not col.decks.decks dict") - def __getitem__(self, item): + def __getitem__(self, item: Any) -> Any: self._warn() return self._col.decks.get(int(item)) - def __setitem__(self, key, val): + def __setitem__(self, key: Any, val: Any) -> None: self._warn() self._col.decks.save(val) - def __len__(self): + def __len__(self) -> int: self._warn() return len(self._col.decks.all_names_and_ids()) - def keys(self): + def keys(self) -> Any: self._warn() return [str(nt.id) for nt in self._col.decks.all_names_and_ids()] - def values(self): + def values(self) -> Any: self._warn() return self._col.decks.all() - def items(self): + def items(self) -> Any: self._warn() return [(str(nt["id"]), nt) for nt in self._col.decks.all()] - def __contains__(self, item): + def __contains__(self, item: Any) -> bool: self._warn() - self._col.decks.have(item) + return self._col.decks.have(item) class DeckManager: @@ -97,7 +100,7 @@ class DeckManager: self.update(g, preserve_usn=False) # legacy - def flush(self): + def flush(self) -> None: pass def __repr__(self) -> str: @@ -135,7 +138,7 @@ class DeckManager: self.col._backend.remove_deck(did) def all_names_and_ids( - self, skip_empty_default=False, include_filtered=True + self, skip_empty_default: bool = False, include_filtered: bool = True ) -> Sequence[DeckNameID]: "A sorted sequence of deck names and IDs." return self.col._backend.get_deck_names( @@ -195,12 +198,12 @@ class DeckManager: ) ] - def collapse(self, did) -> None: + def collapse(self, did: int) -> None: deck = self.get(did) deck["collapsed"] = not deck["collapsed"] self.save(deck) - def collapseBrowser(self, did) -> None: + def collapseBrowser(self, did: int) -> None: deck = self.get(did) collapsed = deck.get("browserCollapsed", False) deck["browserCollapsed"] = not collapsed @@ -241,7 +244,7 @@ class DeckManager: return self.get_legacy(id) return None - def update(self, g: Deck, preserve_usn=True) -> None: + def update(self, g: Deck, preserve_usn: bool = True) -> None: "Add or update an existing deck. Used for syncing and merging." try: g["id"] = self.col._backend.add_or_update_deck_legacy( @@ -303,7 +306,7 @@ class DeckManager: except NotFoundError: return None - def update_config(self, conf: DeckConfig, preserve_usn=False) -> None: + def update_config(self, conf: DeckConfig, preserve_usn: bool = False) -> None: conf["id"] = self.col._backend.add_or_update_deck_config_legacy( config=to_json_bytes(conf), preserve_usn_and_mtime=preserve_usn ) @@ -325,7 +328,7 @@ class DeckManager: ) -> int: return self.add_config(name, clone_from)["id"] - def remove_config(self, id) -> None: + def remove_config(self, id: int) -> None: "Remove a configuration and update all decks using it." self.col.modSchema(check=True) for g in self.all(): @@ -341,14 +344,14 @@ class DeckManager: grp["conf"] = id self.save(grp) - def didsForConf(self, conf) -> List[int]: + def didsForConf(self, conf: DeckConfig) -> List[int]: dids = [] for deck in self.all(): if "conf" in deck and deck["conf"] == conf["id"]: dids.append(deck["id"]) return dids - def restoreToDefault(self, conf) -> None: + def restoreToDefault(self, conf: DeckConfig) -> None: oldOrder = conf["new"]["order"] new = from_json_bytes(self.col._backend.new_deck_config_legacy()) new["id"] = conf["id"] @@ -380,7 +383,7 @@ class DeckManager: return deck["name"] return None - def setDeck(self, cids, did) -> None: + def setDeck(self, cids: List[int], did: int) -> None: self.col.db.execute( "update cards set did=?,usn=?,mod=? where id in " + ids2str(cids), did, @@ -424,7 +427,7 @@ class DeckManager: self.col.conf["activeDecks"] = active # don't use this, it will likely go away - def update_active(self): + def update_active(self) -> None: self.select(self.current()["id"]) # Parents/children @@ -480,7 +483,7 @@ class DeckManager: # Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type def childDids(self, did: int, childMap: DeckManager.childMapNode) -> List: - def gather(node: DeckManager.childMapNode, arr): + def gather(node: DeckManager.childMapNode, arr: List) -> None: for did, child in node.items(): arr.append(did) gather(child, arr) diff --git a/pylib/anki/find.py b/pylib/anki/find.py index 553c9b35e..af829cb7d 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -16,10 +16,10 @@ class Finder: self.col = col.weakref() print("Finder() is deprecated, please use col.find_cards() or .find_notes()") - def findCards(self, query, order): + def findCards(self, query: Any, order: Any) -> Any: return self.col.find_cards(query, order) - def findNotes(self, query): + def findNotes(self, query: Any) -> Any: return self.col.find_notes(query) @@ -55,7 +55,7 @@ def fieldNamesForNotes(col: Collection, nids: List[int]) -> List[str]: ########################################################################## -def fieldNames(col, downcase=True) -> List: +def fieldNames(col: Collection, downcase: bool = True) -> List: fields: Set[str] = set() for m in col.models.all(): for f in m["flds"]: diff --git a/pylib/anki/hooks.py b/pylib/anki/hooks.py index 8a65d7bf2..87ca01ee8 100644 --- a/pylib/anki/hooks.py +++ b/pylib/anki/hooks.py @@ -25,7 +25,7 @@ from anki.hooks_gen import * _hooks: Dict[str, List[Callable[..., Any]]] = {} -def runHook(hook: str, *args) -> None: +def runHook(hook: str, *args: Any) -> None: "Run all functions on hook." hookFuncs = _hooks.get(hook, None) if hookFuncs: @@ -37,7 +37,7 @@ def runHook(hook: str, *args) -> None: raise -def runFilter(hook: str, arg: Any, *args) -> Any: +def runFilter(hook: str, arg: Any, *args: Any) -> Any: hookFuncs = _hooks.get(hook, None) if hookFuncs: for func in hookFuncs: @@ -57,7 +57,7 @@ def addHook(hook: str, func: Callable) -> None: _hooks[hook].append(func) -def remHook(hook, func) -> None: +def remHook(hook: Any, func: Any) -> None: "Remove a function if is on hook." hook = _hooks.get(hook, []) if func in hook: @@ -72,10 +72,10 @@ def remHook(hook, func) -> None: # # If you call wrap() with pos='around', the original function will not be called # automatically but can be called with _old(). -def wrap(old, new, pos="after") -> Callable: +def wrap(old: Any, new: Any, pos: str = "after") -> Callable: "Override an existing function." - def repl(*args, **kwargs): + def repl(*args: Any, **kwargs: Any) -> Any: if pos == "after": old(*args, **kwargs) return new(*args, **kwargs) @@ -85,7 +85,7 @@ def wrap(old, new, pos="after") -> Callable: else: return new(_old=old, *args, **kwargs) - def decorator_wrapper(f, *args, **kwargs): + def decorator_wrapper(f: Any, *args: Any, **kwargs: Any) -> Any: return repl(*args, **kwargs) return decorator.decorator(decorator_wrapper)(old) diff --git a/pylib/anki/media.py b/pylib/anki/media.py index 0b294d45c..c9b3b293e 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -11,7 +11,7 @@ import time import urllib.error import urllib.parse import urllib.request -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable, List, Match, Optional, Tuple import anki import anki._backend.backend_pb2 as _pb @@ -197,7 +197,7 @@ class MediaManager: else: fn = urllib.parse.quote - def repl(match): + def repl(match: Match) -> str: tag = match.group(0) fname = match.group("fname") if re.match("(https?|ftp)://", fname): diff --git a/pylib/anki/models.py b/pylib/anki/models.py index e437171b3..363d12cc1 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -5,7 +5,9 @@ from __future__ import annotations import copy import pprint +import sys import time +import traceback from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import anki # pylint: disable=unused-import @@ -39,36 +41,37 @@ class ModelsDictProxy: def __init__(self, col: anki.collection.Collection): self._col = col.weakref() - def _warn(self): + def _warn(self) -> None: + traceback.print_stack(file=sys.stdout) print("add-on should use methods on col.models, not col.models.models dict") - def __getitem__(self, item): + def __getitem__(self, item: Any) -> Any: self._warn() return self._col.models.get(int(item)) - def __setitem__(self, key, val): + def __setitem__(self, key: str, val: Any) -> None: self._warn() self._col.models.save(val) - def __len__(self): + def __len__(self) -> int: self._warn() return len(self._col.models.all_names_and_ids()) - def keys(self): + def keys(self) -> Any: self._warn() return [str(nt.id) for nt in self._col.models.all_names_and_ids()] - def values(self): + def values(self) -> Any: self._warn() return self._col.models.all() - def items(self): + def items(self) -> Any: self._warn() return [(str(nt["id"]), nt) for nt in self._col.models.all()] - def __contains__(self, item): + def __contains__(self, item: Any) -> bool: self._warn() - self._col.models.have(item) + return self._col.models.have(item) class ModelManager: @@ -123,7 +126,7 @@ class ModelManager: def _get_cached(self, ntid: int) -> Optional[NoteType]: return self._cache.get(ntid) - def _clear_cache(self): + def _clear_cache(self) -> None: self._cache = {} # Listing note types @@ -218,7 +221,7 @@ class ModelManager: "Delete model, and all its cards/notes." self.remove(m["id"]) - def remove_all_notetypes(self): + def remove_all_notetypes(self) -> None: for nt in self.all_names_and_ids(): self._remove_from_cache(nt.id) self.col._backend.remove_notetype(nt.id) @@ -236,7 +239,7 @@ class ModelManager: if existing_id is not None and existing_id != m["id"]: m["name"] += "-" + checksum(str(time.time()))[:5] - def update(self, m: NoteType, preserve_usn=True) -> None: + def update(self, m: NoteType, preserve_usn: bool = True) -> None: "Add or update an existing model. Use .save() instead." self._remove_from_cache(m["id"]) self.ensureNameUnique(m) diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 4b33da9f8..456fd28bd 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -113,7 +113,7 @@ class Note: def __setitem__(self, key: str, value: str) -> None: self.fields[self._fieldOrd(key)] = value - def __contains__(self, key) -> bool: + def __contains__(self, key: str) -> bool: return key in self._fmap # Tags diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 7c60fda30..ad4967a64 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -350,7 +350,7 @@ limit %d""" lastIvl = -(self._delayForGrade(conf, lastLeft)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) - def log(): + def log() -> None: self.col.db.execute( "insert into revlog values (?,?,?,?,?,?,?,?,?)", int(time.time() * 1000), @@ -450,7 +450,7 @@ and due <= ? limit ?)""", self._revQueue: List[Any] = [] self._revDids = self.col.decks.active()[:] - def _fillRev(self, recursing=False) -> bool: + def _fillRev(self, recursing: bool = False) -> bool: "True if a review card can be fetched." if self._revQueue: return True diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 682974a29..b470eb1dc 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -166,7 +166,7 @@ class Scheduler: self._restorePreviewCard(card) self._removeFromFiltered(card) - def _reset_counts(self): + def _reset_counts(self) -> None: tree = self.deck_due_tree(self.col.decks.selected()) node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"])) if not node: @@ -187,7 +187,7 @@ class Scheduler: new, lrn, rev = counts return (new, lrn, rev) - def _is_finished(self): + def _is_finished(self) -> bool: "Don't use this, it is a stop-gap until this code is refactored." return not any((self.newCount, self.revCount, self._immediate_learn_count)) @@ -229,8 +229,12 @@ order by due""" ########################################################################## def update_stats( - self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0 - ): + self, + deck_id: int, + new_delta: int = 0, + review_delta: int = 0, + milliseconds_delta: int = 0, + ) -> None: self.col._backend.update_stats( deck_id=deck_id, new_delta=new_delta, @@ -321,7 +325,7 @@ order by due""" self._newQueue: List[int] = [] self._updateNewCardRatio() - def _fillNew(self, recursing=False) -> bool: + def _fillNew(self, recursing: bool = False) -> bool: if self._newQueue: return True if not self.newCount: @@ -841,7 +845,7 @@ and due <= ? limit ?)""" def _resetRev(self) -> None: self._revQueue: List[int] = [] - def _fillRev(self, recursing=False) -> bool: + def _fillRev(self, recursing: bool = False) -> bool: "True if a review card can be fetched." if self._revQueue: return True @@ -947,7 +951,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l self._removeFromFiltered(card) def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None: - def log(): + def log() -> None: self.col.db.execute( "insert into revlog values (?,?,?,?,?,?,?,?,?)", int(time.time() * 1000), @@ -1344,7 +1348,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe mode = BuryOrSuspendMode.BURY_SCHED self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode) - def bury_note(self, note: Note): + def bury_note(self, note: Note) -> None: self.bury_cards(note.card_ids()) # legacy @@ -1472,7 +1476,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", def orderCards(self, did: int) -> None: self.col._backend.sort_deck(deck_id=did, randomize=False) - def resortConf(self, conf) -> None: + def resortConf(self, conf: DeckConfig) -> None: for did in self.col.decks.didsForConf(conf): if conf["new"]["order"] == 0: self.randomizeCards(did) diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 24cdcc1b9..789d5e852 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -140,7 +140,7 @@ from revlog where id > ? """ relrn = relrn or 0 filt = filt or 0 # studied - def bold(s): + def bold(s: str) -> str: return "" + str(s) + "" if cards: @@ -298,7 +298,7 @@ group by day order by day""" # pylint: disable=invalid-unary-operand-type conf["xaxis"]["min"] = -days + 0.5 - def plot(id, data, ylabel, ylabel2): + def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str: return self._graph( id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2 ) @@ -333,7 +333,7 @@ group by day order by day""" # pylint: disable=invalid-unary-operand-type conf["xaxis"]["min"] = -days + 0.5 - def plot(id, data, ylabel, ylabel2): + def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str: return self._graph( id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2 ) diff --git a/pylib/anki/syncserver/__init__.py b/pylib/anki/syncserver/__init__.py index 71ae39196..aff0a35e0 100644 --- a/pylib/anki/syncserver/__init__.py +++ b/pylib/anki/syncserver/__init__.py @@ -11,9 +11,10 @@ import os import socket import sys import time +from http import HTTPStatus from io import BytesIO from tempfile import NamedTemporaryFile -from typing import Optional +from typing import Iterable, Optional try: import flask @@ -89,7 +90,7 @@ def handle_sync_request(method_str: str) -> Response: elif method == Method.FULL_DOWNLOAD: path = outdata.decode("utf8") - def stream_reply(): + def stream_reply() -> Iterable[bytes]: with open(path, "rb") as f: while chunk := f.read(16 * 1024): yield chunk @@ -106,7 +107,7 @@ def handle_sync_request(method_str: str) -> Response: return resp -def after_full_sync(): +def after_full_sync() -> None: # the server methods do not reopen the collection after a full sync, # so we need to col.reopen(after_full_sync=False) @@ -146,15 +147,17 @@ def get_method( @app.route("/", methods=["POST"]) -def handle_request(pathin: str): +def handle_request(pathin: str) -> Response: path = pathin print(int(time.time()), flask.request.remote_addr, path) if path.startswith("sync/"): return handle_sync_request(path.split("/", maxsplit=1)[1]) + else: + return flask.make_response("not found", HTTPStatus.NOT_FOUND) -def folder(): +def folder() -> str: folder = os.getenv("FOLDER", os.path.expanduser("~/.syncserver")) if not os.path.exists(folder): print("creating", folder) @@ -162,11 +165,11 @@ def folder(): return folder -def col_path(): +def col_path() -> str: return os.path.join(folder(), "collection.server.anki2") -def serve(): +def serve() -> None: global col col = Collection(col_path(), server=True) diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index e64e9d434..67ae46c20 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -13,7 +13,7 @@ from __future__ import annotations import pprint import re -from typing import Collection, List, Optional, Sequence, Tuple +from typing import Collection, List, Match, Optional, Sequence, Tuple import anki # pylint: disable=unused-import import anki._backend.backend_pb2 as _pb @@ -139,7 +139,7 @@ class TagManager: def remFromStr(self, deltags: str, tags: str) -> str: "Delete tags if they exist." - def wildcard(pat: str, repl: str): + def wildcard(pat: str, repl: str) -> Match: pat = re.escape(pat).replace("\\*", ".*") return re.match("^" + pat + "$", repl, re.IGNORECASE) diff --git a/pylib/mypy.ini b/pylib/mypy.ini index 7324ddfbc..074377dad 100644 --- a/pylib/mypy.ini +++ b/pylib/mypy.ini @@ -9,6 +9,14 @@ warn_redundant_casts = True warn_unused_configs = True strict_equality = true +[mypy-anki.*] +disallow_untyped_defs = True +[mypy-anki.importing.*] +disallow_untyped_defs = False +[mypy-anki.exporting] +disallow_untyped_defs = False + + [mypy-win32file] ignore_missing_imports = True [mypy-win32pipe] From 33160dcb00373caa5f5aace69a529c556e8e4032 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 30 Jan 2021 17:54:07 +0100 Subject: [PATCH 05/22] Make editor a rollup package --- qt/aqt/data/web/js/BUILD.bazel | 28 +- qt/aqt/data/web/js/editor/BUILD.bazel | 16 + qt/aqt/data/web/js/editor/filterHtml.ts | 170 +++++++++++ qt/aqt/data/web/js/editor/helpers.ts | 65 +++++ .../web/js/{editor.ts => editor/index.ts} | 274 ++---------------- qt/aqt/data/web/js/rollup.config.js | 15 + 6 files changed, 312 insertions(+), 256 deletions(-) create mode 100644 qt/aqt/data/web/js/editor/BUILD.bazel create mode 100644 qt/aqt/data/web/js/editor/filterHtml.ts create mode 100644 qt/aqt/data/web/js/editor/helpers.ts rename qt/aqt/data/web/js/{editor.ts => editor/index.ts} (72%) create mode 100644 qt/aqt/data/web/js/rollup.config.js diff --git a/qt/aqt/data/web/js/BUILD.bazel b/qt/aqt/data/web/js/BUILD.bazel index b47d90193..24800474f 100644 --- a/qt/aqt/data/web/js/BUILD.bazel +++ b/qt/aqt/data/web/js/BUILD.bazel @@ -1,9 +1,12 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") ts_library( name = "pycmd", srcs = ["pycmd.d.ts"], + visibility = ["//qt/aqt/data/web/js:__subpackages__"], ) ts_library( @@ -26,10 +29,30 @@ filegroup( output_group = "es5_sources", ) +###### aqt bundles + +rollup_bundle( + name = "editor", + config_file = "rollup.config.js", + entry_point = "//qt/aqt/data/web/js/editor:index.ts", + format = "iife", + link_workspace_root = True, + silent = True, + sourcemap = "false", + deps = [ + "//qt/aqt/data/web/js/editor", + "@npm//@rollup/plugin-commonjs", + "@npm//@rollup/plugin-node-resolve", + "@npm//rollup-plugin-terser", + ], +) + + filegroup( name = "js", srcs = [ "aqt_es5", + "editor", "mathjax.js", "//qt/aqt/data/web/js/vendor", ], @@ -47,4 +70,7 @@ prettier_test( # srcs = glob(["*.ts"]), # ) -exports_files(["mathjax.js"]) +exports_files([ + "mathjax.js", + "tsconfig.json", +]) diff --git a/qt/aqt/data/web/js/editor/BUILD.bazel b/qt/aqt/data/web/js/editor/BUILD.bazel new file mode 100644 index 000000000..853ea8471 --- /dev/null +++ b/qt/aqt/data/web/js/editor/BUILD.bazel @@ -0,0 +1,16 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") + +ts_library( + name = "editor", + srcs = glob(["*.ts"]), + tsconfig = "//qt/aqt/data/web/js:tsconfig.json", + deps = [ + "//qt/aqt/data/web/js:pycmd", + "@npm//@types/jquery", + ], + visibility = ["//qt:__subpackages__"], +) + +exports_files([ + "index.ts", +]) diff --git a/qt/aqt/data/web/js/editor/filterHtml.ts b/qt/aqt/data/web/js/editor/filterHtml.ts new file mode 100644 index 000000000..0b142932d --- /dev/null +++ b/qt/aqt/data/web/js/editor/filterHtml.ts @@ -0,0 +1,170 @@ +import { nodeIsElement } from "./helpers"; + +export let filterHTML = function ( + html: string, + internal: boolean, + extendedMode: boolean +): string { + // wrap it in as we aren't allowed to change top level elements + const top = document.createElement("ankitop"); + top.innerHTML = html; + + if (internal) { + filterInternalNode(top); + } else { + filterNode(top, extendedMode); + } + let outHtml = top.innerHTML; + if (!extendedMode && !internal) { + // collapse whitespace + outHtml = outHtml.replace(/[\n\t ]+/g, " "); + } + outHtml = outHtml.trim(); + return outHtml; +}; + +let allowedTagsBasic = {}; +let allowedTagsExtended = {}; + +let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"]; +for (const tag of TAGS_WITHOUT_ATTRS) { + allowedTagsBasic[tag] = { attrs: [] }; +} + +TAGS_WITHOUT_ATTRS = [ + "B", + "BLOCKQUOTE", + "CODE", + "DD", + "DL", + "DT", + "EM", + "H1", + "H2", + "H3", + "I", + "LI", + "OL", + "PRE", + "RP", + "RT", + "RUBY", + "STRONG", + "TABLE", + "U", + "UL", +]; +for (const tag of TAGS_WITHOUT_ATTRS) { + allowedTagsExtended[tag] = { attrs: [] }; +} + +allowedTagsBasic["IMG"] = { attrs: ["SRC"] }; + +allowedTagsExtended["A"] = { attrs: ["HREF"] }; +allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] }; +allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] }; +allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] }; +allowedTagsExtended["FONT"] = { attrs: ["COLOR"] }; + +const allowedStyling = { + color: true, + "background-color": true, + "font-weight": true, + "font-style": true, + "text-decoration-line": true, +}; + +let isNightMode = function (): boolean { + return document.body.classList.contains("nightMode"); +}; + +let filterExternalSpan = function (elem: HTMLElement) { + // filter out attributes + for (const attr of [...elem.attributes]) { + const attrName = attr.name.toUpperCase(); + + if (attrName !== "STYLE") { + elem.removeAttributeNode(attr); + } + } + + // filter styling + for (const name of [...elem.style]) { + const value = elem.style.getPropertyValue(name); + + if ( + !allowedStyling.hasOwnProperty(name) || + // google docs adds this unnecessarily + (name === "background-color" && value === "transparent") || + // ignore coloured text in night mode for now + (isNightMode() && (name === "background-color" || name === "color")) + ) { + elem.style.removeProperty(name); + } + } +}; + +allowedTagsExtended["SPAN"] = filterExternalSpan; + +// add basic tags to extended +Object.assign(allowedTagsExtended, allowedTagsBasic); + +function isHTMLElement(elem: Element): elem is HTMLElement { + return elem instanceof HTMLElement; +} + +// filtering from another field +let filterInternalNode = function (elem: Element) { + if (isHTMLElement(elem)) { + elem.style.removeProperty("background-color"); + elem.style.removeProperty("font-size"); + elem.style.removeProperty("font-family"); + } + // recurse + for (let i = 0; i < elem.children.length; i++) { + const child = elem.children[i]; + filterInternalNode(child); + } +}; + +// filtering from external sources +let filterNode = function (node: Node, extendedMode: boolean): void { + if (!nodeIsElement(node)) { + return; + } + + // descend first, and take a copy of the child nodes as the loop will skip + // elements due to node modifications otherwise + for (const child of [...node.children]) { + filterNode(child, extendedMode); + } + + if (node.tagName === "ANKITOP") { + return; + } + + const tag = extendedMode + ? allowedTagsExtended[node.tagName] + : allowedTagsBasic[node.tagName]; + + if (!tag) { + if (!node.innerHTML || node.tagName === "TITLE") { + node.parentNode.removeChild(node); + } else { + node.outerHTML = node.innerHTML; + } + } else { + if (typeof tag === "function") { + // filtering function provided + tag(node); + } else { + // allowed, filter out attributes + for (const attr of [...node.attributes]) { + const attrName = attr.name.toUpperCase(); + if (tag.attrs.indexOf(attrName) === -1) { + node.removeAttributeNode(attr); + } + } + } + } +}; diff --git a/qt/aqt/data/web/js/editor/helpers.ts b/qt/aqt/data/web/js/editor/helpers.ts new file mode 100644 index 000000000..c2ce1efc1 --- /dev/null +++ b/qt/aqt/data/web/js/editor/helpers.ts @@ -0,0 +1,65 @@ +export function nodeIsElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE; +} + +const INLINE_TAGS = [ + "A", + "ABBR", + "ACRONYM", + "AUDIO", + "B", + "BDI", + "BDO", + "BIG", + "BR", + "BUTTON", + "CANVAS", + "CITE", + "CODE", + "DATA", + "DATALIST", + "DEL", + "DFN", + "EM", + "EMBED", + "I", + "IFRAME", + "IMG", + "INPUT", + "INS", + "KBD", + "LABEL", + "MAP", + "MARK", + "METER", + "NOSCRIPT", + "OBJECT", + "OUTPUT", + "PICTURE", + "PROGRESS", + "Q", + "RUBY", + "S", + "SAMP", + "SCRIPT", + "SELECT", + "SLOT", + "SMALL", + "SPAN", + "STRONG", + "SUB", + "SUP", + "SVG", + "TEMPLATE", + "TEXTAREA", + "TIME", + "U", + "TT", + "VAR", + "VIDEO", + "WBR", +]; + +export function nodeIsInline(node: Node): boolean { + return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName); +} diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor/index.ts similarity index 72% rename from qt/aqt/data/web/js/editor.ts rename to qt/aqt/data/web/js/editor/index.ts index 20a1d7dea..8f15811be 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor/index.ts @@ -1,16 +1,15 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ +import { filterHTML } from "./filterHtml"; +import { nodeIsElement, nodeIsInline } from "./helpers"; + let currentField: EditingArea | null = null; let changeTimer: number | null = null; let currentNoteId: number | null = null; -declare interface String { - format(...args: string[]): string; -} - /* kept for compatibility with add-ons */ -String.prototype.format = function (...args: string[]): string { +(String.prototype as any).format = function (...args: string[]): string { return this.replace(/\{\d+\}/g, (m: string): void => { const match = m.match(/\d+/); @@ -18,11 +17,11 @@ String.prototype.format = function (...args: string[]): string { }); }; -function setFGButton(col: string): void { +export function setFGButton(col: string): void { document.getElementById("forecolor").style.backgroundColor = col; } -function saveNow(keepFocus: boolean): void { +export function saveNow(keepFocus: boolean): void { if (!currentField) { return; } @@ -100,72 +99,6 @@ function onKeyUp(evt: KeyboardEvent): void { } } -function nodeIsElement(node: Node): node is Element { - return node.nodeType === Node.ELEMENT_NODE; -} - -const INLINE_TAGS = [ - "A", - "ABBR", - "ACRONYM", - "AUDIO", - "B", - "BDI", - "BDO", - "BIG", - "BR", - "BUTTON", - "CANVAS", - "CITE", - "CODE", - "DATA", - "DATALIST", - "DEL", - "DFN", - "EM", - "EMBED", - "I", - "IFRAME", - "IMG", - "INPUT", - "INS", - "KBD", - "LABEL", - "MAP", - "MARK", - "METER", - "NOSCRIPT", - "OBJECT", - "OUTPUT", - "PICTURE", - "PROGRESS", - "Q", - "RUBY", - "S", - "SAMP", - "SCRIPT", - "SELECT", - "SLOT", - "SMALL", - "SPAN", - "STRONG", - "SUB", - "SUP", - "SVG", - "TEMPLATE", - "TEXTAREA", - "TIME", - "U", - "TT", - "VAR", - "VIDEO", - "WBR", -]; - -function nodeIsInline(node: Node): boolean { - return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName); -} - function inListItem(): boolean { const anchor = currentField.getSelection().anchorNode; @@ -179,7 +112,7 @@ function inListItem(): boolean { return inList; } -function insertNewline(): void { +export function insertNewline(): void { if (!inPreEnvironment()) { setFormat("insertText", "\n"); return; @@ -228,12 +161,12 @@ function updateButtonState(): void { // 'col': document.queryCommandValue("forecolor") } -function toggleEditorButton(buttonid: string): void { +export function toggleEditorButton(buttonid: string): void { const button = $(buttonid)[0]; button.classList.toggle("highlighted"); } -function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { +export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { document.execCommand(cmd, false, arg); if (!nosave) { saveField("key"); @@ -279,7 +212,7 @@ function onFocus(evt: FocusEvent): void { } } -function focusField(n: number): void { +export function focusField(n: number): void { const field = getEditorField(n); if (field) { @@ -287,7 +220,7 @@ function focusField(n: number): void { } } -function focusIfField(x: number, y: number): boolean { +export function focusIfField(x: number, y: number): boolean { const elements = document.elementsFromPoint(x, y); for (let i = 0; i < elements.length; i++) { let elem = elements[i] as EditingArea; @@ -361,7 +294,7 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string): return match[1] + front + match[2] + back + match[3]; } -function preventButtonFocus(): void { +export function preventButtonFocus(): void { for (const element of document.querySelectorAll("button.linkb")) { element.addEventListener("mousedown", (evt: Event) => { evt.preventDefault(); @@ -386,7 +319,7 @@ function maybeDisableButtons(): void { } } -function wrap(front: string, back: string): void { +export function wrap(front: string, back: string): void { wrapInternal(front, back, false); } @@ -527,7 +460,7 @@ class EditingArea extends HTMLDivElement { return this.editable.style.direction === "rtl"; } - getSelection(): Selection { + getSelection(): any { return this.shadowRoot.getSelection(); } @@ -622,7 +555,7 @@ function forEditorField( } } -function setFields(fields: [string, string][]): void { +export function setFields(fields: [string, string][]): void { // webengine will include the variable after enter+backspace // if we don't convert it to a literal colour const color = window @@ -637,7 +570,7 @@ function setFields(fields: [string, string][]): void { maybeDisableButtons(); } -function setBackgrounds(cols: ("dupe" | "")[]) { +export function setBackgrounds(cols: ("dupe" | "")[]) { forEditorField(cols, (field, value) => field.editingArea.classList.toggle("dupe", value === "dupe") ); @@ -646,17 +579,17 @@ function setBackgrounds(cols: ("dupe" | "")[]) { .classList.toggle("is-inactive", !cols.includes("dupe")); } -function setFonts(fonts: [string, number, boolean][]): void { +export function setFonts(fonts: [string, number, boolean][]): void { forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => { field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr"); }); } -function setNoteId(id: number): void { +export function setNoteId(id: number): void { currentNoteId = id; } -let pasteHTML = function ( +export let pasteHTML = function ( html: string, internal: boolean, extendedMode: boolean @@ -667,172 +600,3 @@ let pasteHTML = function ( setFormat("inserthtml", html); } }; - -let filterHTML = function ( - html: string, - internal: boolean, - extendedMode: boolean -): string { - // wrap it in as we aren't allowed to change top level elements - const top = document.createElement("ankitop"); - top.innerHTML = html; - - if (internal) { - filterInternalNode(top); - } else { - filterNode(top, extendedMode); - } - let outHtml = top.innerHTML; - if (!extendedMode && !internal) { - // collapse whitespace - outHtml = outHtml.replace(/[\n\t ]+/g, " "); - } - outHtml = outHtml.trim(); - return outHtml; -}; - -let allowedTagsBasic = {}; -let allowedTagsExtended = {}; - -let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"]; -for (const tag of TAGS_WITHOUT_ATTRS) { - allowedTagsBasic[tag] = { attrs: [] }; -} - -TAGS_WITHOUT_ATTRS = [ - "B", - "BLOCKQUOTE", - "CODE", - "DD", - "DL", - "DT", - "EM", - "H1", - "H2", - "H3", - "I", - "LI", - "OL", - "PRE", - "RP", - "RT", - "RUBY", - "STRONG", - "TABLE", - "U", - "UL", -]; -for (const tag of TAGS_WITHOUT_ATTRS) { - allowedTagsExtended[tag] = { attrs: [] }; -} - -allowedTagsBasic["IMG"] = { attrs: ["SRC"] }; - -allowedTagsExtended["A"] = { attrs: ["HREF"] }; -allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] }; -allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] }; -allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] }; -allowedTagsExtended["FONT"] = { attrs: ["COLOR"] }; - -const allowedStyling = { - color: true, - "background-color": true, - "font-weight": true, - "font-style": true, - "text-decoration-line": true, -}; - -let isNightMode = function (): boolean { - return document.body.classList.contains("nightMode"); -}; - -let filterExternalSpan = function (elem: HTMLElement) { - // filter out attributes - for (const attr of [...elem.attributes]) { - const attrName = attr.name.toUpperCase(); - - if (attrName !== "STYLE") { - elem.removeAttributeNode(attr); - } - } - - // filter styling - for (const name of [...elem.style]) { - const value = elem.style.getPropertyValue(name); - - if ( - !allowedStyling.hasOwnProperty(name) || - // google docs adds this unnecessarily - (name === "background-color" && value === "transparent") || - // ignore coloured text in night mode for now - (isNightMode() && (name === "background-color" || name === "color")) - ) { - elem.style.removeProperty(name); - } - } -}; - -allowedTagsExtended["SPAN"] = filterExternalSpan; - -// add basic tags to extended -Object.assign(allowedTagsExtended, allowedTagsBasic); - -function isHTMLElement(elem: Element): elem is HTMLElement { - return elem instanceof HTMLElement; -} - -// filtering from another field -let filterInternalNode = function (elem: Element) { - if (isHTMLElement(elem)) { - elem.style.removeProperty("background-color"); - elem.style.removeProperty("font-size"); - elem.style.removeProperty("font-family"); - } - // recurse - for (let i = 0; i < elem.children.length; i++) { - const child = elem.children[i]; - filterInternalNode(child); - } -}; - -// filtering from external sources -let filterNode = function (node: Node, extendedMode: boolean): void { - if (!nodeIsElement(node)) { - return; - } - - // descend first, and take a copy of the child nodes as the loop will skip - // elements due to node modifications otherwise - for (const child of [...node.children]) { - filterNode(child, extendedMode); - } - - if (node.tagName === "ANKITOP") { - return; - } - - const tag = extendedMode - ? allowedTagsExtended[node.tagName] - : allowedTagsBasic[node.tagName]; - - if (!tag) { - if (!node.innerHTML || node.tagName === "TITLE") { - node.parentNode.removeChild(node); - } else { - node.outerHTML = node.innerHTML; - } - } else { - if (typeof tag === "function") { - // filtering function provided - tag(node); - } else { - // allowed, filter out attributes - for (const attr of [...node.attributes]) { - const attrName = attr.name.toUpperCase(); - if (tag.attrs.indexOf(attrName) === -1) { - node.removeAttributeNode(attr); - } - } - } - } -}; diff --git a/qt/aqt/data/web/js/rollup.config.js b/qt/aqt/data/web/js/rollup.config.js new file mode 100644 index 000000000..363cc5ae1 --- /dev/null +++ b/qt/aqt/data/web/js/rollup.config.js @@ -0,0 +1,15 @@ +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import { terser } from "rollup-plugin-terser"; + +import process from "process"; +const production = process.env["COMPILATION_MODE"] === "opt"; + +export default { + output: { + name: "globalThis", + extend: true, + format: "iife", + }, + plugins: [resolve({ browser: true }), commonjs(), production && terser()], +}; From 859a52ab1501c58d702a3c9ce80ea21a12e1faf4 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 30 Jan 2021 18:32:36 +0100 Subject: [PATCH 06/22] Fix type issues --- qt/aqt/data/web/js/editor/index.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/qt/aqt/data/web/js/editor/index.ts b/qt/aqt/data/web/js/editor/index.ts index 8f15811be..d38215d43 100644 --- a/qt/aqt/data/web/js/editor/index.ts +++ b/qt/aqt/data/web/js/editor/index.ts @@ -8,8 +8,21 @@ let currentField: EditingArea | null = null; let changeTimer: number | null = null; let currentNoteId: number | null = null; +declare global { + interface String { + format(...args: string[]): string; + } + + interface Selection { + modify(s: string, t: string, u: string): void; + addRange(r: Range): void; + removeAllRanges(): void; + getRangeAt(n: number): Range; + } +} + /* kept for compatibility with add-ons */ -(String.prototype as any).format = function (...args: string[]): string { +String.prototype.format = function (...args: string[]): string { return this.replace(/\{\d+\}/g, (m: string): void => { const match = m.match(/\d+/); @@ -44,10 +57,6 @@ function triggerKeyTimer(): void { }, 600); } -interface Selection { - modify(s: string, t: string, u: string): void; -} - function onKey(evt: KeyboardEvent): void { // esc clears focus, allowing dialog to close if (evt.code === "Escape") { @@ -460,7 +469,7 @@ class EditingArea extends HTMLDivElement { return this.editable.style.direction === "rtl"; } - getSelection(): any { + getSelection(): Selection { return this.shadowRoot.getSelection(); } From 2ab06a654048214a90926c1e6eec2707b80adbf3 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 31 Jan 2021 14:15:03 +0100 Subject: [PATCH 07/22] Move editor to /ts/editor --- qt/aqt/data/web/js/BUILD.bazel | 20 ++------ qt/aqt/data/web/js/editor/BUILD.bazel | 16 ------ ts/BUILD.bazel | 2 +- ts/editor/BUILD.bazel | 49 +++++++++++++++++++ .../data/web/js => ts}/editor/filterHtml.ts | 0 {qt/aqt/data/web/js => ts}/editor/helpers.ts | 0 {qt/aqt/data/web/js => ts}/editor/index.ts | 11 +++-- ts/editor/lib.ts | 9 ++++ .../rollup.aqt.config.js | 0 9 files changed, 71 insertions(+), 36 deletions(-) delete mode 100644 qt/aqt/data/web/js/editor/BUILD.bazel create mode 100644 ts/editor/BUILD.bazel rename {qt/aqt/data/web/js => ts}/editor/filterHtml.ts (100%) rename {qt/aqt/data/web/js => ts}/editor/helpers.ts (100%) rename {qt/aqt/data/web/js => ts}/editor/index.ts (98%) create mode 100644 ts/editor/lib.ts rename qt/aqt/data/web/js/rollup.config.js => ts/rollup.aqt.config.js (100%) diff --git a/qt/aqt/data/web/js/BUILD.bazel b/qt/aqt/data/web/js/BUILD.bazel index 24800474f..417c1a9bb 100644 --- a/qt/aqt/data/web/js/BUILD.bazel +++ b/qt/aqt/data/web/js/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") +load("//qt/aqt/data/web/pages:defs.bzl", "copy_page") load("//ts:prettier.bzl", "prettier_test") load("//ts:eslint.bzl", "eslint_test") @@ -29,25 +30,14 @@ filegroup( output_group = "es5_sources", ) -###### aqt bundles - -rollup_bundle( +copy_page( name = "editor", - config_file = "rollup.config.js", - entry_point = "//qt/aqt/data/web/js/editor:index.ts", - format = "iife", - link_workspace_root = True, - silent = True, - sourcemap = "false", - deps = [ - "//qt/aqt/data/web/js/editor", - "@npm//@rollup/plugin-commonjs", - "@npm//@rollup/plugin-node-resolve", - "@npm//rollup-plugin-terser", + srcs = [ + "editor.js", ], + package = "//ts/editor", ) - filegroup( name = "js", srcs = [ diff --git a/qt/aqt/data/web/js/editor/BUILD.bazel b/qt/aqt/data/web/js/editor/BUILD.bazel deleted file mode 100644 index 853ea8471..000000000 --- a/qt/aqt/data/web/js/editor/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_library") - -ts_library( - name = "editor", - srcs = glob(["*.ts"]), - tsconfig = "//qt/aqt/data/web/js:tsconfig.json", - deps = [ - "//qt/aqt/data/web/js:pycmd", - "@npm//@types/jquery", - ], - visibility = ["//qt:__subpackages__"], -) - -exports_files([ - "index.ts", -]) diff --git a/ts/BUILD.bazel b/ts/BUILD.bazel index 53b97d3cd..a64f51265 100644 --- a/ts/BUILD.bazel +++ b/ts/BUILD.bazel @@ -18,9 +18,9 @@ sql_format_setup() exports_files([ "tsconfig.json", - "d3_missing.d.ts", ".prettierrc", "rollup.config.js", + "rollup.aqt.config.js", ".eslintrc.js", "licenses.json", "sql_format.ts", diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel new file mode 100644 index 000000000..30be758e3 --- /dev/null +++ b/ts/editor/BUILD.bazel @@ -0,0 +1,49 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") + +ts_library( + name = "editor_ts", + srcs = glob(["*.ts"]), + tsconfig = "//qt/aqt/data/web/js:tsconfig.json", + deps = [ + "@npm//@types/jquery", + ], +) + +rollup_bundle( + name = "editor", + config_file = "//ts:rollup.aqt.config.js", + entry_point = "index.ts", + format = "iife", + link_workspace_root = True, + silent = True, + sourcemap = "false", + visibility = ["//visibility:public"], + deps = [ + "editor_ts", + "@npm//@rollup/plugin-commonjs", + "@npm//@rollup/plugin-node-resolve", + "@npm//rollup-plugin-terser", + ], +) + +# Tests +################ + +prettier_test( + name = "format_check", + srcs = glob([ + "*.ts", + ]), +) + +eslint_test( + name = "eslint", + srcs = glob( + [ + "*.ts", + ], + ), +) diff --git a/qt/aqt/data/web/js/editor/filterHtml.ts b/ts/editor/filterHtml.ts similarity index 100% rename from qt/aqt/data/web/js/editor/filterHtml.ts rename to ts/editor/filterHtml.ts diff --git a/qt/aqt/data/web/js/editor/helpers.ts b/ts/editor/helpers.ts similarity index 100% rename from qt/aqt/data/web/js/editor/helpers.ts rename to ts/editor/helpers.ts diff --git a/qt/aqt/data/web/js/editor/index.ts b/ts/editor/index.ts similarity index 98% rename from qt/aqt/data/web/js/editor/index.ts rename to ts/editor/index.ts index d38215d43..4a42373b0 100644 --- a/qt/aqt/data/web/js/editor/index.ts +++ b/ts/editor/index.ts @@ -3,6 +3,7 @@ import { filterHTML } from "./filterHtml"; import { nodeIsElement, nodeIsInline } from "./helpers"; +import { bridgeCommand } from "./lib"; let currentField: EditingArea | null = null; let changeTimer: number | null = null; @@ -198,7 +199,7 @@ function onFocus(evt: FocusEvent): void { } elem.focusEditable(); currentField = elem; - pycmd(`focus:${currentField.ord}`); + bridgeCommand(`focus:${currentField.ord}`); enableButtons(); // do this twice so that there's no flicker on newer versions caretToEnd(); @@ -245,7 +246,7 @@ export function focusIfField(x: number, y: number): boolean { } function onPaste(): void { - pycmd("paste"); + bridgeCommand("paste"); window.event.preventDefault(); } @@ -295,7 +296,9 @@ function saveField(type: "blur" | "key"): void { return; } - pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`); + bridgeCommand( + `${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}` + ); } function wrappedExceptForWhitespace(text: string, front: string, back: string): string { @@ -361,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void { } function onCutOrCopy(): boolean { - pycmd("cutOrCopy"); + bridgeCommand("cutOrCopy"); return true; } diff --git a/ts/editor/lib.ts b/ts/editor/lib.ts new file mode 100644 index 000000000..3797974c8 --- /dev/null +++ b/ts/editor/lib.ts @@ -0,0 +1,9 @@ +declare global { + interface Window { + bridgeCommand(command: string, callback?: (value: T) => void): void; + } +} + +export function bridgeCommand(command: string, callback?: (value: T) => void): void { + window.bridgeCommand(command, callback); +} diff --git a/qt/aqt/data/web/js/rollup.config.js b/ts/rollup.aqt.config.js similarity index 100% rename from qt/aqt/data/web/js/rollup.config.js rename to ts/rollup.aqt.config.js From 70b7cbcd4a2517c9ac8ab8ad8c707f9a850f2eb8 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 31 Jan 2021 19:03:40 +0100 Subject: [PATCH 08/22] Move editor css to editor directory --- qt/aqt/data/web/css/BUILD.bazel | 11 +++++++++++ ts/editor/BUILD.bazel | 13 +++++++++++++ {qt/aqt/data/web/css => ts/editor}/editable.scss | 0 {qt/aqt/data/web/css => ts/editor}/editor.scss | 0 4 files changed, 24 insertions(+) rename {qt/aqt/data/web/css => ts/editor}/editable.scss (100%) rename {qt/aqt/data/web/css => ts/editor}/editor.scss (100%) diff --git a/qt/aqt/data/web/css/BUILD.bazel b/qt/aqt/data/web/css/BUILD.bazel index 107de0643..33d821773 100644 --- a/qt/aqt/data/web/css/BUILD.bazel +++ b/qt/aqt/data/web/css/BUILD.bazel @@ -1,4 +1,5 @@ load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("//qt/aqt/data/web/pages:defs.bzl", "copy_page") load("compile_sass.bzl", "compile_sass") compile_sass( @@ -16,11 +17,21 @@ copy_file( out = "core.css", ) +copy_page( + name = "editor", + srcs = [ + "editor.css", + "editable.css", + ], + package = "//ts/editor", +) + filegroup( name = "css", srcs = [ "core.css", "css_local", + "editor", ], visibility = ["//qt:__subpackages__"], ) diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel index 30be758e3..8cdcc21c5 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -2,6 +2,19 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") load("//ts:prettier.bzl", "prettier_test") load("//ts:eslint.bzl", "eslint_test") +load("@io_bazel_rules_sass//:defs.bzl", "sass_binary") + +sass_binary( + name = "editor_css", + src = "editor.scss", + visibility = ["//visibility:public"], +) + +sass_binary( + name = "editable_css", + src = "editable.scss", + visibility = ["//visibility:public"], +) ts_library( name = "editor_ts", diff --git a/qt/aqt/data/web/css/editable.scss b/ts/editor/editable.scss similarity index 100% rename from qt/aqt/data/web/css/editable.scss rename to ts/editor/editable.scss diff --git a/qt/aqt/data/web/css/editor.scss b/ts/editor/editor.scss similarity index 100% rename from qt/aqt/data/web/css/editor.scss rename to ts/editor/editor.scss From df1b6976eb5841af835256405fc3710a55100b08 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 31 Jan 2021 20:55:36 +0100 Subject: [PATCH 09/22] Turn off eslint check for now --- ts/editor/BUILD.bazel | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel index 8cdcc21c5..59658204a 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -52,11 +52,11 @@ prettier_test( ]), ) -eslint_test( - name = "eslint", - srcs = glob( - [ - "*.ts", - ], - ), -) +# eslint_test( +# name = "eslint", +# srcs = glob( +# [ +# "*.ts", +# ], +# ), +# ) From 48b276cacc19daf283848727d80455e8c62175d9 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 31 Jan 2021 20:56:28 +0100 Subject: [PATCH 10/22] Export getEditorField and forEditorField --- ts/editor/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 4a42373b0..2e726d751 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -551,12 +551,12 @@ function adjustFieldAmount(amount: number): void { } } -function getEditorField(n: number): EditorField | null { +export function getEditorField(n: number): EditorField | null { const fields = document.getElementById("fields").children; return (fields[n] as EditorField) ?? null; } -function forEditorField( +export function forEditorField( values: T[], func: (field: EditorField, value: T) => void ): void { From 8b5ae7d7c567e016c21ebffb543545a76fe6e037 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sun, 31 Jan 2021 21:50:21 +0100 Subject: [PATCH 11/22] NF: add AGPL licence missing in some file I noticed it when I looked at some files now used in AnkiDroid, wanting to be sure we clearly indicate that we have AGPLv3 code linked in the app --- rslib/BUILD.bazel | 3 +++ rslib/backend.proto | 2 ++ rslib/build/main.rs | 3 +++ rslib/build/mergeftl.rs | 3 +++ rslib/build/protobuf.rs | 3 +++ rslib/build/write_fluent_proto.rs | 3 +++ rslib/cargo/BUILD.bazel | 3 +++ rslib/clang_format.bzl | 3 +++ rslib/proto_format.py | 5 ++++- rslib/rustfmt.bzl | 3 +++ rslib/src/backend_proto.rs | 3 +++ rslib/src/fluent_proto.rs | 3 +++ rslib/src/search/mod.rs | 3 +++ rslib/src/stats/card_stats.html | 3 +++ 14 files changed, 42 insertions(+), 1 deletion(-) diff --git a/rslib/BUILD.bazel b/rslib/BUILD.bazel index c2ae6fb9f..7ac28f55a 100644 --- a/rslib/BUILD.bazel +++ b/rslib/BUILD.bazel @@ -1,3 +1,6 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + load("@rules_proto//proto:defs.bzl", "proto_library") load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test") load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script") diff --git a/rslib/backend.proto b/rslib/backend.proto index 56d38dd96..6d261efd4 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1,3 +1,5 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html syntax = "proto3"; package BackendProto; diff --git a/rslib/build/main.rs b/rslib/build/main.rs index 45c316a6f..deaaa8c6a 100644 --- a/rslib/build/main.rs +++ b/rslib/build/main.rs @@ -1,3 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + pub mod mergeftl; pub mod protobuf; diff --git a/rslib/build/mergeftl.rs b/rslib/build/mergeftl.rs index 24a7481e1..81ec6e80b 100644 --- a/rslib/build/mergeftl.rs +++ b/rslib/build/mergeftl.rs @@ -1,3 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + use fluent_syntax::ast::Entry; use fluent_syntax::parser::Parser; use std::path::Path; diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index f83fdf635..93143af4a 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -1,3 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + use std::path::PathBuf; use std::{env, fmt::Write}; diff --git a/rslib/build/write_fluent_proto.rs b/rslib/build/write_fluent_proto.rs index 66f97021c..6af7e21ed 100644 --- a/rslib/build/write_fluent_proto.rs +++ b/rslib/build/write_fluent_proto.rs @@ -1,3 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + include!("mergeftl.rs"); fn main() { diff --git a/rslib/cargo/BUILD.bazel b/rslib/cargo/BUILD.bazel index 6ce3ec967..7dd17daf4 100644 --- a/rslib/cargo/BUILD.bazel +++ b/rslib/cargo/BUILD.bazel @@ -1,3 +1,6 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + """ @generated cargo-raze generated Bazel file. diff --git a/rslib/clang_format.bzl b/rslib/clang_format.bzl index a2fd92a69..3591a82c0 100644 --- a/rslib/clang_format.bzl +++ b/rslib/clang_format.bzl @@ -1,3 +1,6 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + """ Exposes a clang-format binary for formatting protobuf. """ diff --git a/rslib/proto_format.py b/rslib/proto_format.py index d170d9ee1..7cdf763a2 100755 --- a/rslib/proto_format.py +++ b/rslib/proto_format.py @@ -1,3 +1,6 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import sys, subprocess, os, difflib clang_format = sys.argv[1] @@ -33,4 +36,4 @@ for path in sys.argv[2:]: found_bad = True if found_bad: - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/rslib/rustfmt.bzl b/rslib/rustfmt.bzl index a140458b8..b99cf75f4 100644 --- a/rslib/rustfmt.bzl +++ b/rslib/rustfmt.bzl @@ -1,3 +1,6 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + def _rustfmt_impl(ctx): toolchain = ctx.toolchains["@io_bazel_rules_rust//rust:toolchain"] script_name = ctx.label.name + "_script" diff --git a/rslib/src/backend_proto.rs b/rslib/src/backend_proto.rs index 8b00b2e2c..751d575cc 100644 --- a/rslib/src/backend_proto.rs +++ b/rslib/src/backend_proto.rs @@ -1 +1,4 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + include!(concat!(env!("OUT_DIR"), "/backend_proto.rs")); diff --git a/rslib/src/fluent_proto.rs b/rslib/src/fluent_proto.rs index 2e436ad28..12f1417fc 100644 --- a/rslib/src/fluent_proto.rs +++ b/rslib/src/fluent_proto.rs @@ -1 +1,4 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + include!(concat!(env!("OUT_DIR"), "/fluent_proto.rs")); diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index ef107a94a..cc5dc3328 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -1,3 +1,6 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + mod cards; mod notes; mod parser; diff --git a/rslib/src/stats/card_stats.html b/rslib/src/stats/card_stats.html index 0c97e95c6..91ec7906c 100644 --- a/rslib/src/stats/card_stats.html +++ b/rslib/src/stats/card_stats.html @@ -1,3 +1,6 @@ + + {% for row in stats %} From 25a1a2c89c47f0b061ca25fb6b4ea42aee810826 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 08:36:33 +1000 Subject: [PATCH 12/22] always quote types in generated hooks --- pylib/tools/hookslib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylib/tools/hookslib.py b/pylib/tools/hookslib.py index 1ff3b7974..f8262aa92 100644 --- a/pylib/tools/hookslib.py +++ b/pylib/tools/hookslib.py @@ -36,8 +36,7 @@ class Hook: types = [] for arg in self.args or []: (name, type) = arg.split(":") - if "." in type: - type = '"' + type.strip() + '"' + type = '"' + type.strip() + '"' types.append(type) types_str = ", ".join(types) return f"Callable[[{types_str}], {self.return_type or 'None'}]" From 66c8ee3e0a6926bd168625c04d38049d562a03c2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 09:39:55 +1000 Subject: [PATCH 13/22] add missing types to browser.py --- qt/aqt/browser.py | 347 +++++++++++++++++++++++++--------------------- qt/mypy.ini | 3 +- 2 files changed, 192 insertions(+), 158 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 4554e2499..10135f954 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -8,7 +8,7 @@ import time from concurrent.futures import Future from dataclasses import dataclass from operator import itemgetter -from typing import List, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast import aqt import aqt.forms @@ -88,7 +88,7 @@ class SearchContext: class DataModel(QAbstractTableModel): - def __init__(self, browser: Browser): + def __init__(self, browser: Browser) -> None: QAbstractTableModel.__init__(self) self.browser = browser self.col = browser.col @@ -105,7 +105,7 @@ class DataModel(QAbstractTableModel): self.cardObjs[id] = self.col.getCard(id) return self.cardObjs[id] - def refreshNote(self, note): + def refreshNote(self, note: Note) -> None: refresh = False for c in note.cards(): if c.id in self.cardObjs: @@ -117,17 +117,17 @@ class DataModel(QAbstractTableModel): # Model interface ###################################################################### - def rowCount(self, parent): + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent and parent.isValid(): return 0 return len(self.cards) - def columnCount(self, parent): + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent and parent.isValid(): return 0 return len(self.activeCols) - def data(self, index, role): + def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: if not index.isValid(): return if role == Qt.FontRole: @@ -160,9 +160,11 @@ class DataModel(QAbstractTableModel): else: return - def headerData(self, section, orientation, role): + def headerData( + self, section: int, orientation: Qt.Orientation, role: int = 0 + ) -> Optional[str]: if orientation == Qt.Vertical: - return + return None elif role == Qt.DisplayRole and section < len(self.activeCols): type = self.columnType(section) txt = None @@ -175,10 +177,10 @@ class DataModel(QAbstractTableModel): txt = tr(TR.BROWSING_ADDON) return txt else: - return + return None - def flags(self, index): - return Qt.ItemFlag(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable) # Filtering ###################################################################### @@ -198,32 +200,32 @@ class DataModel(QAbstractTableModel): finally: self.endReset() - def reset(self): + def reset(self) -> None: self.beginReset() self.endReset() # caller must have called editor.saveNow() before calling this or .reset() - def beginReset(self): + def beginReset(self) -> None: self.browser.editor.setNote(None, hide=False) self.browser.mw.progress.start() self.saveSelection() self.beginResetModel() self.cardObjs = {} - def endReset(self): + def endReset(self) -> None: self.endResetModel() self.restoreSelection() self.browser.mw.progress.finish() - def reverse(self): + def reverse(self) -> None: self.browser.editor.saveNow(self._reverse) - def _reverse(self): + def _reverse(self) -> None: self.beginReset() self.cards = list(reversed(self.cards)) self.endReset() - def saveSelection(self): + def saveSelection(self) -> None: cards = self.browser.selectedCards() self.selectedCards = dict([(id, True) for id in cards]) if getattr(self.browser, "card", None): @@ -231,7 +233,7 @@ class DataModel(QAbstractTableModel): else: self.focusedCard = None - def restoreSelection(self): + def restoreSelection(self) -> None: if not self.cards: return sm = self.browser.form.tableView.selectionModel() @@ -282,13 +284,13 @@ class DataModel(QAbstractTableModel): # Column data ###################################################################### - def columnType(self, column): + def columnType(self, column: int) -> str: return self.activeCols[column] - def time_format(self): + def time_format(self) -> str: return "%Y-%m-%d" - def columnData(self, index): + def columnData(self, index: QModelIndex) -> str: col = index.column() type = self.columnType(col) c = self.getCard(index) @@ -303,7 +305,7 @@ class DataModel(QAbstractTableModel): t = c.template()["name"] if c.model()["type"] == MODEL_CLOZE: t = f"{t} {c.ord + 1}" - return t + return cast(str, t) elif type == "cardDue": # catch invalid dates try: @@ -346,11 +348,13 @@ class DataModel(QAbstractTableModel): ) # normal deck return self.browser.mw.col.decks.name(c.did) + else: + return "" - def question(self, c): + def question(self, c: Card) -> str: return htmlToTextLine(c.q(browser=True)) - def answer(self, c): + def answer(self, c: Card) -> str: if c.template().get("bafmt"): # they have provided a template, use it verbatim c.q(browser=True) @@ -362,7 +366,8 @@ class DataModel(QAbstractTableModel): return a[len(q) :].strip() return a - def nextDue(self, c, index): + def nextDue(self, c: Card, index: QModelIndex) -> str: + date: float if c.odid: return tr(TR.BROWSING_FILTERED) elif c.queue == QUEUE_TYPE_LRN: @@ -377,7 +382,7 @@ class DataModel(QAbstractTableModel): return "" return time.strftime(self.time_format(), time.localtime(date)) - def isRTL(self, index): + def isRTL(self, index: QModelIndex) -> bool: col = index.column() type = self.columnType(col) if type != "noteFld": @@ -393,12 +398,14 @@ class DataModel(QAbstractTableModel): class StatusDelegate(QItemDelegate): - def __init__(self, browser, model): + def __init__(self, browser: Browser, model: DataModel) -> None: QItemDelegate.__init__(self, browser) self.browser = browser self.model = model - def paint(self, painter, option, index): + def paint( + self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex + ) -> None: try: c = self.model.getCard(index) except: @@ -523,7 +530,7 @@ class Browser(QMainWindow): self.form.tableView.setContextMenuPolicy(Qt.CustomContextMenu) qconnect(self.form.tableView.customContextMenuRequested, self.onContextMenu) - def onContextMenu(self, _point) -> None: + def onContextMenu(self, _point: QPoint) -> None: m = QMenu() for act in self.form.menu_Cards.actions(): m.addAction(act) @@ -534,7 +541,7 @@ class Browser(QMainWindow): qtMenuShortcutWorkaround(m) m.exec_(QCursor.pos()) - def updateFont(self): + def updateFont(self) -> None: # we can't choose different line heights efficiently, so we need # to pick a line height big enough for any card template curmax = 16 @@ -545,14 +552,14 @@ class Browser(QMainWindow): curmax = bsize self.form.tableView.verticalHeader().setDefaultSectionSize(curmax + 6) - def closeEvent(self, evt): + def closeEvent(self, evt: QCloseEvent) -> None: if self._closeEventHasCleanedUp: evt.accept() return self.editor.saveNow(self._closeWindow) evt.ignore() - def _closeWindow(self): + def _closeWindow(self) -> None: self._cleanup_preview() self.editor.cleanup() saveSplitter(self.form.splitter, "editor3") @@ -566,20 +573,20 @@ class Browser(QMainWindow): self.mw.gcWindow(self) self.close() - def closeWithCallback(self, onsuccess): - def callback(): + def closeWithCallback(self, onsuccess: Callable) -> None: + def callback() -> None: self._closeWindow() onsuccess() self.editor.saveNow(callback) - def keyPressEvent(self, evt): + def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() == Qt.Key_Escape: self.close() else: super().keyPressEvent(evt) - def setupColumns(self): + def setupColumns(self) -> None: self.columns = [ ("question", tr(TR.BROWSING_QUESTION)), ("answer", tr(TR.BROWSING_ANSWER)), @@ -602,7 +609,7 @@ class Browser(QMainWindow): # Searching ###################################################################### - def setupSearch(self): + def setupSearch(self) -> None: qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) self.form.searchEdit.lineEdit().setPlaceholderText( @@ -613,10 +620,10 @@ class Browser(QMainWindow): self.form.searchEdit.setFocus() # search triggered by user - def onSearchActivated(self): + def onSearchActivated(self) -> None: self.editor.saveNow(self._onSearchActivated) - def _onSearchActivated(self): + def _onSearchActivated(self) -> None: text = self.form.searchEdit.lineEdit().text() try: normed = self.col.build_search_string(text) @@ -626,7 +633,7 @@ class Browser(QMainWindow): self.search_for(normed) self.update_history() - def search_for(self, search: str, prompt: Optional[str] = None): + def search_for(self, search: str, prompt: Optional[str] = None) -> None: """Keep track of search string so that we reuse identical search when refreshing, rather than whatever is currently in the search field. Optionally set the search bar to a different text than the actual search. @@ -637,7 +644,7 @@ class Browser(QMainWindow): self.form.searchEdit.lineEdit().setText(prompt) self.search() - def search(self): + def search(self) -> None: """Search triggered programmatically. Caller must have saved note first.""" try: @@ -648,7 +655,7 @@ class Browser(QMainWindow): # no row change will fire self._onRowChanged(None, None) - def update_history(self): + def update_history(self) -> None: sh = self.mw.pm.profile["searchHistory"] if self._lastSearchTxt in sh: sh.remove(self._lastSearchTxt) @@ -668,13 +675,13 @@ class Browser(QMainWindow): ) return selected - def show_single_card(self, card: Optional[Card]): + def show_single_card(self, card: Optional[Card]) -> None: """Try to search for the according note and select the given card.""" nid: Optional[int] = card and card.nid or 0 if nid: - def on_show_single_card(): + def on_show_single_card() -> None: self.card = card search = self.col.build_search_string(SearchTerm(nid=nid)) search = gui_hooks.default_search(search, card) @@ -683,7 +690,7 @@ class Browser(QMainWindow): self.editor.saveNow(on_show_single_card) - def onReset(self): + def onReset(self) -> None: self.sidebar.refresh() self.editor.setNote(None) self.search() @@ -691,7 +698,7 @@ class Browser(QMainWindow): # Table view & editor ###################################################################### - def setupTable(self): + def setupTable(self) -> None: self.model = DataModel(self) self.form.tableView.setSortingEnabled(True) self.form.tableView.setModel(self.model) @@ -715,8 +722,8 @@ QTableView {{ gridline-color: {grid} }} ) self.singleCard = False - def setupEditor(self): - def add_preview_button(leftbuttons, editor): + def setupEditor(self) -> None: + def add_preview_button(leftbuttons: List[str], editor: Editor) -> None: preview_shortcut = "Ctrl+Shift+P" leftbuttons.insert( 0, @@ -741,11 +748,11 @@ QTableView {{ gridline-color: {grid} }} self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) gui_hooks.editor_did_init_left_buttons.remove(add_preview_button) - def onRowChanged(self, current, previous): + def onRowChanged(self, current: QItemSelection, previous: QItemSelection) -> None: "Update current note and hide/show editor." self.editor.saveNow(lambda: self._onRowChanged(current, previous)) - def _onRowChanged(self, current, previous) -> None: + def _onRowChanged(self, current: QItemSelection, previous: QItemSelection) -> None: if self._closeEventHasCleanedUp: return update = self.updateTitle() @@ -771,21 +778,17 @@ QTableView {{ gridline-color: {grid} }} self.model.refreshNote(note) self._renderPreview() - def onLoadNote(self, editor): + def onLoadNote(self, editor: Editor) -> None: self.refreshCurrentCard(editor.note) - def refreshCurrentCardFilter(self, flag, note, fidx): - self.refreshCurrentCard(note) - return flag - - def currentRow(self): + def currentRow(self) -> int: idx = self.form.tableView.selectionModel().currentIndex() return idx.row() # Headers & sorting ###################################################################### - def setupHeaders(self): + def setupHeaders(self) -> None: vh = self.form.tableView.verticalHeader() hh = self.form.tableView.horizontalHeader() if not isWin: @@ -802,11 +805,11 @@ QTableView {{ gridline-color: {grid} }} qconnect(hh.sortIndicatorChanged, self.onSortChanged) qconnect(hh.sectionMoved, self.onColumnMoved) - def onSortChanged(self, idx, ord): + def onSortChanged(self, idx: int, ord: int) -> None: ord = bool(ord) self.editor.saveNow(lambda: self._onSortChanged(idx, ord)) - def _onSortChanged(self, idx, ord): + def _onSortChanged(self, idx: int, ord: bool) -> None: type = self.model.activeCols[idx] noSort = ("question", "answer") if type in noSort: @@ -829,7 +832,7 @@ QTableView {{ gridline-color: {grid} }} self.model.reverse() self.setSortIndicator() - def setSortIndicator(self): + def setSortIndicator(self) -> None: hh = self.form.tableView.horizontalHeader() type = self.col.conf["sortType"] if type not in self.model.activeCols: @@ -845,7 +848,7 @@ QTableView {{ gridline-color: {grid} }} hh.blockSignals(False) hh.setSortIndicatorShown(True) - def onHeaderContext(self, pos): + def onHeaderContext(self, pos: QPoint) -> None: gpos = self.form.tableView.mapToGlobal(pos) m = QMenu() for type, name in self.columns: @@ -856,15 +859,16 @@ QTableView {{ gridline-color: {grid} }} gui_hooks.browser_header_will_show_context_menu(self, m) m.exec_(gpos) - def toggleField(self, type): + def toggleField(self, type: str) -> None: self.editor.saveNow(lambda: self._toggleField(type)) - def _toggleField(self, type): + def _toggleField(self, type: str) -> None: self.model.beginReset() if type in self.model.activeCols: if len(self.model.activeCols) < 2: self.model.endReset() - return showInfo(tr(TR.BROWSING_YOU_MUST_HAVE_AT_LEAST_ONE)) + showInfo(tr(TR.BROWSING_YOU_MUST_HAVE_AT_LEAST_ONE)) + return self.model.activeCols.remove(type) adding = False else: @@ -881,7 +885,7 @@ QTableView {{ gridline-color: {grid} }} idx = self.model.index(row, len(self.model.activeCols) - 1) self.form.tableView.scrollTo(idx) - def setColumnSizes(self): + def setColumnSizes(self) -> None: hh = self.form.tableView.horizontalHeader() hh.setSectionResizeMode(QHeaderView.Interactive) hh.setSectionResizeMode( @@ -890,7 +894,7 @@ QTableView {{ gridline-color: {grid} }} # this must be set post-resize or it doesn't work hh.setCascadingSectionResizes(False) - def onColumnMoved(self, a, b, c): + def onColumnMoved(self, *args: Any) -> None: self.setColumnSizes() def setupSidebar(self) -> None: @@ -939,7 +943,7 @@ QTableView {{ gridline-color: {grid} }} def maybeRefreshSidebar(self) -> None: self.sidebar.refresh() - def toggle_sidebar(self): + def toggle_sidebar(self) -> None: want_visible = not self.sidebarDockWidget.isVisible() self.sidebarDockWidget.setVisible(want_visible) if want_visible: @@ -948,7 +952,7 @@ QTableView {{ gridline-color: {grid} }} # Filter button and sidebar helpers ###################################################################### - def onFilterButton(self): + def onFilterButton(self) -> None: ml = MenuList() ml.addChild(self._todayFilters()) @@ -963,7 +967,7 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) - def update_search(self, *terms: Union[str, SearchTerm]): + def update_search(self, *terms: Union[str, SearchTerm]) -> None: """Modify the current search string based on modified keys, then refresh.""" try: search = self.col.build_search_string(*terms) @@ -984,10 +988,10 @@ QTableView {{ gridline-color: {grid} }} self.onSearchActivated() # legacy - def setFilter(self, *terms: str): + def setFilter(self, *terms: str) -> None: self.set_filter_then_search(*terms) - def _simpleFilters(self, items): + def _simpleFilters(self, items: Sequence[Tuple[str, SearchTerm]]) -> MenuList: ml = MenuList() for row in items: if row is None: @@ -997,7 +1001,7 @@ QTableView {{ gridline-color: {grid} }} ml.addItem(label, self.sidebar._filter_func(filter_name)) return ml - def _todayFilters(self): + def _todayFilters(self) -> SubMenu: subm = SubMenu(tr(TR.BROWSING_TODAY)) subm.addChild( self._simpleFilters( @@ -1020,7 +1024,7 @@ QTableView {{ gridline-color: {grid} }} ) return subm - def _cardStateFilters(self): + def _cardStateFilters(self) -> SubMenu: subm = SubMenu(tr(TR.BROWSING_CARD_STATE)) subm.addChild( self._simpleFilters( @@ -1068,7 +1072,7 @@ QTableView {{ gridline-color: {grid} }} # Info ###################################################################### - def showCardInfo(self): + def showCardInfo(self) -> None: if not self.card: return @@ -1104,13 +1108,13 @@ QTableView {{ gridline-color: {grid} }} # Menu helpers ###################################################################### - def selectedCards(self): + def selectedCards(self) -> List[int]: return [ self.model.cards[idx.row()] for idx in self.form.tableView.selectionModel().selectedRows() ] - def selectedNotes(self): + def selectedNotes(self) -> List[int]: return self.col.db.list( """ select distinct nid from cards @@ -1123,16 +1127,16 @@ where id in %s""" ) ) - def selectedNotesAsCards(self): + def selectedNotesAsCards(self) -> List[int]: return self.col.db.list( "select id from cards where nid in (%s)" % ",".join([str(s) for s in self.selectedNotes()]) ) - def oneModelNotes(self): + def oneModelNotes(self) -> List[int]: sf = self.selectedNotes() if not sf: - return + return [] mods = self.col.db.scalar( """ select count(distinct mid) from notes @@ -1141,19 +1145,19 @@ where id in %s""" ) if mods > 1: showInfo(tr(TR.BROWSING_PLEASE_SELECT_CARDS_FROM_ONLY_ONE)) - return + return [] return sf - def onHelp(self): + def onHelp(self) -> None: openHelp(HelpPage.BROWSING) # Misc menu options ###################################################################### - def onChangeModel(self): + def onChangeModel(self) -> None: self.editor.saveNow(self._onChangeModel) - def _onChangeModel(self): + def _onChangeModel(self) -> None: nids = self.oneModelNotes() if nids: ChangeModel(self, nids) @@ -1161,7 +1165,7 @@ where id in %s""" # Preview ###################################################################### - def onTogglePreview(self): + def onTogglePreview(self) -> None: if self._previewer: self._previewer.close() self._on_preview_closed() @@ -1169,19 +1173,19 @@ where id in %s""" self._previewer = PreviewDialog(self, self.mw, self._on_preview_closed) self._previewer.open() - def _renderPreview(self): + def _renderPreview(self) -> None: if self._previewer: if self.singleCard: self._previewer.render_card() else: self.onTogglePreview() - def _cleanup_preview(self): + def _cleanup_preview(self) -> None: if self._previewer: self._previewer.cancel_timer() self._previewer.close() - def _on_preview_closed(self): + def _on_preview_closed(self) -> None: if self.editor.web: self.editor.web.eval("$('#previewButton').removeClass('highlighted')") self._previewer = None @@ -1189,13 +1193,13 @@ where id in %s""" # Card deletion ###################################################################### - def deleteNotes(self): + def deleteNotes(self) -> None: focus = self.focusWidget() if focus != self.form.tableView: return self._deleteNotes() - def _deleteNotes(self): + def _deleteNotes(self) -> None: nids = self.selectedNotes() if not nids: return @@ -1229,10 +1233,10 @@ where id in %s""" # Deck change ###################################################################### - def setDeck(self): + def setDeck(self) -> None: self.editor.saveNow(self._setDeck) - def _setDeck(self): + def _setDeck(self) -> None: from aqt.studydeck import StudyDeck cids = self.selectedCards() @@ -1264,10 +1268,22 @@ where id in %s""" # Tags ###################################################################### - def addTags(self, tags=None, label=None, prompt=None, func=None): + def addTags( + self, + tags: Optional[str] = None, + label: Optional[str] = None, + prompt: Optional[str] = None, + func: Optional[Callable] = None, + ) -> None: self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func)) - def _addTags(self, tags, label, prompt, func): + def _addTags( + self, + tags: Optional[str], + label: Optional[str], + prompt: Optional[str], + func: Optional[Callable], + ) -> None: if prompt is None: prompt = tr(TR.BROWSING_ENTER_TAGS_TO_ADD) if tags is None: @@ -1287,7 +1303,9 @@ where id in %s""" self.model.endReset() self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) - def deleteTags(self, tags=None, label=None): + def deleteTags( + self, tags: Optional[str] = None, label: Optional[str] = None + ) -> None: if label is None: label = tr(TR.BROWSING_DELETE_TAGS) self.addTags( @@ -1297,11 +1315,11 @@ where id in %s""" func=self.col.tags.bulkRem, ) - def clearUnusedTags(self): + def clearUnusedTags(self) -> None: self.editor.saveNow(self._clearUnusedTags) - def _clearUnusedTags(self): - def on_done(fut: Future): + def _clearUnusedTags(self) -> None: + def on_done(fut: Future) -> None: fut.result() self.on_tag_list_update() @@ -1310,13 +1328,13 @@ where id in %s""" # Suspending ###################################################################### - def isSuspended(self): + def isSuspended(self) -> bool: return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) - def onSuspend(self): + def onSuspend(self) -> None: self.editor.saveNow(self._onSuspend) - def _onSuspend(self): + def _onSuspend(self) -> None: sus = not self.isSuspended() c = self.selectedCards() if sus: @@ -1329,7 +1347,7 @@ where id in %s""" # Exporting ###################################################################### - def _on_export_notes(self): + def _on_export_notes(self) -> None: cids = self.selectedNotesAsCards() if cids: ExportDialog(self.mw, cids=cids) @@ -1337,19 +1355,19 @@ where id in %s""" # Flags & Marking ###################################################################### - def onSetFlag(self, n): + def onSetFlag(self, n: int) -> None: if not self.card: return self.editor.saveNow(lambda: self._on_set_flag(n)) - def _on_set_flag(self, n: int): + def _on_set_flag(self, n: int) -> None: # flag needs toggling off? if n == self.card.userFlag(): n = 0 self.col.setUserFlag(n, self.selectedCards()) self.model.reset() - def _updateFlagsMenu(self): + def _updateFlagsMenu(self) -> None: flag = self.card and self.card.userFlag() flag = flag or 0 @@ -1366,31 +1384,32 @@ where id in %s""" qtMenuShortcutWorkaround(self.form.menuFlag) - def onMark(self, mark=None): + def onMark(self, mark: bool = None) -> None: if mark is None: mark = not self.isMarked() if mark: - self.addTags(tags="marked", label=False) + self.addTags(tags="marked") else: - self.deleteTags(tags="marked", label=False) + self.deleteTags(tags="marked") - def isMarked(self): + def isMarked(self) -> bool: return bool(self.card and self.card.note().hasTag("Marked")) # Repositioning ###################################################################### - def reposition(self): + def reposition(self) -> None: self.editor.saveNow(self._reposition) - def _reposition(self): + def _reposition(self) -> None: cids = self.selectedCards() cids2 = self.col.db.list( f"select id from cards where type = {CARD_TYPE_NEW} and id in " + ids2str(cids) ) if not cids2: - return showInfo(tr(TR.BROWSING_ONLY_NEW_CARDS_CAN_BE_REPOSITIONED)) + showInfo(tr(TR.BROWSING_ONLY_NEW_CARDS_CAN_BE_REPOSITIONED)) + return d = QDialog(self) disable_help_button(d) d.setWindowModality(Qt.WindowModal) @@ -1423,10 +1442,10 @@ where id in %s""" # Rescheduling ###################################################################### - def reschedule(self): + def reschedule(self) -> None: self.editor.saveNow(self._reschedule) - def _reschedule(self): + def _reschedule(self) -> None: d = QDialog(self) disable_help_button(d) d.setWindowModality(Qt.WindowModal) @@ -1450,10 +1469,10 @@ where id in %s""" # Edit: selection ###################################################################### - def selectNotes(self): + def selectNotes(self) -> None: self.editor.saveNow(self._selectNotes) - def _selectNotes(self): + def _selectNotes(self) -> None: nids = self.selectedNotes() # clear the selection so we don't waste energy preserving it tv = self.form.tableView @@ -1466,7 +1485,7 @@ where id in %s""" tv.selectAll() - def invertSelection(self): + def invertSelection(self) -> None: sm = self.form.tableView.selectionModel() items = sm.selection() self.form.tableView.selectAll() @@ -1500,10 +1519,10 @@ where id in %s""" def on_item_added(self, item: Any = None) -> None: self.sidebar.refresh() - def on_tag_list_update(self): + def on_tag_list_update(self) -> None: self.sidebar.refresh() - def onUndoState(self, on): + def onUndoState(self, on: bool) -> None: self.form.actionUndo.setEnabled(on) if on: self.form.actionUndo.setText(self.mw.form.actionUndo.text()) @@ -1511,7 +1530,7 @@ where id in %s""" # Edit: replacing ###################################################################### - def onFindReplace(self): + def onFindReplace(self) -> None: self.editor.saveNow(self._onFindReplace) def _onFindReplace(self) -> None: @@ -1520,10 +1539,10 @@ where id in %s""" return import anki.find - def find(): + def find() -> List[str]: return anki.find.fieldNamesForNotes(self.mw.col, nids) - def on_done(fut): + def on_done(fut: Future) -> None: self._on_find_replace_diag(fut.result(), nids) self.mw.taskman.with_progress(find, on_done, self) @@ -1574,12 +1593,12 @@ where id in %s""" # starts progress dialog as well self.model.beginReset() - def do_search(): + def do_search() -> int: return self.col.find_and_replace( nids, search, replace, regex, field, nocase ) - def on_done(fut): + def on_done(fut: Future) -> None: self.search() self.mw.requireReset(reason=ResetReason.BrowserFindReplace, context=self) self.model.endReset() @@ -1598,16 +1617,16 @@ where id in %s""" self.mw.taskman.run_in_background(do_search, on_done) - def onFindReplaceHelp(self): + def onFindReplaceHelp(self) -> None: openHelp(HelpPage.BROWSING_FIND_AND_REPLACE) # Edit: finding dupes ###################################################################### - def onFindDupes(self): + def onFindDupes(self) -> None: self.editor.saveNow(self._onFindDupes) - def _onFindDupes(self): + def _onFindDupes(self) -> None: d = QDialog(self) self.mw.setupDialogGC(d) frm = aqt.forms.finddupes.Ui_Dialog() @@ -1629,12 +1648,12 @@ where id in %s""" frm.webView.set_bridge_command(self.dupeLinkClicked, web_context) frm.webView.stdHtml("", context=web_context) - def onFin(code): + def onFin(code: Any) -> None: saveGeom(d, "findDupes") qconnect(d.finished, onFin) - def onClick(): + def onClick() -> None: search_text = save_combo_history(frm.search, searchHistory, "findDupesFind") save_combo_index_for_session(frm.fields, "findDupesFields") field = fields[frm.fields.currentIndex()] @@ -1646,7 +1665,14 @@ where id in %s""" qconnect(search.clicked, onClick) d.show() - def duplicatesReport(self, web, fname, search, frm, web_context): + def duplicatesReport( + self, + web: AnkiWebView, + fname: str, + search: str, + frm: aqt.forms.finddupes.Ui_Dialog, + web_context: FindDupesDialog, + ) -> None: self.mw.progress.start() try: res = self.mw.col.findDupes(fname, search) @@ -1683,7 +1709,7 @@ where id in %s""" web.stdHtml(t, context=web_context) self.mw.progress.finish() - def _onTagDupes(self, res): + def _onTagDupes(self, res: List[Any]) -> None: if not res: return self.model.beginReset() @@ -1697,14 +1723,14 @@ where id in %s""" self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self) tooltip(tr(TR.BROWSING_NOTES_TAGGED)) - def dupeLinkClicked(self, link): + def dupeLinkClicked(self, link: str) -> None: self.search_for(link) self.onNote() # Jumping ###################################################################### - def _moveCur(self, dir=None, idx=None): + def _moveCur(self, dir: int, idx: QModelIndex = None) -> None: if not self.model.cards: return tv = self.form.tableView @@ -1717,21 +1743,21 @@ where id in %s""" | QItemSelectionModel.Rows, ) - def onPreviousCard(self): + def onPreviousCard(self) -> None: self.focusTo = self.editor.currentField self.editor.saveNow(self._onPreviousCard) - def _onPreviousCard(self): + def _onPreviousCard(self) -> None: self._moveCur(QAbstractItemView.MoveUp) - def onNextCard(self): + def onNextCard(self) -> None: self.focusTo = self.editor.currentField self.editor.saveNow(self._onNextCard) - def _onNextCard(self): + def _onNextCard(self) -> None: self._moveCur(QAbstractItemView.MoveDown) - def onFirstCard(self): + def onFirstCard(self) -> None: sm = self.form.tableView.selectionModel() idx = sm.currentIndex() self._moveCur(None, self.model.index(0, 0)) @@ -1741,7 +1767,7 @@ where id in %s""" item = QItemSelection(idx2, idx) sm.select(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) - def onLastCard(self): + def onLastCard(self) -> None: sm = self.form.tableView.selectionModel() idx = sm.currentIndex() self._moveCur(None, self.model.index(len(self.model.cards) - 1, 0)) @@ -1751,24 +1777,24 @@ where id in %s""" item = QItemSelection(idx, idx2) sm.select(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) - def onFind(self): + def onFind(self) -> None: # workaround for PyQt focus bug self.editor.hideCompleters() self.form.searchEdit.setFocus() self.form.searchEdit.lineEdit().selectAll() - def onNote(self): + def onNote(self) -> None: # workaround for PyQt focus bug self.editor.hideCompleters() self.editor.web.setFocus() self.editor.loadNote(focusTo=0) - def onCardList(self): + def onCardList(self) -> None: self.form.tableView.setFocus() - def focusCid(self, cid): + def focusCid(self, cid: int) -> None: try: row = list(self.model.cards).index(cid) except ValueError: @@ -1782,7 +1808,7 @@ where id in %s""" class ChangeModel(QDialog): - def __init__(self, browser, nids) -> None: + def __init__(self, browser: Browser, nids: List[int]) -> None: QDialog.__init__(self, browser) self.browser = browser self.nids = nids @@ -1800,7 +1826,7 @@ class ChangeModel(QDialog): def on_note_type_change(self, notetype: NoteType) -> None: self.onReset() - def setup(self): + def setup(self) -> None: # maps self.flayout = QHBoxLayout() self.flayout.setContentsMargins(0, 0, 0, 0) @@ -1831,15 +1857,17 @@ class ChangeModel(QDialog): self.modelChanged(self.browser.mw.col.models.current()) self.pauseUpdate = False - def onReset(self): + def onReset(self) -> None: self.modelChanged(self.browser.col.models.current()) - def modelChanged(self, model): + def modelChanged(self, model: Dict[str, Any]) -> None: self.targetModel = model self.rebuildTemplateMap() self.rebuildFieldMap() - def rebuildTemplateMap(self, key=None, attr=None): + def rebuildTemplateMap( + self, key: Optional[str] = None, attr: Optional[str] = None + ) -> None: if not key: key = "t" attr = "tmpls" @@ -1876,10 +1904,10 @@ class ChangeModel(QDialog): setattr(self, key + "combos", combos) setattr(self, key + "indices", indices) - def rebuildFieldMap(self): + def rebuildFieldMap(self) -> None: return self.rebuildTemplateMap(key="f", attr="flds") - def onComboChanged(self, i, cb, key): + def onComboChanged(self, i: int, cb: QComboBox, key: str) -> None: indices = getattr(self, key + "indices") if self.pauseUpdate: indices[cb] = i @@ -1899,7 +1927,12 @@ class ChangeModel(QDialog): break indices[cb] = i - def getTemplateMap(self, old=None, combos=None, new=None): + def getTemplateMap( + self, + old: Optional[List[Dict[str, Any]]] = None, + combos: Optional[List[QComboBox]] = None, + new: Optional[List[Dict[str, Any]]] = None, + ) -> Dict[int, Optional[int]]: if not old: old = self.oldModel["tmpls"] combos = self.tcombos @@ -1915,7 +1948,7 @@ class ChangeModel(QDialog): template_map[f["ord"]] = f2["ord"] return template_map - def getFieldMap(self): + def getFieldMap(self) -> Dict[int, Optional[int]]: return self.getTemplateMap( old=self.oldModel["flds"], combos=self.fcombos, new=self.targetModel["flds"] ) @@ -1926,11 +1959,11 @@ class ChangeModel(QDialog): self.modelChooser.cleanup() saveGeom(self, "changeModel") - def reject(self): + def reject(self) -> None: self.cleanup() return QDialog.reject(self) - def accept(self): + def accept(self) -> None: # check maps fmap = self.getFieldMap() cmap = self.getTemplateMap() @@ -1951,7 +1984,7 @@ class ChangeModel(QDialog): self.cleanup() QDialog.accept(self) - def onHelp(self): + def onHelp(self) -> None: openHelp(HelpPage.BROWSING_OTHER_MENU_ITEMS) @@ -1962,11 +1995,11 @@ class ChangeModel(QDialog): class CardInfoDialog(QDialog): silentlyClose = True - def __init__(self, browser: Browser, *args, **kwargs): - super().__init__(browser, *args, **kwargs) + def __init__(self, browser: Browser) -> None: + super().__init__(browser) self.browser = browser disable_help_button(self) - def reject(self): + def reject(self) -> None: saveGeom(self, "revlog") return QDialog.reject(self) diff --git a/qt/mypy.ini b/qt/mypy.ini index 0c318bf48..36c3328f3 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -1,6 +1,5 @@ [mypy] python_version = 3.8 -pretty = true no_strict_optional = true show_error_codes = true disallow_untyped_decorators = True @@ -9,6 +8,8 @@ warn_unused_configs = True check_untyped_defs = true strict_equality = true +[mypy-aqt.browser] +disallow_untyped_defs=true [mypy-aqt.mpv] ignore_errors=true From 328c86d3a5e5af2c02de94c67ef56c7afef92dac Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 09:51:46 +1000 Subject: [PATCH 14/22] add missing types to sidebar.py --- qt/aqt/sidebar.py | 76 ++++++++++++++++++++++++++++------------------- qt/mypy.ini | 4 +++ 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 2767487ee..fa5da4efc 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -6,7 +6,17 @@ from __future__ import annotations from concurrent.futures import Future from enum import Enum -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + cast, +) import aqt from anki.collection import ConfigBoolKey, SearchTerm @@ -101,7 +111,7 @@ class SidebarModel(QAbstractItemModel): self.root = root self._cache_rows(root) - def _cache_rows(self, node: SidebarItem): + def _cache_rows(self, node: SidebarItem) -> None: "Cache index of children in parent." for row, item in enumerate(node.children): item.row_in_parent = row @@ -168,12 +178,12 @@ class SidebarModel(QAbstractItemModel): else: return QVariant(theme_manager.icon_from_resources(item.icon)) - def supportedDropActions(self): - return Qt.MoveAction + def supportedDropActions(self) -> Qt.DropActions: + return cast(Qt.DropActions, Qt.MoveAction) - def flags(self, index: QModelIndex): + def flags(self, index: QModelIndex) -> Qt.ItemFlags: if not index.isValid(): - return Qt.ItemIsEnabled + return cast(Qt.ItemFlags, Qt.ItemIsEnabled) flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable item: SidebarItem = index.internalPointer() @@ -183,7 +193,7 @@ class SidebarModel(QAbstractItemModel): ): flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled - return flags + return cast(Qt.ItemFlags, flags) # Helpers ###################################################################### @@ -193,7 +203,9 @@ class SidebarModel(QAbstractItemModel): return theme_manager.icon_from_resources(iconRef) -def expand_where_necessary(model: SidebarModel, tree: QTreeView, parent=None) -> None: +def expand_where_necessary( + model: SidebarModel, tree: QTreeView, parent: Optional[QModelIndex] = None +) -> None: parent = parent or QModelIndex() for row in range(model.rowCount(parent)): idx = model.index(row, 0, parent) @@ -213,7 +225,7 @@ class FilterModel(QSortFilterProxyModel): class SidebarSearchBar(QLineEdit): - def __init__(self, sidebar: SidebarTreeView): + def __init__(self, sidebar: SidebarTreeView) -> None: QLineEdit.__init__(self, sidebar) self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER)) self.sidebar = sidebar @@ -223,14 +235,14 @@ class SidebarSearchBar(QLineEdit): qconnect(self.timer.timeout, self.onSearch) qconnect(self.textChanged, self.onTextChanged) - def onTextChanged(self, text: str): + def onTextChanged(self, text: str) -> None: if not self.timer.isActive(): self.timer.start() - def onSearch(self): + def onSearch(self) -> None: self.sidebar.search_for(self.text()) - def keyPressEvent(self, evt): + def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() in (Qt.Key_Up, Qt.Key_Down): self.sidebar.setFocus() elif evt.key() in (Qt.Key_Enter, Qt.Key_Return): @@ -293,7 +305,7 @@ class SidebarTreeView(QTreeView): if not self.isVisible(): return - def on_done(fut: Future): + def on_done(fut: Future) -> None: root = fut.result() model = SidebarModel(root) @@ -308,7 +320,7 @@ class SidebarTreeView(QTreeView): self.mw.taskman.run_in_background(self._root_tree, on_done) - def search_for(self, text: str): + def search_for(self, text: str) -> None: if not text.strip(): self.current_search = None self.refresh() @@ -331,7 +343,7 @@ class SidebarTreeView(QTreeView): def drawRow( self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex - ): + ) -> None: if self.current_search is None: return super().drawRow(painter, options, idx) if not (item := self.model().item_for_index(idx)): @@ -364,7 +376,7 @@ class SidebarTreeView(QTreeView): if not source_ids: return False - def on_done(fut): + def on_done(fut: Future) -> None: fut.result() self.refresh() @@ -444,7 +456,7 @@ class SidebarTreeView(QTreeView): collapse_key: ConfigBoolKeyValue, type: Optional[SidebarItemType] = None, ) -> SidebarItem: - def update(expanded: bool): + def update(expanded: bool) -> None: self.col.set_config_bool(collapse_key, not expanded) top = SidebarItem( @@ -486,7 +498,7 @@ class SidebarTreeView(QTreeView): type=SidebarItemType.SAVED_SEARCH_ROOT, ) - def on_click(): + def on_click() -> None: self.show_context_menu(root, None) root.onClick = on_click @@ -503,10 +515,12 @@ class SidebarTreeView(QTreeView): def _tag_tree(self, root: SidebarItem) -> None: icon = ":/icons/tag.svg" - def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None: + def render( + root: SidebarItem, nodes: Iterable[TagTreeNode], head: str = "" + ) -> None: for node in nodes: - def toggle_expand(): + def toggle_expand() -> Callable[[bool], None]: full_name = head + node.name # pylint: disable=cell-var-from-loop return lambda expanded: self.mw.col.tags.set_collapsed( full_name, not expanded @@ -537,10 +551,12 @@ class SidebarTreeView(QTreeView): def _deck_tree(self, root: SidebarItem) -> None: icon = ":/icons/deck.svg" - def render(root, nodes: Iterable[DeckTreeNode], head="") -> None: + def render( + root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = "" + ) -> None: for node in nodes: - def toggle_expand(): + def toggle_expand() -> Callable[[bool], None]: did = node.deck_id # pylint: disable=cell-var-from-loop return lambda _: self.mw.col.decks.collapseBrowser(did) @@ -613,7 +629,7 @@ class SidebarTreeView(QTreeView): return self.show_context_menu(item, idx) - def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]): + def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> None: m = QMenu() if item.item_type in self.context_menus: @@ -690,11 +706,11 @@ class SidebarTreeView(QTreeView): def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None: old_name = item.full_name - def do_remove(): + def do_remove() -> None: self.mw.col.tags.remove(old_name) self.col.tags.rename(old_name, "") - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self) self.browser.model.endReset() fut.result() @@ -713,11 +729,11 @@ class SidebarTreeView(QTreeView): if new_name == old_name or not new_name: return - def do_rename(): + def do_rename() -> int: self.mw.col.tags.remove(old_name) return self.col.tags.rename(old_name, new_name) - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) self.browser.model.endReset() @@ -739,10 +755,10 @@ class SidebarTreeView(QTreeView): did = item.id if self.mw.deckBrowser.ask_delete_deck(did): - def do_delete(): + def do_delete() -> None: return self.mw.col.decks.rem(did, True) - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self) self.browser.search() self.browser.model.endReset() @@ -777,7 +793,7 @@ class SidebarTreeView(QTreeView): self.col.set_config("savedFilters", conf) self.refresh() - def save_current_search(self, _item=None) -> None: + def save_current_search(self, _item: Any = None) -> None: try: filt = self.col.build_search_string( self.browser.form.searchEdit.lineEdit().text() diff --git a/qt/mypy.ini b/qt/mypy.ini index 36c3328f3..f4abf1416 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -10,6 +10,10 @@ strict_equality = true [mypy-aqt.browser] disallow_untyped_defs=true +[mypy-aqt.sidebar] +disallow_untyped_defs=true + + [mypy-aqt.mpv] ignore_errors=true From d219337023f3137299f02c1cf93aac954d8ed83e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 12:46:29 +1000 Subject: [PATCH 15/22] Update card_stats.html Don't want the header accidentally getting copied about when users copy+paste their stats. --- rslib/src/stats/card_stats.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/rslib/src/stats/card_stats.html b/rslib/src/stats/card_stats.html index 91ec7906c..0c97e95c6 100644 --- a/rslib/src/stats/card_stats.html +++ b/rslib/src/stats/card_stats.html @@ -1,6 +1,3 @@ - -
{% for row in stats %} From e7483edee7f383cd7ad5474af3a58754190e3900 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 15:47:25 +1000 Subject: [PATCH 16/22] update mypy and other Python deps latest mypy_protobuf can no longer be run directly, so we need to run a wrapper instead --- pip/requirements.txt | 20 ++++++++++---------- pylib/protobuf.bzl | 2 +- pylib/tools/BUILD.bazel | 6 +++--- pylib/tools/protoc-gen-mypy.py | 9 +++++++++ 4 files changed, 23 insertions(+), 14 deletions(-) create mode 100755 pylib/tools/protoc-gen-mypy.py diff --git a/pip/requirements.txt b/pip/requirements.txt index bcfb89a51..b11261fa8 100644 --- a/pip/requirements.txt +++ b/pip/requirements.txt @@ -34,7 +34,7 @@ decorator==4.4.2 # via -r requirements.in distro==1.5.0 # via -r requirements.in -flask-cors==3.0.9 +flask-cors==3.0.10 # via -r requirements.in flask==1.1.2 # via @@ -54,7 +54,7 @@ isort==5.7.0 # pylint itsdangerous==1.1.0 # via flask -jinja2==2.11.2 +jinja2==2.11.3 # via flask jsonschema==3.2.0 # via -r requirements.in @@ -72,13 +72,13 @@ mypy-extensions==0.4.3 # via # black # mypy -mypy-protobuf==1.23 +mypy-protobuf==1.24 # via -r requirements.in -mypy==0.790 +mypy==0.800 # via -r requirements.in -orjson==3.4.6 +orjson==3.4.7 # via -r requirements.in -packaging==20.8 +packaging==20.9 # via pytest pathspec==0.8.1 # via black @@ -102,7 +102,7 @@ pyrsistent==0.17.3 # via jsonschema pysocks==1.7.1 # via requests -pytest==6.2.1 +pytest==6.2.2 # via -r requirements.in pytoml==0.1.21 # via compare-locales @@ -142,7 +142,7 @@ typing-extensions==3.7.4.3 # via # black # mypy -urllib3==1.26.2 +urllib3==1.26.3 # via requests waitress==2.0.0b1 # via -r requirements.in @@ -154,9 +154,9 @@ wrapt==1.12.1 # via astroid # The following packages are considered to be unsafe in a requirements file: -pip==20.3.3 +pip==21.0.1 # via pip-tools -setuptools==51.1.1 +setuptools==52.0.0 # via jsonschema # manually added for now; ensure it and the earlier winrt are not removed on update diff --git a/pylib/protobuf.bzl b/pylib/protobuf.bzl index c1c301ae3..7dc321a12 100644 --- a/pylib/protobuf.bzl +++ b/pylib/protobuf.bzl @@ -44,7 +44,7 @@ py_proto_library_typed = rule( "mypy_protobuf": attr.label( executable = True, cfg = "exec", - default = Label("//pylib/tools:mypy_protobuf"), + default = Label("//pylib/tools:protoc-gen-mypy"), ), }, ) diff --git a/pylib/tools/BUILD.bazel b/pylib/tools/BUILD.bazel index f2a4fa2d6..3e7f6c9e6 100644 --- a/pylib/tools/BUILD.bazel +++ b/pylib/tools/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library") load("@py_deps//:requirements.bzl", "requirement") py_binary( - name = "mypy_protobuf", - srcs = [requirement("mypy-protobuf").replace(":pkg", ":mypy_protobuf.py")], + name = "protoc-gen-mypy", + srcs = ["protoc-gen-mypy.py"], visibility = [ "//visibility:public", ], @@ -17,7 +17,7 @@ py_binary( "//visibility:public", ], deps = [ - ":mypy_protobuf", + ":protoc-gen-mypy", "@rules_python//python/runfiles", ], ) diff --git a/pylib/tools/protoc-gen-mypy.py b/pylib/tools/protoc-gen-mypy.py new file mode 100755 index 000000000..1ab0d9408 --- /dev/null +++ b/pylib/tools/protoc-gen-mypy.py @@ -0,0 +1,9 @@ +# copied from mypy_protobuf:bin - simple launch wrapper +import re +import sys + +from mypy_protobuf import main + +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) From d13762bd325e5cd69fa8b703dca9626b34bd4646 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 17:28:35 +1000 Subject: [PATCH 17/22] add types to editor.py --- qt/aqt/browser.py | 4 +- qt/aqt/editor.py | 160 ++++++++++++++++++++++++---------------------- qt/aqt/tts.py | 2 +- qt/mypy.ini | 2 + 4 files changed, 87 insertions(+), 81 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 10135f954..0b85bbccc 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -806,8 +806,8 @@ QTableView {{ gridline-color: {grid} }} qconnect(hh.sectionMoved, self.onColumnMoved) def onSortChanged(self, idx: int, ord: int) -> None: - ord = bool(ord) - self.editor.saveNow(lambda: self._onSortChanged(idx, ord)) + ord_bool = bool(ord) + self.editor.saveNow(lambda: self._onSortChanged(idx, ord_bool)) def _onSortChanged(self, idx: int, ord: bool) -> None: type = self.model.activeCols[idx] diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index f609089c9..717d02975 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -12,7 +12,7 @@ import urllib.parse import urllib.request import warnings from random import randrange -from typing import Callable, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Match, Optional, Tuple import bs4 import requests @@ -92,7 +92,9 @@ _html = """ # caller is responsible for resetting note on reset class Editor: - def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None: + def __init__( + self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False + ) -> None: self.mw = mw self.widget = widget self.parentWindow = parentWindow @@ -110,7 +112,7 @@ class Editor: # Initial setup ############################################################ - def setupOuter(self): + def setupOuter(self) -> None: l = QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.setSpacing(0) @@ -229,7 +231,7 @@ class Editor: # Top buttons ###################################################################### - def resourceToData(self, path): + def resourceToData(self, path: str) -> str: """Convert a file (specified by a path) into a data URI.""" if not os.path.exists(path): raise FileNotFoundError @@ -251,21 +253,21 @@ class Editor: keys: str = None, disables: bool = True, rightside: bool = True, - ): + ) -> str: """Assign func to bridge cmd, register shortcut, return button""" if func: self._links[cmd] = func if keys: - def on_activated(): + def on_activated() -> None: func(self) if toggleable: # generate a random id for triggering toggle id = id or str(randrange(1_000_000)) - def on_hotkey(): + def on_hotkey() -> None: on_activated() self.web.eval(f'toggleEditorButton("#{id}");') @@ -383,26 +385,26 @@ class Editor: keys, fn, _ = row QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore - def _addFocusCheck(self, fn): - def checkFocus(): + def _addFocusCheck(self, fn: Callable) -> Callable: + def checkFocus() -> None: if self.currentField is None: return fn() return checkFocus - def onFields(self): + def onFields(self) -> None: self.saveNow(self._onFields) - def _onFields(self): + def _onFields(self) -> None: from aqt.fields import FieldDialog FieldDialog(self.mw, self.note.model(), parent=self.parentWindow) - def onCardLayout(self): + def onCardLayout(self) -> None: self.saveNow(self._onCardLayout) - def _onCardLayout(self): + def _onCardLayout(self) -> None: from aqt.clayout import CardLayout if self.card: @@ -422,16 +424,16 @@ class Editor: # JS->Python bridge ###################################################################### - def onBridgeCmd(self, cmd) -> None: + def onBridgeCmd(self, cmd: str) -> None: if not self.note: # shutdown return # focus lost or key/button pressed? if cmd.startswith("blur") or cmd.startswith("key"): - (type, ord, nid, txt) = cmd.split(":", 3) - ord = int(ord) + (type, ord_str, nid_str, txt) = cmd.split(":", 3) + ord = int(ord_str) try: - nid = int(nid) + nid = int(nid_str) except ValueError: nid = 0 if nid != self.note.id: @@ -465,13 +467,15 @@ class Editor: else: print("uncaught cmd", cmd) - def mungeHTML(self, txt): + def mungeHTML(self, txt: str) -> str: return gui_hooks.editor_will_munge_html(txt, self) # Setting/unsetting the current note ###################################################################### - def setNote(self, note, hide=True, focusTo=None): + def setNote( + self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None + ) -> None: "Make NOTE the current note." self.note = note self.currentField = None @@ -482,10 +486,10 @@ class Editor: if hide: self.widget.hide() - def loadNoteKeepingFocus(self): + def loadNoteKeepingFocus(self) -> None: self.loadNote(self.currentField) - def loadNote(self, focusTo=None) -> None: + def loadNote(self, focusTo: Optional[int] = None) -> None: if not self.note: return @@ -496,7 +500,7 @@ class Editor: self.widget.show() self.updateTags() - def oncallback(arg): + def oncallback(arg: Any) -> None: if not self.note: return self.setupForegroundButton() @@ -520,7 +524,7 @@ class Editor: for f in self.note.model()["flds"] ] - def saveNow(self, callback, keepFocus=False): + def saveNow(self, callback: Callable, keepFocus: bool = False) -> None: "Save unsaved edits then call callback()." if not self.note: # calling code may not expect the callback to fire immediately @@ -529,7 +533,7 @@ class Editor: self.saveTags() self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback()) - def checkValid(self): + def checkValid(self) -> None: cols = [""] * len(self.note.fields) err = self.note.dupeOrEmpty() if err == 2: @@ -537,7 +541,7 @@ class Editor: self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) - def showDupes(self): + def showDupes(self) -> None: self.mw.browser_search( SearchTerm( dupe=SearchTerm.Dupe( @@ -546,7 +550,7 @@ class Editor: ) ) - def fieldsAreBlank(self, previousNote=None): + def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool: if not self.note: return True m = self.note.model() @@ -559,7 +563,7 @@ class Editor: return False return True - def cleanup(self): + def cleanup(self) -> None: self.setNote(None) # prevent any remaining evalWithCallback() events from firing after C++ object deleted self.web = None @@ -567,11 +571,11 @@ class Editor: # HTML editing ###################################################################### - def onHtmlEdit(self): + def onHtmlEdit(self) -> None: field = self.currentField self.saveNow(lambda: self._onHtmlEdit(field)) - def _onHtmlEdit(self, field): + def _onHtmlEdit(self, field: int) -> None: d = QDialog(self.widget, Qt.Window) form = aqt.forms.edithtml.Ui_Dialog() form.setupUi(d) @@ -604,7 +608,7 @@ class Editor: # Tag handling ###################################################################### - def setupTags(self): + def setupTags(self) -> None: import aqt.tagedit g = QGroupBox(self.widget) @@ -626,7 +630,7 @@ class Editor: g.setLayout(tb) self.outerLayout.addWidget(g) - def updateTags(self): + def updateTags(self) -> None: if self.tags.col != self.mw.col: self.tags.setCol(self.mw.col) if not self.tags.text() or not self.addMode: @@ -640,44 +644,44 @@ class Editor: self.note.flush() gui_hooks.editor_did_update_tags(self.note) - def saveAddModeVars(self): + def saveAddModeVars(self) -> None: if self.addMode: # save tags to model m = self.note.model() m["tags"] = self.note.tags self.mw.col.models.save(m, updateReqs=False) - def hideCompleters(self): + def hideCompleters(self) -> None: self.tags.hideCompleter() - def onFocusTags(self): + def onFocusTags(self) -> None: self.tags.setFocus() # Format buttons ###################################################################### - def toggleBold(self): + def toggleBold(self) -> None: self.web.eval("setFormat('bold');") - def toggleItalic(self): + def toggleItalic(self) -> None: self.web.eval("setFormat('italic');") - def toggleUnderline(self): + def toggleUnderline(self) -> None: self.web.eval("setFormat('underline');") - def toggleSuper(self): + def toggleSuper(self) -> None: self.web.eval("setFormat('superscript');") - def toggleSub(self): + def toggleSub(self) -> None: self.web.eval("setFormat('subscript');") - def removeFormat(self): + def removeFormat(self) -> None: self.web.eval("setFormat('removeFormat');") - def onCloze(self): + def onCloze(self) -> None: self.saveNow(self._onCloze, keepFocus=True) - def _onCloze(self): + def _onCloze(self) -> None: # check that the model is set up for cloze deletion if self.note.model()["type"] != MODEL_CLOZE: if self.addMode: @@ -701,16 +705,16 @@ class Editor: # Foreground colour ###################################################################### - def setupForegroundButton(self): + def setupForegroundButton(self) -> None: self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") self.onColourChanged() # use last colour - def onForeground(self): + def onForeground(self) -> None: self._wrapWithColour(self.fcolour) # choose new colour - def onChangeCol(self): + def onChangeCol(self) -> None: if isLin: new = QColorDialog.getColor( QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog @@ -724,32 +728,32 @@ class Editor: self.onColourChanged() self._wrapWithColour(self.fcolour) - def _updateForegroundButton(self): + def _updateForegroundButton(self) -> None: self.web.eval("setFGButton('%s')" % self.fcolour) - def onColourChanged(self): + def onColourChanged(self) -> None: self._updateForegroundButton() self.mw.pm.profile["lastColour"] = self.fcolour - def _wrapWithColour(self, colour): + def _wrapWithColour(self, colour: str) -> None: self.web.eval("setFormat('forecolor', '%s')" % colour) # Audio/video/images ###################################################################### - def onAddMedia(self): + def onAddMedia(self) -> None: extension_filter = " ".join( "*." + extension for extension in sorted(itertools.chain(pics, audio)) ) key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")" - def accept(file): + def accept(file: str) -> None: self.addMedia(file, canDelete=True) file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media") self.parentWindow.activateWindow() - def addMedia(self, path, canDelete=False): + def addMedia(self, path: str, canDelete: bool = False) -> None: try: html = self._addMedia(path, canDelete) except Exception as e: @@ -757,7 +761,7 @@ class Editor: return self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html)) - def _addMedia(self, path, canDelete=False): + 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) @@ -774,7 +778,7 @@ class Editor: def _addMediaFromData(self, fname: str, data: bytes) -> str: return self.mw.col.media.writeData(fname, data) - def onRecSound(self): + def onRecSound(self) -> None: aqt.sound.record_audio( self.parentWindow, self.mw, @@ -808,7 +812,7 @@ class Editor: # not a supported type return None - def isURL(self, s): + def isURL(self, s: str) -> bool: s = s.lower() return ( s.startswith("http://") @@ -957,23 +961,23 @@ class Editor: ) def doDrop(self, html: str, internal: bool, extended: bool = False) -> None: - def pasteIfField(ret): + def pasteIfField(ret: bool) -> None: if ret: self.doPaste(html, internal, extended) p = self.web.mapFromGlobal(QCursor.pos()) self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField) - def onPaste(self): + def onPaste(self) -> None: self.web.onPaste() - def onCutOrCopy(self): + def onCutOrCopy(self) -> None: self.web.flagAnkiText() # Advanced menu ###################################################################### - def onAdvanced(self): + def onAdvanced(self) -> None: m = QMenu(self.mw) for text, handler, shortcut in ( @@ -1000,28 +1004,28 @@ class Editor: # LaTeX ###################################################################### - def insertLatex(self): + def insertLatex(self) -> None: self.web.eval("wrap('[latex]', '[/latex]');") - def insertLatexEqn(self): + def insertLatexEqn(self) -> None: self.web.eval("wrap('[$]', '[/$]');") - def insertLatexMathEnv(self): + def insertLatexMathEnv(self) -> None: self.web.eval("wrap('[$$]', '[/$$]');") - def insertMathjaxInline(self): + def insertMathjaxInline(self) -> None: self.web.eval("wrap('\\\\(', '\\\\)');") - def insertMathjaxBlock(self): + def insertMathjaxBlock(self) -> None: self.web.eval("wrap('\\\\[', '\\\\]');") - def insertMathjaxChemistry(self): + def insertMathjaxChemistry(self) -> None: self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');") # Links from HTML ###################################################################### - _links = dict( + _links: Dict[str, Callable] = dict( fields=onFields, cards=onCardLayout, bold=toggleBold, @@ -1047,7 +1051,7 @@ class Editor: class EditorWebView(AnkiWebView): - def __init__(self, parent, editor): + def __init__(self, parent: QWidget, editor: Editor) -> None: AnkiWebView.__init__(self, title="editor") self.editor = editor self.strip = self.editor.mw.pm.profile["stripHTML"] @@ -1057,15 +1061,15 @@ class EditorWebView(AnkiWebView): qconnect(clip.dataChanged, self._onClipboardChange) gui_hooks.editor_web_view_did_init(self) - def _onClipboardChange(self): + def _onClipboardChange(self) -> None: if self._markInternal: self._markInternal = False self._flagAnkiText() - def onCut(self): + def onCut(self) -> None: self.triggerPageAction(QWebEnginePage.Cut) - def onCopy(self): + def onCopy(self) -> None: self.triggerPageAction(QWebEnginePage.Copy) def _wantsExtendedPaste(self) -> bool: @@ -1088,10 +1092,10 @@ class EditorWebView(AnkiWebView): def onMiddleClickPaste(self) -> None: self._onPaste(QClipboard.Selection) - def dragEnterEvent(self, evt): + def dragEnterEvent(self, evt: QDragEnterEvent) -> None: evt.accept() - def dropEvent(self, evt): + def dropEvent(self, evt: QDropEvent) -> None: extended = self._wantsExtendedPaste() mime = evt.mimeData() @@ -1172,7 +1176,7 @@ class EditorWebView(AnkiWebView): token = html.escape(token).replace("\t", " " * 4) # if there's more than one consecutive space, # use non-breaking spaces for the second one on - def repl(match): + def repl(match: Match) -> None: return match.group(1).replace(" ", " ") + " " token = re.sub(" ( +)", repl, token) @@ -1218,11 +1222,11 @@ class EditorWebView(AnkiWebView): return self.editor.fnameToLink(fname) return None - def flagAnkiText(self): + def flagAnkiText(self) -> None: # be ready to adjust when clipboard event fires self._markInternal = True - def _flagAnkiText(self): + def _flagAnkiText(self) -> None: # add a comment in the clipboard html so we can tell text is copied # from us and doesn't need to be stripped clip = self.editor.mw.app.clipboard() @@ -1250,20 +1254,20 @@ class EditorWebView(AnkiWebView): # QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light" # - there may be other cases like a trailing 'Bold' that need fixing, but will # wait for further reports first. -def fontMungeHack(font): +def fontMungeHack(font: str) -> str: return re.sub(" L$", " Light", font) -def munge_html(txt, editor): +def munge_html(txt: str, editor: Editor) -> str: return "" if txt in ("
", "

") else txt -def remove_null_bytes(txt, editor): +def remove_null_bytes(txt: str, editor: Editor) -> str: # misbehaving apps may include a null byte in the text return txt.replace("\x00", "") -def reverse_url_quoting(txt, editor): +def reverse_url_quoting(txt: str, editor: Editor) -> str: # reverse the url quoting we added to get images to display return editor.mw.col.media.escape_media_filenames(txt, unescape=True) diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index 2f47a17c2..0bf9f9342 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -525,7 +525,7 @@ if isWin: id: Any class WindowsRTTTSFilePlayer(TTSProcessPlayer): - voice_list = None + voice_list: List[Any] = [] tmppath = os.path.join(tmpdir(), "tts.wav") def import_voices(self) -> None: diff --git a/qt/mypy.ini b/qt/mypy.ini index f4abf1416..c6cdac729 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -12,6 +12,8 @@ strict_equality = true disallow_untyped_defs=true [mypy-aqt.sidebar] disallow_untyped_defs=true +[mypy-aqt.editor] +disallow_untyped_defs=true [mypy-aqt.mpv] From 08b76e4489b1d91f3c8671657980adea9ef9e2e4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 17:29:03 +1000 Subject: [PATCH 18/22] add helper script to run mypy daemon --- qt/dmypy-watch.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 qt/dmypy-watch.sh diff --git a/qt/dmypy-watch.sh b/qt/dmypy-watch.sh new file mode 100755 index 000000000..d5e2b3a00 --- /dev/null +++ b/qt/dmypy-watch.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# +# semi-working support for mypy daemon +# - install fs_watch +# - build anki/aqt wheels first +# - create a new venv and activate it +# - install the wheels +# - then run this script from this folder + +(sleep 1 && touch aqt) +. ~/pyenv/bin/activate +fswatch -o aqt | xargs -n1 -I{} sh -c 'printf \\033c; dmypy run aqt' + From 98f4b3db81c2bb805ca25d34b23848fbd7d41213 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 20:23:48 +1000 Subject: [PATCH 19/22] add types to utils.py The function signatures for things like getFile() are awful, but sadly are used by a bunch of add-ons. --- qt/aqt/addons.py | 3 +- qt/aqt/clayout.py | 6 +- qt/aqt/deckbrowser.py | 3 +- qt/aqt/editor.py | 10 +- qt/aqt/main.py | 8 +- qt/aqt/sidebar.py | 3 +- qt/aqt/utils.py | 296 ++++++++++++++++++++++++++---------------- qt/mypy.ini | 2 + 8 files changed, 210 insertions(+), 121 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 656379771..5b32d21e2 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -867,9 +867,10 @@ class AddonsDialog(QDialog): def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]: if not paths: key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext) - paths = getFile( + paths_ = getFile( self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True ) + paths = paths_ # type: ignore if not paths: return False diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 0fc9fb3d9..011b8fe5f 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -607,14 +607,14 @@ class CardLayout(QDialog): n = len(self.templates) template = self.current_template() current_pos = self.templates.index(template) + 1 - pos = getOnlyText( + pos_txt = getOnlyText( tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n), default=str(current_pos), ) - if not pos: + if not pos_txt: return try: - pos = int(pos) + pos = int(pos_txt) except ValueError: return if pos < 1 or pos > n: diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 10041e9fb..bcd935f70 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -256,7 +256,8 @@ class DeckBrowser: self.mw.col.decks.rename(deck, newName) gui_hooks.sidebar_should_refresh_decks() except DeckRenameError as e: - return showWarning(e.description) + showWarning(e.description) + return self.show() def _options(self, did): diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 717d02975..88aadb8d1 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -12,7 +12,7 @@ import urllib.parse import urllib.request import warnings from random import randrange -from typing import Any, Callable, Dict, List, Match, Optional, Tuple +from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast import bs4 import requests @@ -750,7 +750,13 @@ class Editor: def accept(file: str) -> None: self.addMedia(file, canDelete=True) - file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media") + file = getFile( + self.widget, + tr(TR.EDITING_ADD_MEDIA), + cast(Callable[[Any], None], accept), + key, + key="media", + ) self.parentWindow.activateWindow() def addMedia(self, path: str, canDelete: bool = False) -> None: diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 173d7a4d8..80475ae7a 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1530,14 +1530,15 @@ title="%s" %s>%s""" % ( def setupAppMsg(self) -> None: qconnect(self.app.appMsg, self.onAppMsg) - def onAppMsg(self, buf: str) -> Optional[QTimer]: + def onAppMsg(self, buf: str) -> None: is_addon = self._isAddon(buf) if self.state == "startup": # try again in a second - return self.progress.timer( + self.progress.timer( 1000, lambda: self.onAppMsg(buf), False, requiresCollection=False ) + return elif self.state == "profileManager": # can't raise window while in profile manager if buf == "raise": @@ -1547,7 +1548,8 @@ title="%s" %s>%s""" % ( msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A) else: msg = tr(TR.QT_MISC_DECK_WILL_BE_IMPORTED_WHEN_A) - return tooltip(msg) + tooltip(msg) + return if not self.interactiveState() or self.progress.busy(): # we can't raise the main window while in profile dialog, syncing, etc if buf != "raise": diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index fa5da4efc..bafc12ed9 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -696,7 +696,8 @@ class SidebarTreeView(QTreeView): try: self.mw.col.decks.rename(deck, new_name) except DeckRenameError as e: - return showWarning(e.description) + showWarning(e.description) + return self.refresh() self.mw.deckBrowser.refresh() diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index ac511e9a4..3c0407e06 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -8,12 +8,35 @@ import re import subprocess import sys from enum import Enum -from typing import TYPE_CHECKING, Any, List, Literal, Optional, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + List, + Literal, + Optional, + Sequence, + Tuple, + Union, + cast, +) from markdown import markdown +from PyQt5.QtWidgets import ( + QAction, + QDialog, + QDialogButtonBox, + QFileDialog, + QHeaderView, + QMenu, + QPushButton, + QSplitter, + QWidget, +) import anki import aqt +from anki import Collection from anki.errors import InvalidInput from anki.lang import TR # pylint: disable=unused-import from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild @@ -77,7 +100,7 @@ argument. However, add-on may use string, and we want to accept this. """ -def openHelp(section: HelpPageArgument): +def openHelp(section: HelpPageArgument) -> None: link = aqt.appHelpSite if section: if isinstance(section, HelpPage): @@ -87,27 +110,35 @@ def openHelp(section: HelpPageArgument): openLink(link) -def openLink(link): +def openLink(link: str) -> None: tooltip(tr(TR.QT_MISC_LOADING), period=1000) with noBundledLibs(): QDesktopServices.openUrl(QUrl(link)) def showWarning( - text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None -): + text: str, + parent: Optional[QDialog] = None, + help: HelpPageArgument = "", + title: str = "Anki", + textFormat: Optional[TextFormat] = None, +) -> int: "Show a small warning with an OK button." return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat) def showCritical( - text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None -): + text: str, + parent: Optional[QDialog] = None, + help: str = "", + title: str = "Anki", + textFormat: Optional[TextFormat] = None, +) -> int: "Show a small critical error with an OK button." return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat) -def show_invalid_search_error(err: Exception): +def show_invalid_search_error(err: Exception) -> None: "Render search errors in markdown, then display a warning." text = str(err) if isinstance(err, InvalidInput): @@ -116,24 +147,27 @@ def show_invalid_search_error(err: Exception): def showInfo( - text, - parent=False, - help="", - type="info", - title="Anki", + text: str, + parent: Union[Literal[False], QDialog] = False, + help: HelpPageArgument = "", + type: str = "info", + title: str = "Anki", textFormat: Optional[TextFormat] = None, - customBtns=None, + customBtns: Optional[List[QMessageBox.StandardButton]] = None, ) -> int: "Show a small info window with an OK button." + parent_widget: QWidget if parent is False: - parent = aqt.mw.app.activeWindow() or aqt.mw + parent_widget = aqt.mw.app.activeWindow() or aqt.mw + else: + parent_widget = parent if type == "warning": icon = QMessageBox.Warning elif type == "critical": icon = QMessageBox.Critical else: icon = QMessageBox.Information - mb = QMessageBox(parent) + mb = QMessageBox(parent_widget) # if textFormat == "plain": mb.setTextFormat(Qt.PlainText) elif textFormat == "rich": @@ -161,16 +195,16 @@ def showInfo( def showText( - txt, - parent=None, - type="text", - run=True, - geomKey=None, - minWidth=500, - minHeight=400, - title="Anki", - copyBtn=False, -): + txt: str, + parent: Optional[QWidget] = None, + type: str = "text", + run: bool = True, + geomKey: Optional[str] = None, + minWidth: int = 500, + minHeight: int = 400, + title: str = "Anki", + copyBtn: bool = False, +) -> Optional[Tuple[QDialog, QDialogButtonBox]]: if not parent: parent = aqt.mw.app.activeWindow() or aqt.mw diag = QDialog(parent) @@ -189,21 +223,21 @@ def showText( layout.addWidget(box) if copyBtn: - def onCopy(): + def onCopy() -> None: QApplication.clipboard().setText(text.toPlainText()) btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD)) qconnect(btn.clicked, onCopy) box.addButton(btn, QDialogButtonBox.ActionRole) - def onReject(): + def onReject() -> None: if geomKey: saveGeom(diag, geomKey) QDialog.reject(diag) qconnect(box.rejected, onReject) - def onFinish(): + def onFinish() -> None: if geomKey: saveGeom(diag, geomKey) @@ -214,18 +248,19 @@ def showText( restoreGeom(diag, geomKey) if run: diag.exec_() + return None else: return diag, box def askUser( - text, - parent=None, + text: str, + parent: QDialog = None, help: HelpPageArgument = None, - defaultno=False, - msgfunc=None, - title="Anki", -): + defaultno: bool = False, + msgfunc: Optional[Callable] = None, + title: str = "Anki", +) -> bool: "Show a yes/no question. Return true if yes." if not parent: parent = aqt.mw.app.activeWindow() @@ -239,7 +274,7 @@ def askUser( default = QMessageBox.No else: default = QMessageBox.Yes - r = msgfunc(parent, title, text, sb, default) + r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default) if r == QMessageBox.Help: openHelp(help) @@ -250,10 +285,15 @@ def askUser( class ButtonedDialog(QMessageBox): def __init__( - self, text, buttons, parent=None, help: HelpPageArgument = None, title="Anki" + self, + text: str, + buttons: List[str], + parent: Optional[QDialog] = None, + help: HelpPageArgument = None, + title: str = "Anki", ): QMessageBox.__init__(self, parent) - self._buttons = [] + self._buttons: List[QPushButton] = [] self.setWindowTitle(title) self.help = help self.setIcon(QMessageBox.Warning) @@ -264,7 +304,7 @@ class ButtonedDialog(QMessageBox): self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole) buttons.append(tr(TR.ACTIONS_HELP)) - def run(self): + def run(self) -> str: self.exec_() but = self.clickedButton().text() if but == "Help": @@ -274,13 +314,17 @@ class ButtonedDialog(QMessageBox): # work around KDE 'helpfully' adding accelerators to button text of Qt apps return txt.replace("&", "") - def setDefault(self, idx): + def setDefault(self, idx: int) -> None: self.setDefaultButton(self._buttons[idx]) def askUserDialog( - text, buttons, parent=None, help: HelpPageArgument = None, title="Anki" -): + text: str, + buttons: List[str], + parent: Optional[QDialog] = None, + help: HelpPageArgument = None, + title: str = "Anki", +) -> ButtonedDialog: if not parent: parent = aqt.mw diag = ButtonedDialog(text, buttons, parent, help, title=title) @@ -290,14 +334,14 @@ def askUserDialog( class GetTextDialog(QDialog): def __init__( self, - parent, - question, + parent: Optional[QDialog], + question: str, help: HelpPageArgument = None, - edit=None, - default="", - title="Anki", - minWidth=400, - ): + edit: Optional[QLineEdit] = None, + default: str = "", + title: str = "Anki", + minWidth: int = 400, + ) -> None: QDialog.__init__(self, parent) self.setWindowTitle(title) disable_help_button(self) @@ -325,26 +369,26 @@ class GetTextDialog(QDialog): if help: qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested) - def accept(self): + def accept(self) -> None: return QDialog.accept(self) - def reject(self): + def reject(self) -> None: return QDialog.reject(self) - def helpRequested(self): + def helpRequested(self) -> None: openHelp(self.help) def getText( - prompt, - parent=None, + prompt: str, + parent: Optional[QDialog] = None, help: HelpPageArgument = None, - edit=None, - default="", - title="Anki", - geomKey=None, - **kwargs, -): + edit: Optional[QLineEdit] = None, + default: str = "", + title: str = "Anki", + geomKey: Optional[str] = None, + **kwargs: Any, +) -> Tuple[str, int]: if not parent: parent = aqt.mw.app.activeWindow() or aqt.mw d = GetTextDialog( @@ -359,7 +403,7 @@ def getText( return (str(d.l.text()), ret) -def getOnlyText(*args, **kwargs): +def getOnlyText(*args: Any, **kwargs: Any) -> str: (s, r) = getText(*args, **kwargs) if r: return s @@ -368,7 +412,10 @@ def getOnlyText(*args, **kwargs): # fixme: these utilities could be combined into a single base class -def chooseList(prompt, choices, startrow=0, parent=None): +# unused by Anki, but used by add-ons +def chooseList( + prompt: str, choices: List[str], startrow: int = 0, parent: Any = None +) -> int: if not parent: parent = aqt.mw.app.activeWindow() d = QDialog(parent) @@ -389,7 +436,9 @@ def chooseList(prompt, choices, startrow=0, parent=None): return c.currentRow() -def getTag(parent, deck, question, tags="user", **kwargs): +def getTag( + parent: QDialog, deck: Collection, question: str, **kwargs: Any +) -> Tuple[str, int]: from aqt.tagedit import TagEdit te = TagEdit(parent) @@ -409,7 +458,15 @@ def disable_help_button(widget: QWidget) -> None: ###################################################################### -def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False): +def getFile( + parent: QDialog, + title: str, + cb: Optional[Callable[[Union[str, Sequence[str]]], None]], + filter: str = "*.*", + dir: Optional[str] = None, + key: Optional[str] = None, + multi: bool = False, # controls whether a single or multiple files is returned +) -> Optional[Union[Sequence[str], str]]: "Ask the user for a file." assert not dir or not key if not dir: @@ -426,7 +483,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False): d.setNameFilter(filter) ret = [] - def accept(): + def accept() -> None: files = list(d.selectedFiles()) if dirkey: dir = os.path.dirname(files[0]) @@ -442,10 +499,17 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False): d.exec_() if key: saveState(d, key) - return ret and ret[0] + return ret[0] if ret else None -def getSaveFile(parent, title, dir_description, key, ext, fname=None): +def getSaveFile( + parent: QDialog, + title: str, + dir_description: str, + key: str, + ext: str, + fname: Optional[str] = None, +) -> str: """Ask the user for a file to save. Use DIR_DESCRIPTION as config variable. The file dialog will default to open with FNAME.""" config_key = dir_description + "Directory" @@ -474,7 +538,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None): return file -def saveGeom(widget, key: str): +def saveGeom(widget: QDialog, key: str) -> None: key += "Geom" if isMac and widget.windowState() & Qt.WindowFullScreen: geom = None @@ -483,7 +547,9 @@ def saveGeom(widget, key: str): aqt.mw.pm.profile[key] = geom -def restoreGeom(widget, key: str, offset=None, adjustSize=False): +def restoreGeom( + widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False +) -> None: key += "Geom" if aqt.mw.pm.profile.get(key): widget.restoreGeometry(aqt.mw.pm.profile[key]) @@ -498,7 +564,7 @@ def restoreGeom(widget, key: str, offset=None, adjustSize=False): widget.adjustSize() -def ensureWidgetInScreenBoundaries(widget): +def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: handle = widget.window().windowHandle() if not handle: # window has not yet been shown, retry later @@ -524,58 +590,60 @@ def ensureWidgetInScreenBoundaries(widget): widget.move(x, y) -def saveState(widget, key: str): +def saveState(widget: QFileDialog, key: str) -> None: key += "State" aqt.mw.pm.profile[key] = widget.saveState() -def restoreState(widget, key: str): +def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None: key += "State" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key]) -def saveSplitter(widget, key): +def saveSplitter(widget: QSplitter, key: str) -> None: key += "Splitter" aqt.mw.pm.profile[key] = widget.saveState() -def restoreSplitter(widget, key): +def restoreSplitter(widget: QSplitter, key: str) -> None: key += "Splitter" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key]) -def saveHeader(widget, key): +def saveHeader(widget: QHeaderView, key: str) -> None: key += "Header" aqt.mw.pm.profile[key] = widget.saveState() -def restoreHeader(widget, key): +def restoreHeader(widget: QHeaderView, key: str) -> None: key += "Header" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key]) -def save_is_checked(widget, key: str): +def save_is_checked(widget: QWidget, key: str) -> None: key += "IsChecked" aqt.mw.pm.profile[key] = widget.isChecked() -def restore_is_checked(widget, key: str): +def restore_is_checked(widget: QWidget, key: str) -> None: key += "IsChecked" if aqt.mw.pm.profile.get(key) is not None: widget.setChecked(aqt.mw.pm.profile[key]) -def save_combo_index_for_session(widget: QComboBox, key: str): +def save_combo_index_for_session(widget: QComboBox, key: str) -> None: textKey = key + "ComboActiveText" indexKey = key + "ComboActiveIndex" aqt.mw.pm.session[textKey] = widget.currentText() aqt.mw.pm.session[indexKey] = widget.currentIndex() -def restore_combo_index_for_session(widget: QComboBox, history: List[str], key: str): +def restore_combo_index_for_session( + widget: QComboBox, history: List[str], key: str +) -> None: textKey = key + "ComboActiveText" indexKey = key + "ComboActiveIndex" text = aqt.mw.pm.session.get(textKey) @@ -585,7 +653,7 @@ def restore_combo_index_for_session(widget: QComboBox, history: List[str], key: widget.setCurrentIndex(index) -def save_combo_history(comboBox: QComboBox, history: List[str], name: str): +def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str: name += "BoxHistory" text_input = comboBox.lineEdit().text() if text_input in history: @@ -599,7 +667,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str): return text_input -def restore_combo_history(comboBox: QComboBox, name: str): +def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]: name += "BoxHistory" history = aqt.mw.pm.profile.get(name, []) comboBox.addItems([""] + history) @@ -611,13 +679,13 @@ def restore_combo_history(comboBox: QComboBox, name: str): return history -def mungeQA(col, txt): +def mungeQA(col: Collection, txt: str) -> str: print("mungeQA() deprecated; use mw.prepare_card_text_for_display()") txt = col.media.escape_media_filenames(txt) return txt -def openFolder(path): +def openFolder(path: str) -> None: if isWin: subprocess.Popen(["explorer", "file://" + path]) else: @@ -625,27 +693,27 @@ def openFolder(path): QDesktopServices.openUrl(QUrl("file://" + path)) -def shortcut(key): +def shortcut(key: str) -> str: if isMac: return re.sub("(?i)ctrl", "Command", key) return key -def maybeHideClose(bbox): +def maybeHideClose(bbox: QDialogButtonBox) -> None: if isMac: b = bbox.button(QDialogButtonBox.Close) if b: bbox.removeButton(b) -def addCloseShortcut(widg): +def addCloseShortcut(widg: QDialog) -> None: if not isMac: return widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg) qconnect(widg._closeShortcut.activated, widg.reject) -def downArrow(): +def downArrow() -> str: if isWin: return "â–¼" # windows 10 is lacking the smaller arrow on English installs @@ -659,13 +727,19 @@ _tooltipTimer: Optional[QTimer] = None _tooltipLabel: Optional[QLabel] = None -def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100): +def tooltip( + msg: str, + period: int = 3000, + parent: Optional[aqt.AnkiQt] = None, + x_offset: int = 0, + y_offset: int = 100, +) -> None: global _tooltipTimer, _tooltipLabel class CustomLabel(QLabel): silentlyClose = True - def mousePressEvent(self, evt): + def mousePressEvent(self, evt: QMouseEvent) -> None: evt.accept() self.hide() @@ -697,7 +771,7 @@ def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100): _tooltipLabel = lab -def closeTooltip(): +def closeTooltip() -> None: global _tooltipLabel, _tooltipTimer if _tooltipLabel: try: @@ -712,7 +786,7 @@ def closeTooltip(): # true if invalid; print warning -def checkInvalidFilename(str, dirsep=True): +def checkInvalidFilename(str: str, dirsep: bool = True) -> bool: bad = invalidFilename(str, dirsep) if bad: showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad)) @@ -723,28 +797,30 @@ def checkInvalidFilename(str, dirsep=True): # Menus ###################################################################### +MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"] + class MenuList: - def __init__(self): - self.children = [] + def __init__(self) -> None: + self.children: List[MenuListChild] = [] - def addItem(self, title, func): + def addItem(self, title: str, func: Callable) -> MenuItem: item = MenuItem(title, func) self.children.append(item) return item - def addSeparator(self): + def addSeparator(self) -> None: self.children.append(None) - def addMenu(self, title): + def addMenu(self, title: str) -> SubMenu: submenu = SubMenu(title) self.children.append(submenu) return submenu - def addChild(self, child): + def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None: self.children.append(child) - def renderTo(self, qmenu): + def renderTo(self, qmenu: QMenu) -> None: for child in self.children: if child is None: qmenu.addSeparator() @@ -753,33 +829,33 @@ class MenuList: else: child.renderTo(qmenu) - def popupOver(self, widget): + def popupOver(self, widget: QPushButton) -> None: qmenu = QMenu() self.renderTo(qmenu) qmenu.exec_(widget.mapToGlobal(QPoint(0, 0))) class SubMenu(MenuList): - def __init__(self, title): + def __init__(self, title: str) -> None: super().__init__() self.title = title - def renderTo(self, menu): + def renderTo(self, menu: QMenu) -> None: submenu = menu.addMenu(self.title) super().renderTo(submenu) class MenuItem: - def __init__(self, title, func): + def __init__(self, title: str, func: Callable) -> None: self.title = title self.func = func - def renderTo(self, qmenu): + def renderTo(self, qmenu: QMenu) -> None: a = qmenu.addAction(self.title) qconnect(a.triggered, self.func) -def qtMenuShortcutWorkaround(qmenu): +def qtMenuShortcutWorkaround(qmenu: QMenu) -> None: if qtminor < 10: return for act in qmenu.actions(): @@ -789,7 +865,7 @@ def qtMenuShortcutWorkaround(qmenu): ###################################################################### -def supportText(): +def supportText() -> str: import platform import time @@ -802,9 +878,9 @@ def supportText(): else: platname = "Linux" - def schedVer(): + def schedVer() -> str: try: - return mw.col.schedVer() + return str(mw.col.schedVer()) except: return "?" @@ -832,7 +908,7 @@ Add-ons, last update check: {} ###################################################################### # adapted from version detection in qutebrowser -def opengl_vendor(): +def opengl_vendor() -> Optional[str]: old_context = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() @@ -871,7 +947,7 @@ def opengl_vendor(): old_context.makeCurrent(old_surface) -def gfxDriverIsBroken(): +def gfxDriverIsBroken() -> bool: driver = opengl_vendor() return driver == "nouveau" diff --git a/qt/mypy.ini b/qt/mypy.ini index c6cdac729..d04edadc8 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -14,6 +14,8 @@ disallow_untyped_defs=true disallow_untyped_defs=true [mypy-aqt.editor] disallow_untyped_defs=true +[mypy-aqt.utils] +disallow_untyped_defs=true [mypy-aqt.mpv] From 84f8d7f6049c94bead492d25a7b19d62185a1d5a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 20:59:18 +1000 Subject: [PATCH 20/22] add some types to main.py --- qt/aqt/deckbrowser.py | 4 ++-- qt/aqt/main.py | 48 ++++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index bcd935f70..ef8e0a621 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -235,13 +235,13 @@ class DeckBrowser: a = m.addAction(tr(TR.ACTIONS_OPTIONS)) qconnect(a.triggered, lambda b, did=did: self._options(did)) a = m.addAction(tr(TR.ACTIONS_EXPORT)) - qconnect(a.triggered, lambda b, did=did: self._export(did)) + qconnect(a.triggered, lambda b, did=did: self._export(int(did))) a = m.addAction(tr(TR.ACTIONS_DELETE)) qconnect(a.triggered, lambda b, did=did: self._delete(int(did))) gui_hooks.deck_browser_will_show_options_menu(m, int(did)) m.exec_(QCursor.pos()) - def _export(self, did): + def _export(self, did: int) -> None: self.mw.onExport(did=did) def _rename(self, did: int) -> None: diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 80475ae7a..591df9048 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -14,7 +14,7 @@ import weakref import zipfile from argparse import Namespace from threading import Thread -from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, cast +from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, Union, cast import anki import aqt @@ -71,6 +71,7 @@ install_pylib_legacy() class ResetReason(enum.Enum): + Unknown = "unknown" AddCardsAddNote = "addCardsAddNote" EditCurrentInit = "editCurrentInit" EditorBridgeCmd = "editorBridgeCmd" @@ -510,12 +511,12 @@ class AnkiQt(QMainWindow): return True - def _loadCollection(self): + def _loadCollection(self) -> None: cpath = self.pm.collectionPath() self.col = Collection(cpath, backend=self.backend, log=True) self.setEnabled(True) - def reopen(self): + def reopen(self) -> None: self.col.reopen() def unloadCollection(self, onsuccess: Callable) -> None: @@ -656,10 +657,10 @@ class AnkiQt(QMainWindow): return self.moveToState("deckBrowser") self.overview.show() - def _reviewState(self, oldState): + def _reviewState(self, oldState: str) -> None: self.reviewer.show() - def _reviewCleanup(self, newState): + def _reviewCleanup(self, newState: str) -> None: if newState != "resetRequired" and newState != "review": self.reviewer.cleanup() @@ -675,7 +676,12 @@ class AnkiQt(QMainWindow): self.maybeEnableUndo() self.moveToState(self.state) - def requireReset(self, modal=False, reason="unknown", context=None): + def requireReset( + self, + modal: bool = False, + reason: ResetReason = ResetReason.Unknown, + context: Any = None, + ) -> None: "Signal queue needs to be rebuilt when edits are finished or by user." self.autosave() self.resetModal = modal @@ -684,7 +690,7 @@ class AnkiQt(QMainWindow): ): self.moveToState("resetRequired") - def interactiveState(self): + def interactiveState(self) -> bool: "True if not in profile manager, syncing, etc." return self.state in ("overview", "review", "deckBrowser") @@ -823,7 +829,7 @@ title="%s" %s>%s""" % ( self.addonManager.loadAddons() self.maybe_check_for_addon_updates() - def maybe_check_for_addon_updates(self): + def maybe_check_for_addon_updates(self) -> None: last_check = self.pm.last_addon_update_check() elap = intTime() - last_check @@ -866,7 +872,7 @@ title="%s" %s>%s""" % ( # Syncing ########################################################################## - def on_sync_button_clicked(self): + def on_sync_button_clicked(self) -> None: if self.media_syncer.is_syncing(): self.media_syncer.show_sync_log() else: @@ -879,7 +885,7 @@ title="%s" %s>%s""" % ( else: self._sync_collection_and_media(self._refresh_after_sync) - def _refresh_after_sync(self): + def _refresh_after_sync(self) -> None: self.toolbar.redraw() def _sync_collection_and_media(self, after_sync: Callable[[], None]): @@ -1028,7 +1034,7 @@ title="%s" %s>%s""" % ( self.form.actionUndo.setEnabled(False) gui_hooks.undo_state_did_change(False) - def checkpoint(self, name): + def checkpoint(self, name: str) -> None: self.col.save(name) self.maybeEnableUndo() @@ -1048,7 +1054,7 @@ title="%s" %s>%s""" % ( browser = aqt.dialogs.open("Browser", self) browser.show_single_card(self.reviewer.card) - def onEditCurrent(self): + def onEditCurrent(self) -> None: aqt.dialogs.open("EditCurrent", self) def onDeckConf(self, deck=None): @@ -1067,7 +1073,7 @@ title="%s" %s>%s""" % ( self.col.reset() self.moveToState("overview") - def onStats(self): + def onStats(self) -> None: deck = self._selectedDeck() if not deck: return @@ -1080,7 +1086,7 @@ title="%s" %s>%s""" % ( def onPrefs(self): aqt.dialogs.open("Preferences", self) - def onNoteTypes(self): + def onNoteTypes(self) -> None: import aqt.models aqt.models.Models(self, self, fromMain=True) @@ -1107,12 +1113,12 @@ title="%s" %s>%s""" % ( aqt.importing.importFile(self, path) return None - def onImport(self): + def onImport(self) -> None: import aqt.importing aqt.importing.onImport(self) - def onExport(self, did=None): + def onExport(self, did: Optional[int] = None) -> None: import aqt.exporting aqt.exporting.ExportDialog(self, did=did) @@ -1229,7 +1235,7 @@ title="%s" %s>%s""" % ( elif self.state == "overview": self.overview.refresh() - def on_autosync_timer(self): + def on_autosync_timer(self) -> None: elap = self.media_syncer.seconds_since_last_sync() minutes = self.pm.auto_sync_media_minutes() if not minutes: @@ -1295,7 +1301,7 @@ title="%s" %s>%s""" % ( ########################################################################## # this will gradually be phased out - def onSchemaMod(self, arg): + def onSchemaMod(self, arg: bool) -> bool: assert self.inMainThread() progress_shown = self.progress.busy() if progress_shown: @@ -1316,14 +1322,14 @@ title="%s" %s>%s""" % ( # Advanced features ########################################################################## - def onCheckDB(self): + def onCheckDB(self) -> None: check_db(self) def on_check_media_db(self) -> None: gui_hooks.media_check_will_start() check_media_db(self) - def onStudyDeck(self): + def onStudyDeck(self) -> None: from aqt.studydeck import StudyDeck ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"]) @@ -1377,7 +1383,7 @@ title="%s" %s>%s""" % ( a = menu.addAction("Clear Code") a.setShortcuts(QKeySequence("ctrl+shift+l")) qconnect(a.triggered, frm.text.clear) - menu.exec(QCursor.pos()) + menu.exec_(QCursor.pos()) frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log") frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text") From f15715fb07bad68ebf1f985dd8cab8e12640e1ef Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 22:08:56 +1000 Subject: [PATCH 21/22] add types to various other files Mainly automated with MonkeyType --- qt/aqt/addcards.py | 8 +++--- qt/aqt/clayout.py | 51 +++++++++++++++++---------------- qt/aqt/customstudy.py | 21 ++++++++------ qt/aqt/dyndeckconf.py | 21 +++++++++----- qt/aqt/editcurrent.py | 4 +-- qt/aqt/emptycards.py | 2 +- qt/aqt/errors.py | 28 +++++++++++------- qt/aqt/exporting.py | 6 ++-- qt/aqt/fields.py | 30 ++++++++++--------- qt/aqt/importing.py | 26 ++++++++++------- qt/aqt/main.py | 2 +- qt/aqt/mediasrv.py | 6 ++-- qt/aqt/models.py | 2 +- qt/aqt/overview.py | 24 ++++++++-------- qt/aqt/previewer.py | 49 ++++++++++++++++--------------- qt/aqt/profiles.py | 18 ++++++------ qt/aqt/progress.py | 28 +++++++++--------- qt/aqt/reviewer.py | 4 +-- qt/aqt/schema_change_tracker.py | 2 +- qt/aqt/stats.py | 14 ++++----- qt/aqt/studydeck.py | 6 ++-- qt/aqt/tagedit.py | 33 +++++++++++++-------- qt/aqt/taglimit.py | 19 ++++++------ qt/aqt/taskman.py | 2 +- qt/aqt/update.py | 3 +- 25 files changed, 227 insertions(+), 182 deletions(-) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 5a8d07607..296dda0a0 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -137,7 +137,7 @@ class AddCards(QDialog): def removeTempNote(self, note: Note) -> None: print("removeTempNote() will go away") - def addHistory(self, note): + def addHistory(self, note: Note) -> None: self.history.insert(0, note.id) self.history = self.history[:15] self.historyButton.setEnabled(True) @@ -186,10 +186,10 @@ class AddCards(QDialog): gui_hooks.add_cards_did_add_note(note) return note - def addCards(self): + def addCards(self) -> None: self.editor.saveNow(self._addCards) - def _addCards(self): + def _addCards(self) -> None: self.editor.saveAddModeVars() if not self.addNote(self.editor.note): return @@ -202,7 +202,7 @@ class AddCards(QDialog): self.onReset(keep=True) self.mw.col.autosave() - def keyPressEvent(self, evt): + def keyPressEvent(self, evt: QKeyEvent) -> None: "Show answer on RET or register answer." if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus(): evt.accept() diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 011b8fe5f..1525f9793 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -14,6 +14,7 @@ from anki.lang import without_unicode_isolation from anki.notes import Note from anki.template import TemplateRenderContext from aqt import AnkiQt, gui_hooks +from aqt.forms.browserdisp import Ui_Dialog from aqt.qt import * from aqt.schema_change_tracker import ChangeTracker from aqt.sound import av_player, play_clicked_audio @@ -90,7 +91,7 @@ class CardLayout(QDialog): # as users tend to accidentally type into the template self.setFocus() - def redraw_everything(self): + def redraw_everything(self) -> None: self.ignore_change_signals = True self.updateTopArea() self.ignore_change_signals = False @@ -104,13 +105,13 @@ class CardLayout(QDialog): self.fill_fields_from_template() self.renderPreview() - def _isCloze(self): + def _isCloze(self) -> bool: return self.model["type"] == MODEL_CLOZE # Top area ########################################################################## - def setupTopArea(self): + def setupTopArea(self) -> None: self.topArea = QWidget() self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self.topAreaForm = aqt.forms.clayout_top.Ui_Form() @@ -125,10 +126,10 @@ class CardLayout(QDialog): ) self.topAreaForm.card_type_label.setText(tr(TR.CARD_TEMPLATES_CARD_TYPE)) - def updateTopArea(self): + def updateTopArea(self) -> None: self.updateCardNames() - def updateCardNames(self): + def updateCardNames(self) -> None: self.ignore_change_signals = True combo = self.topAreaForm.templatesBox combo.clear() @@ -170,7 +171,7 @@ class CardLayout(QDialog): s += "+..." return s - def setupShortcuts(self): + def setupShortcuts(self) -> None: self.tform.front_button.setToolTip(shortcut("Ctrl+1")) self.tform.back_button.setToolTip(shortcut("Ctrl+2")) self.tform.style_button.setToolTip(shortcut("Ctrl+3")) @@ -193,7 +194,7 @@ class CardLayout(QDialog): # Main area setup ########################################################################## - def setupMainArea(self): + def setupMainArea(self) -> None: split = self.mainArea = QSplitter() split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) split.setOrientation(Qt.Horizontal) @@ -216,7 +217,7 @@ class CardLayout(QDialog): split.addWidget(right) split.setCollapsible(1, False) - def setup_edit_area(self): + def setup_edit_area(self) -> None: tform = self.tform tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE)) @@ -248,7 +249,7 @@ class CardLayout(QDialog): qconnect(widg.textChanged, self.on_search_changed) qconnect(widg.returnPressed, self.on_search_next) - def setup_cloze_number_box(self): + def setup_cloze_number_box(self) -> None: names = (tr(TR.CARD_TEMPLATES_CLOZE, val=n) for n in self.cloze_numbers) self.pform.cloze_number_combo.addItems(names) try: @@ -266,7 +267,7 @@ class CardLayout(QDialog): self.have_autoplayed = False self._renderPreview() - def on_editor_toggled(self): + def on_editor_toggled(self) -> None: if self.tform.front_button.isChecked(): self.current_editor_index = 0 self.pform.preview_front.setChecked(True) @@ -297,7 +298,7 @@ class CardLayout(QDialog): text = self.tform.search_edit.text() self.on_search_changed(text) - def setup_preview(self): + def setup_preview(self) -> None: pform = self.pform self.preview_web = AnkiWebView(title="card layout") pform.verticalLayout.addWidget(self.preview_web) @@ -336,7 +337,7 @@ class CardLayout(QDialog): self.cloze_numbers = [] self.pform.cloze_number_combo.setHidden(True) - def on_fill_empty_action_toggled(self): + def on_fill_empty_action_toggled(self) -> None: self.fill_empty_action_toggled = not self.fill_empty_action_toggled self.on_preview_toggled() @@ -344,11 +345,11 @@ class CardLayout(QDialog): self.night_mode_is_enabled = not self.night_mode_is_enabled self.on_preview_toggled() - def on_mobile_class_action_toggled(self): + def on_mobile_class_action_toggled(self) -> None: self.mobile_emulation_enabled = not self.mobile_emulation_enabled self.on_preview_toggled() - def on_preview_settings(self): + def on_preview_settings(self) -> None: m = QMenu(self) a = m.addAction(tr(TR.CARD_TEMPLATES_FILL_EMPTY)) @@ -370,7 +371,7 @@ class CardLayout(QDialog): m.exec_(self.pform.preview_settings.mapToGlobal(QPoint(0, 0))) - def on_preview_toggled(self): + def on_preview_toggled(self) -> None: self.have_autoplayed = False self._renderPreview() @@ -388,7 +389,7 @@ class CardLayout(QDialog): # Buttons ########################################################################## - def setupButtons(self): + def setupButtons(self) -> None: l = self.buttons = QHBoxLayout() help = QPushButton(tr(TR.ACTIONS_HELP)) help.setAutoDefault(False) @@ -424,7 +425,7 @@ class CardLayout(QDialog): return self.templates[0] return self.templates[self.ord] - def fill_fields_from_template(self): + def fill_fields_from_template(self) -> None: t = self.current_template() self.ignore_change_signals = True @@ -438,7 +439,7 @@ class CardLayout(QDialog): self.tform.edit_area.setPlainText(text) self.ignore_change_signals = False - def write_edits_to_template_and_redraw(self): + def write_edits_to_template_and_redraw(self) -> None: if self.ignore_change_signals: return @@ -458,14 +459,14 @@ class CardLayout(QDialog): # Preview ########################################################################## - _previewTimer = None + _previewTimer: Optional[QTimer] = None - def renderPreview(self): + def renderPreview(self) -> None: # schedule a preview when timing stops self.cancelPreviewTimer() self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False) - def cancelPreviewTimer(self): + def cancelPreviewTimer(self) -> None: if self._previewTimer: self._previewTimer.stop() self._previewTimer = None @@ -512,7 +513,7 @@ class CardLayout(QDialog): self.updateCardNames() - def maybeTextInput(self, txt, type="q"): + def maybeTextInput(self, txt: str, type: str = "q") -> str: if "[[type:" not in txt: return txt origLen = len(txt) @@ -668,7 +669,7 @@ class CardLayout(QDialog): dst["qfmt"] = m.group(2).strip() return True - def onMore(self): + def onMore(self) -> None: m = QMenu(self) if not self._isCloze(): @@ -699,7 +700,7 @@ class CardLayout(QDialog): m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0))) - def onBrowserDisplay(self): + def onBrowserDisplay(self) -> None: d = QDialog() disable_help_button(d) f = aqt.forms.browserdisp.Ui_Dialog() @@ -714,7 +715,7 @@ class CardLayout(QDialog): qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f)) d.exec_() - def onBrowserDisplayOk(self, f): + def onBrowserDisplayOk(self, f: Ui_Dialog) -> None: t = self.current_template() self.change_tracker.mark_basic() t["bqfmt"] = f.qfmt.text().strip() diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index baa022243..ff0ed557c 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -1,6 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import aqt from anki.collection import SearchTerm from anki.consts import * @@ -35,7 +36,7 @@ class CustomStudy(QDialog): f.radioNew.click() self.exec_() - def setupSignals(self): + def setupSignals(self) -> None: f = self.form qconnect(f.radioNew.clicked, lambda: self.onRadioChange(RADIO_NEW)) qconnect(f.radioRev.clicked, lambda: self.onRadioChange(RADIO_REV)) @@ -44,7 +45,7 @@ class CustomStudy(QDialog): qconnect(f.radioPreview.clicked, lambda: self.onRadioChange(RADIO_PREVIEW)) qconnect(f.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM)) - def onRadioChange(self, idx): + def onRadioChange(self, idx: int) -> None: f = self.form sp = f.spin smin = 1 @@ -123,7 +124,7 @@ class CustomStudy(QDialog): f.buttonBox.button(QDialogButtonBox.Ok).setText(ok) self.radioIdx = idx - def accept(self): + def accept(self) -> None: f = self.form i = self.radioIdx spin = f.spin.value() @@ -132,13 +133,15 @@ class CustomStudy(QDialog): self.mw.col.decks.save(self.deck) self.mw.col.sched.extendLimits(spin, 0) self.mw.reset() - return QDialog.accept(self) + QDialog.accept(self) + return elif i == RADIO_REV: self.deck["extendRev"] = spin self.mw.col.decks.save(self.deck) self.mw.col.sched.extendLimits(0, spin) self.mw.reset() - return QDialog.accept(self) + QDialog.accept(self) + return elif i == RADIO_CRAM: tags = self._getTags() # the rest create a filtered deck @@ -146,7 +149,8 @@ class CustomStudy(QDialog): if cur: if not cur["dyn"]: showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK)) - return QDialog.accept(self) + QDialog.accept(self) + return else: # safe to empty self.mw.col.sched.empty_filtered_deck(cur["id"]) @@ -211,7 +215,8 @@ class CustomStudy(QDialog): # generate cards self.created_custom_study = True if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]): - return showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU)) + showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU)) + return self.mw.moveToState("overview") QDialog.accept(self) @@ -222,7 +227,7 @@ class CustomStudy(QDialog): # fixme: clean up the empty custom study deck QDialog.reject(self) - def _getTags(self): + def _getTags(self) -> str: from aqt.taglimit import TagLimit return TagLimit(self.mw, self).tags diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 12b5180d4..0082537d5 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -1,12 +1,13 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import List, Optional +from typing import Dict, List, Optional import aqt from anki.collection import SearchTerm from anki.errors import InvalidInput from anki.lang import without_unicode_isolation +from aqt.main import AnkiQt from aqt.qt import * from aqt.utils import ( TR, @@ -23,7 +24,13 @@ from aqt.utils import ( class DeckConf(QDialog): - def __init__(self, mw, first=False, search="", deck=None): + def __init__( + self, + mw: AnkiQt, + first: bool = False, + search: str = "", + deck: Optional[Dict] = None, + ) -> None: QDialog.__init__(self, mw) self.mw = mw self.deck = deck or self.mw.col.decks.current() @@ -65,7 +72,7 @@ class DeckConf(QDialog): self.exec_() saveGeom(self, "dyndeckconf") - def initialSetup(self): + def initialSetup(self) -> None: import anki.consts as cs self.form.order.addItems(list(cs.dynOrderLabels(self.mw.col).values())) @@ -73,12 +80,12 @@ class DeckConf(QDialog): qconnect(self.form.resched.stateChanged, self._onReschedToggled) - def _onReschedToggled(self, _state): + def _onReschedToggled(self, _state: int) -> None: self.form.previewDelayWidget.setVisible( not self.form.resched.isChecked() and self.mw.col.schedVer() > 1 ) - def loadConf(self): + def loadConf(self) -> None: f = self.form d = self.deck @@ -113,7 +120,7 @@ class DeckConf(QDialog): f.secondFilter.setChecked(False) f.filter2group.setVisible(False) - def saveConf(self): + def saveConf(self) -> None: f = self.form d = self.deck d["resched"] = f.resched.isChecked() @@ -142,7 +149,7 @@ class DeckConf(QDialog): self.ok = False QDialog.reject(self) - def accept(self): + def accept(self) -> None: try: self.saveConf() except InvalidInput as err: diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index d1bbc3491..f066c76ae 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -52,10 +52,10 @@ class EditCurrent(QDialog): tooltip("Please finish editing the existing card first.") self.onReset() - def reject(self): + def reject(self) -> None: self.saveAndClose() - def saveAndClose(self): + def saveAndClose(self) -> None: self.editor.saveNow(self._saveAndClose) def _saveAndClose(self) -> None: diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index b9e94d8de..aa1016776 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -68,7 +68,7 @@ class EmptyCardsDialog(QDialog): def _on_note_link_clicked(self, link): self.mw.browser_search(link) - def _on_delete(self): + def _on_delete(self) -> None: self.mw.progress.start() def delete(): diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index bfbf12ab8..485ddc898 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -5,10 +5,12 @@ import html import re import sys import traceback +from typing import Optional from markdown import markdown from aqt import mw +from aqt.main import AnkiQt from aqt.qt import * from aqt.utils import TR, showText, showWarning, supportText, tr @@ -29,20 +31,20 @@ class ErrorHandler(QObject): errorTimer = pyqtSignal() - def __init__(self, mw): + def __init__(self, mw: AnkiQt) -> None: QObject.__init__(self, mw) self.mw = mw - self.timer = None + self.timer: Optional[QTimer] = None qconnect(self.errorTimer, self._setTimer) self.pool = "" self._oldstderr = sys.stderr sys.stderr = self - def unload(self): + def unload(self) -> None: sys.stderr = self._oldstderr sys.excepthook = None - def write(self, data): + def write(self, data: str) -> None: # dump to stdout sys.stdout.write(data) # save in buffer @@ -50,12 +52,12 @@ class ErrorHandler(QObject): # and update timer self.setTimer() - def setTimer(self): + def setTimer(self) -> None: # we can't create a timer from a different thread, so we post a # message to the object on the main thread self.errorTimer.emit() # type: ignore - def _setTimer(self): + def _setTimer(self) -> None: if not self.timer: self.timer = QTimer(self.mw) qconnect(self.timer.timeout, self.onTimeout) @@ -66,7 +68,7 @@ class ErrorHandler(QObject): def tempFolderMsg(self): return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER) - def onTimeout(self): + def onTimeout(self) -> None: error = html.escape(self.pool) self.pool = "" self.mw.progress.clear() @@ -75,15 +77,19 @@ class ErrorHandler(QObject): if "DeprecationWarning" in error: return if "10013" in error: - return showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS)) + showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS)) + return if "no default input" in error.lower(): - return showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE)) + showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE)) + return if "invalidTempFolder" in error: - return showWarning(self.tempFolderMsg()) + showWarning(self.tempFolderMsg()) + return if "Beautiful Soup is not an HTTP client" in error: return if "database or disk is full" in error or "Errno 28" in error: - return showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL)) + showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL)) + return if "disk I/O error" in error: showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB))) return diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index 6881dbd19..dd3157365 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -71,7 +71,7 @@ class ExportDialog(QDialog): index = self.frm.deck.findText(name) self.frm.deck.setCurrentIndex(index) - def exporterChanged(self, idx): + def exporterChanged(self, idx: int) -> None: self.exporter = self.exporters[idx][1](self.col) self.isApkg = self.exporter.ext == ".apkg" self.isVerbatim = getattr(self.exporter, "verbatim", False) @@ -94,7 +94,7 @@ class ExportDialog(QDialog): # show deck list? self.frm.deck.setVisible(not self.isVerbatim) - def accept(self): + def accept(self) -> None: self.exporter.includeSched = self.frm.includeSched.isChecked() self.exporter.includeMedia = self.frm.includeMedia.isChecked() self.exporter.includeTags = self.frm.includeTags.isChecked() @@ -177,7 +177,7 @@ class ExportDialog(QDialog): self.mw.taskman.run_in_background(do_export, on_done) - def on_export_finished(self): + def on_export_finished(self) -> None: if self.isVerbatim: msg = tr(TR.EXPORTING_COLLECTION_EXPORTED) self.mw.reopen() diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 2cf6170ba..33b448dba 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -41,7 +41,7 @@ class FieldDialog(QDialog): self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) - self.currentIdx = None + self.currentIdx: Optional[int] = None self.oldSortField = self.model["sortf"] self.fillFields() self.setupSignals() @@ -52,13 +52,13 @@ class FieldDialog(QDialog): ########################################################################## - def fillFields(self): + def fillFields(self) -> None: self.currentIdx = None self.form.fieldList.clear() for c, f in enumerate(self.model["flds"]): self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"])) - def setupSignals(self): + def setupSignals(self) -> None: f = self.form qconnect(f.fieldList.currentRowChanged, self.onRowChange) qconnect(f.fieldAdd.clicked, self.onAdd) @@ -86,29 +86,31 @@ class FieldDialog(QDialog): movePos -= 1 self.moveField(movePos + 1) # convert to 1 based. - def onRowChange(self, idx): + def onRowChange(self, idx: int) -> None: if idx == -1: return self.saveField() self.loadField(idx) - def _uniqueName(self, prompt, ignoreOrd=None, old=""): + def _uniqueName( + self, prompt: str, ignoreOrd: Optional[int] = None, old: str = "" + ) -> Optional[str]: txt = getOnlyText(prompt, default=old).replace('"', "").strip() if not txt: - return + return None if txt[0] in "#^/": showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID)) - return + return None for letter in """:{"}""": if letter in txt: showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER)) - return + return None for f in self.model["flds"]: if ignoreOrd is not None and f["ord"] == ignoreOrd: continue if f["name"] == txt: showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED)) - return + return None return txt def onRename(self): @@ -127,7 +129,7 @@ class FieldDialog(QDialog): self.fillFields() self.form.fieldList.setCurrentRow(idx) - def onAdd(self): + def onAdd(self) -> None: name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME)) if not name: return @@ -185,7 +187,7 @@ class FieldDialog(QDialog): self.fillFields() self.form.fieldList.setCurrentRow(pos - 1) - def loadField(self, idx): + def loadField(self, idx: int) -> None: self.currentIdx = idx fld = self.model["flds"][idx] f = self.form @@ -195,7 +197,7 @@ class FieldDialog(QDialog): f.sortField.setChecked(self.model["sortf"] == fld["ord"]) f.rtl.setChecked(fld["rtl"]) - def saveField(self): + def saveField(self) -> None: # not initialized yet? if self.currentIdx is None: return @@ -219,14 +221,14 @@ class FieldDialog(QDialog): fld["rtl"] = rtl self.change_tracker.mark_basic() - def reject(self): + def reject(self) -> None: if self.change_tracker.changed(): if not askUser("Discard changes?"): return QDialog.reject(self) - def accept(self): + def accept(self) -> None: self.saveField() def save(): diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index ed69fc2b9..e0b77df11 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -9,12 +9,13 @@ import traceback import unicodedata import zipfile from concurrent.futures import Future -from typing import Optional +from typing import Any, Optional import anki.importing as importing import aqt.deckchooser import aqt.forms import aqt.modelchooser +from anki.importing.apkg import AnkiPackageImporter from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.utils import ( @@ -106,14 +107,14 @@ class ImportDialog(QDialog): self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) self.exec_() - def setupOptions(self): + def setupOptions(self) -> None: self.model = self.mw.col.models.current() self.modelChooser = aqt.modelchooser.ModelChooser( self.mw, self.frm.modelArea, label=False ) self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False) - def modelChanged(self, unused=None): + def modelChanged(self, unused: Any = None) -> None: self.importer.model = self.mw.col.models.current() self.importer.initMapping() self.showMapping() @@ -142,7 +143,7 @@ class ImportDialog(QDialog): self.showMapping(hook=updateDelim) self.updateDelimiterButtonText() - def updateDelimiterButtonText(self): + def updateDelimiterButtonText(self) -> None: if not self.importer.needDelimiter: return if self.importer.delimiter: @@ -164,7 +165,7 @@ class ImportDialog(QDialog): txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val=d) self.frm.autoDetect.setText(txt) - def accept(self): + def accept(self) -> None: self.importer.mapping = self.mapping if not self.importer.mappingOk(): showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE)) @@ -211,19 +212,21 @@ class ImportDialog(QDialog): self.mw.taskman.run_in_background(self.importer.run, on_done) - def setupMappingFrame(self): + def setupMappingFrame(self) -> None: # qt seems to have a bug with adding/removing from a grid, so we add # to a separate object and add/remove that instead self.frame = QFrame(self.frm.mappingArea) self.frm.mappingArea.setWidget(self.frame) self.mapbox = QVBoxLayout(self.frame) self.mapbox.setContentsMargins(0, 0, 0, 0) - self.mapwidget = None + self.mapwidget: Optional[QWidget] = None def hideMapping(self): self.frm.mappingGroup.hide() - def showMapping(self, keepMapping=False, hook=None): + def showMapping( + self, keepMapping: bool = False, hook: Optional[Callable] = None + ) -> None: if hook: hook() if not keepMapping: @@ -295,7 +298,7 @@ def showUnicodeWarning(): showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8)) -def onImport(mw): +def onImport(mw: AnkiQt) -> None: filt = ";;".join([x[0] for x in importing.Importers]) file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt) if not file: @@ -314,7 +317,7 @@ def onImport(mw): importFile(mw, file) -def importFile(mw, file): +def importFile(mw: AnkiQt, file: str) -> None: importerClass = None done = False for i in importing.Importers: @@ -406,7 +409,7 @@ def invalidZipMsg(): return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO) -def setupApkgImport(mw, importer): +def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool: base = os.path.basename(importer.file).lower() full = ( (base == "collection.apkg") @@ -424,6 +427,7 @@ def setupApkgImport(mw, importer): return False replaceWithApkg(mw, importer.file, mw.restoringBackup) + return False def replaceWithApkg(mw, file, backup): diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 591df9048..c549e5052 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1141,7 +1141,7 @@ title="%s" %s>%s""" % ( # Cramming ########################################################################## - def onCram(self, search=""): + def onCram(self, search: str = "") -> None: import aqt.dyndeckconf n = 1 diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 6609a5a7c..1af54e39d 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -26,7 +26,7 @@ from aqt.qt import * from aqt.utils import aqt_data_folder -def _getExportFolder(): +def _getExportFolder() -> str: data_folder = aqt_data_folder() webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web")) if os.path.exists(webInSrcFolder): @@ -83,7 +83,7 @@ class MediaServer(threading.Thread): if not self.is_shutdown: raise - def shutdown(self): + def shutdown(self) -> None: self.is_shutdown = True sockets = list(self.server._map.values()) # type: ignore for socket in sockets: @@ -91,7 +91,7 @@ class MediaServer(threading.Thread): # https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104 self.server.task_dispatcher.shutdown() - def getPort(self): + def getPort(self) -> int: self._ready.wait() return int(self.server.effective_port) # type: ignore diff --git a/qt/aqt/models.py b/qt/aqt/models.py index bc398a5ba..df8154ee7 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -57,7 +57,7 @@ class Models(QDialog): # Models ########################################################################## - def maybe_select_provided_notetype(self): + def maybe_select_provided_notetype(self) -> None: if not self.selected_notetype_id: self.form.modelsList.setCurrentRow(0) return diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 088065c95..e2756bbc1 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional +from typing import Any, Callable, Dict, List, Optional, Tuple import aqt from anki.collection import SearchTerm @@ -45,13 +45,13 @@ class Overview: self.web = mw.web self.bottom = BottomBar(mw, mw.bottomWeb) - def show(self): + def show(self) -> None: av_player.stop_and_clear_queue() self.web.set_bridge_command(self._linkHandler, self) self.mw.setStateShortcuts(self._shortcutKeys()) self.refresh() - def refresh(self): + def refresh(self) -> None: self.mw.col.reset() self._renderPage() self._renderBottom() @@ -61,7 +61,7 @@ class Overview: # Handlers ############################################################ - def _linkHandler(self, url): + def _linkHandler(self, url: str) -> bool: if url == "study": self.mw.col.startTimebox() self.mw.moveToState("review") @@ -92,7 +92,7 @@ class Overview: openLink(url) return False - def _shortcutKeys(self): + def _shortcutKeys(self) -> List[Tuple[str, Callable]]: return [ ("o", self.mw.onDeckConf), ("r", self.onRebuildKey), @@ -101,7 +101,7 @@ class Overview: ("u", self.onUnbury), ] - def _filteredDeck(self): + def _filteredDeck(self) -> int: return self.mw.col.decks.current()["dyn"] def onRebuildKey(self): @@ -114,7 +114,7 @@ class Overview: self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected()) self.mw.reset() - def onCustomStudyKey(self): + def onCustomStudyKey(self) -> None: if not self._filteredDeck(): self.onStudyMore() @@ -150,7 +150,7 @@ class Overview: # HTML ############################################################ - def _renderPage(self): + def _renderPage(self) -> None: but = self.mw.button deck = self.mw.col.decks.current() self.sid = deck.get("sharedFrom") @@ -177,10 +177,10 @@ class Overview: context=self, ) - def _show_finished_screen(self): + def _show_finished_screen(self) -> None: self.web.load_ts_page("congrats") - def _desc(self, deck): + def _desc(self, deck: Dict[str, Any]) -> str: if deck["dyn"]: desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR) desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO) @@ -229,7 +229,7 @@ class Overview: # Bottom area ###################################################################### - def _renderBottom(self): + def _renderBottom(self) -> None: links = [ ["O", "opts", tr(TR.ACTIONS_OPTIONS)], ] @@ -256,7 +256,7 @@ class Overview: # Studying more ###################################################################### - def onStudyMore(self): + def onStudyMore(self) -> None: import aqt.customstudy aqt.customstudy.CustomStudy(self.mw) diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 5c5b70d4d..7557a6412 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -4,7 +4,7 @@ import json import re import time -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Optional, Tuple, Union from anki.cards import Card from anki.collection import ConfigBoolKey @@ -19,6 +19,7 @@ from aqt.qt import ( QPixmap, QShortcut, Qt, + QTimer, QVBoxLayout, QWidget, qconnect, @@ -29,12 +30,14 @@ from aqt.theme import theme_manager from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr from aqt.webview import AnkiWebView +LastStateAndMod = Tuple[str, int, int] + class Previewer(QDialog): - _last_state = None + _last_state: Optional[LastStateAndMod] = None _card_changed = False _last_render: Union[int, float] = 0 - _timer = None + _timer: Optional[QTimer] = None _show_both_sides = False def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]): @@ -54,7 +57,7 @@ class Previewer(QDialog): def card_changed(self) -> bool: raise NotImplementedError - def open(self): + def open(self) -> None: self._state = "question" self._last_state = None self._create_gui() @@ -62,7 +65,7 @@ class Previewer(QDialog): self.render_card() self.show() - def _create_gui(self): + def _create_gui(self) -> None: self.setWindowTitle(tr(TR.ACTIONS_PREVIEW)) self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self) @@ -98,25 +101,25 @@ class Previewer(QDialog): self.setLayout(self.vbox) restoreGeom(self, "preview") - def _on_finished(self, ok): + def _on_finished(self, ok: int) -> None: saveGeom(self, "preview") self.mw.progress.timer(100, self._on_close, False) - def _on_replay_audio(self): + def _on_replay_audio(self) -> None: if self._state == "question": replay_audio(self.card(), True) elif self._state == "answer": replay_audio(self.card(), False) - def close(self): + def close(self) -> None: self._on_close() super().close() - def _on_close(self): + def _on_close(self) -> None: self._open = False self._close_callback() - def _setup_web_view(self): + def _setup_web_view(self) -> None: jsinc = [ "js/vendor/jquery.min.js", "js/vendor/css_browser_selector.min.js", @@ -136,7 +139,7 @@ class Previewer(QDialog): if cmd.startswith("play:"): play_clicked_audio(cmd, self.card()) - def render_card(self): + def render_card(self) -> None: self.cancel_timer() # Keep track of whether render() has ever been called # with cardChanged=True since the last successful render @@ -151,7 +154,7 @@ class Previewer(QDialog): else: self._render_scheduled() - def cancel_timer(self): + def cancel_timer(self) -> None: if self._timer: self._timer.stop() self._timer = None @@ -214,7 +217,7 @@ class Previewer(QDialog): self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass)) self._card_changed = False - def _on_show_both_sides(self, toggle): + def _on_show_both_sides(self, toggle: bool) -> None: self._show_both_sides = toggle self.mw.col.set_config_bool(ConfigBoolKey.PREVIEW_BOTH_SIDES, toggle) self.mw.col.setMod() @@ -222,7 +225,7 @@ class Previewer(QDialog): self._state = "question" self.render_card() - def _state_and_mod(self): + def _state_and_mod(self) -> Tuple[str, int, int]: c = self.card() n = c.note() n.load() @@ -241,7 +244,7 @@ class MultiCardPreviewer(Previewer): # need to state explicitly it's not implement to avoid W0223 raise NotImplementedError - def _create_gui(self): + def _create_gui(self) -> None: super()._create_gui() self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole) self._prev.setAutoDefault(False) @@ -266,7 +269,7 @@ class MultiCardPreviewer(Previewer): def _on_prev_card(self): pass - def _on_next(self): + def _on_next(self) -> None: if self._state == "question": self._state = "answer" self.render_card() @@ -276,19 +279,19 @@ class MultiCardPreviewer(Previewer): def _on_next_card(self): pass - def _updateButtons(self): + def _updateButtons(self) -> None: if not self._open: return self._prev.setEnabled(self._should_enable_prev()) self._next.setEnabled(self._should_enable_next()) - def _should_enable_prev(self): + def _should_enable_prev(self) -> bool: return self._state == "answer" and not self._show_both_sides - def _should_enable_next(self): + def _should_enable_next(self) -> bool: return self._state == "question" - def _on_close(self): + def _on_close(self) -> None: super()._on_close() self._prev = None self._next = None @@ -317,15 +320,15 @@ class BrowserPreviewer(MultiCardPreviewer): lambda: self._parent._moveCur(QAbstractItemView.MoveUp) ) - def _on_next_card(self): + def _on_next_card(self) -> None: self._parent.editor.saveNow( lambda: self._parent._moveCur(QAbstractItemView.MoveDown) ) - def _should_enable_prev(self): + def _should_enable_prev(self) -> bool: return super()._should_enable_prev() or self._parent.currentRow() > 0 - def _should_enable_next(self): + def _should_enable_next(self) -> bool: return ( super()._should_enable_next() or self._parent.currentRow() < self._parent.model.rowCount(None) - 1 diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index af548c5c3..6a42e4938 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -119,11 +119,11 @@ class AnkiRestart(SystemExit): class ProfileManager: - def __init__(self, base=None): + def __init__(self, base: Optional[str] = None) -> None: ## Settings which should be forgotten each Anki restart - self.session = {} - self.name = None - self.db = None + self.session: Dict[str, Any] = {} + self.name: Optional[str] = None + self.db: Optional[DB] = None self.profile: Optional[Dict] = None # instantiate base folder self.base: str @@ -170,7 +170,7 @@ class ProfileManager: return p return os.path.expanduser("~/Documents/Anki") - def maybeMigrateFolder(self): + def maybeMigrateFolder(self) -> None: newBase = self.base oldBase = self._oldFolderLocation() @@ -206,7 +206,7 @@ class ProfileManager: confirmation.setText( "Anki needs to move its data folder from Documents/Anki to a new location. Proceed?" ) - retval = confirmation.exec() + retval = confirmation.exec_() if retval == QMessageBox.Ok: progress = QMessageBox() @@ -228,7 +228,7 @@ class ProfileManager: completion.setWindowTitle(window_title) completion.setText("Migration complete. Please start Anki again.") completion.show() - completion.exec() + completion.exec_() else: diag = QMessageBox() diag.setIcon(QMessageBox.Warning) @@ -239,7 +239,7 @@ class ProfileManager: "Migration aborted. If you would like to keep the old folder location, please " "see the Startup Options section of the manual. Anki will now quit." ) - diag.exec() + diag.exec_() raise AnkiRestart(exitcode=0) @@ -424,7 +424,7 @@ class ProfileManager: os.makedirs(path) return path - def _setBaseFolder(self, cmdlineBase: None) -> None: + def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None: if cmdlineBase: self.base = os.path.abspath(cmdlineBase) elif os.environ.get("ANKI_BASE"): diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index 2754f9b34..47229a3ee 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -4,7 +4,7 @@ from __future__ import annotations import time -from typing import Optional +from typing import Callable, Optional import aqt.forms from aqt.qt import * @@ -15,13 +15,13 @@ from aqt.utils import TR, disable_help_button, tr class ProgressManager: - def __init__(self, mw): + def __init__(self, mw: aqt.AnkiQt) -> None: self.mw = mw self.app = QApplication.instance() self.inDB = False self.blockUpdates = False self._show_timer: Optional[QTimer] = None - self._win = None + self._win: Optional[ProgressDialog] = None self._levels = 0 # Safer timers @@ -29,7 +29,9 @@ class ProgressManager: # A custom timer which avoids firing while a progress dialog is active # (likely due to some long-running DB operation) - def timer(self, ms, func, repeat, requiresCollection=True): + def timer( + self, ms: int, func: Callable, repeat: bool, requiresCollection: bool = True + ) -> QTimer: """Create and start a standard Anki timer. If the timer fires while a progress window is shown: @@ -136,7 +138,7 @@ class ProgressManager: self._updating = False self._lastUpdate = time.time() - def finish(self): + def finish(self) -> None: self._levels -= 1 self._levels = max(0, self._levels) if self._levels == 0: @@ -147,13 +149,13 @@ class ProgressManager: self._show_timer.stop() self._show_timer = None - def clear(self): + def clear(self) -> None: "Restore the interface after an error." if self._levels: self._levels = 1 self.finish() - def _maybeShow(self): + def _maybeShow(self) -> None: if not self._levels: return if self._shown: @@ -181,17 +183,17 @@ class ProgressManager: self._win = None self._shown = 0 - def _setBusy(self): + def _setBusy(self) -> None: self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor)) - def _unsetBusy(self): + def _unsetBusy(self) -> None: self.app.restoreOverrideCursor() - def busy(self): + def busy(self) -> int: "True if processing." return self._levels - def _on_show_timer(self): + def _on_show_timer(self) -> None: self._show_timer = None self._showWin() @@ -209,7 +211,7 @@ class ProgressManager: class ProgressDialog(QDialog): - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: QDialog.__init__(self, parent) disable_help_button(self) self.form = aqt.forms.progress.Ui_Dialog() @@ -219,7 +221,7 @@ class ProgressDialog(QDialog): # required for smooth progress bars self.form.progressBar.setStyleSheet("QProgressBar::chunk { width: 1px; }") - def cancel(self): + def cancel(self) -> None: self._closingDown = True self.hide() diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a5f12e381..8a3e05998 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -8,7 +8,7 @@ import html import json import re import unicodedata as ucd -from typing import Callable, List, Optional, Tuple, Union +from typing import Any, Callable, List, Optional, Tuple, Union from PyQt5.QtCore import Qt @@ -697,7 +697,7 @@ time = %(time)d; ########################################################################## # note the shortcuts listed here also need to be defined above - def _contextMenu(self): + def _contextMenu(self) -> List[Any]: currentFlag = self.card and self.card.userFlag() opts = [ [ diff --git a/qt/aqt/schema_change_tracker.py b/qt/aqt/schema_change_tracker.py index 59e9c955c..e489f6de6 100644 --- a/qt/aqt/schema_change_tracker.py +++ b/qt/aqt/schema_change_tracker.py @@ -20,7 +20,7 @@ class ChangeTracker: def __init__(self, mw: AnkiQt): self.mw = mw - def mark_basic(self): + def mark_basic(self) -> None: if self._changed == Change.NO_CHANGE: self._changed = Change.BASIC_CHANGE diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 2903f1917..d9fe1c870 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -54,7 +54,7 @@ class NewDeckStats(QDialog): self.form.web.set_bridge_command(self._on_bridge_cmd, self) self.activateWindow() - def reject(self): + def reject(self) -> None: self.form.web = None saveGeom(self, self.name) aqt.dialogs.markClosed("NewDeckStats") @@ -98,14 +98,14 @@ class NewDeckStats(QDialog): return False - def refresh(self): + def refresh(self) -> None: self.form.web.load_ts_page("graphs") class DeckStats(QDialog): """Legacy deck stats, used by some add-ons.""" - def __init__(self, mw): + def __init__(self, mw: aqt.main.AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) mw.setupDialogGC(self) self.mw = mw @@ -143,7 +143,7 @@ class DeckStats(QDialog): self.refresh() self.activateWindow() - def reject(self): + def reject(self) -> None: self.form.web = None saveGeom(self, self.name) aqt.dialogs.markClosed("DeckStats") @@ -173,15 +173,15 @@ class DeckStats(QDialog): self.form.web.page().printToPdf(path) tooltip(tr(TR.STATISTICS_SAVED)) - def changePeriod(self, n): + def changePeriod(self, n: int) -> None: self.period = n self.refresh() - def changeScope(self, type): + def changeScope(self, type: str) -> None: self.wholeCollection = type == "collection" self.refresh() - def refresh(self): + def refresh(self) -> None: self.mw.progress.start(parent=self) stats = self.mw.col.stats() stats.wholeCollection = self.wholeCollection diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 106a38635..5ee750687 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional + import aqt from aqt import gui_hooks from aqt.qt import * @@ -109,7 +111,7 @@ class StudyDeck(QDialog): return True return False - def redraw(self, filt, focus=None): + def redraw(self, filt: str, focus: Optional[str] = None) -> None: self.filt = filt self.focus = focus self.names = [n for n in self.origNames if self._matches(n, filt)] @@ -123,7 +125,7 @@ class StudyDeck(QDialog): l.setCurrentRow(idx) l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter) - def _matches(self, name, filt): + def _matches(self, name: str, filt: str) -> bool: name = name.lower() filt = filt.lower() if not filt: diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index a0ba44b26..ff81f9eae 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -4,7 +4,9 @@ from __future__ import annotations import re +from typing import Iterable, List, Optional, Union +from anki.collection import Collection from aqt import gui_hooks from aqt.qt import * @@ -15,9 +17,9 @@ class TagEdit(QLineEdit): lostFocus = pyqtSignal() # 0 = tags, 1 = decks - def __init__(self, parent, type=0): + def __init__(self, parent: QDialog, type: int = 0) -> None: QLineEdit.__init__(self, parent) - self.col = None + self.col: Optional[Collection] = None self.model = QStringListModel() self.type = type if type == 0: @@ -28,19 +30,20 @@ class TagEdit(QLineEdit): self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.setCompleter(self.completer) - def setCol(self, col): + def setCol(self, col: Collection) -> None: "Set the current col, updating list of available tags." self.col = col + l: Iterable[str] if self.type == 0: l = self.col.tags.all() else: l = (d.name for d in self.col.decks.all_names_and_ids()) self.model.setStringList(l) - def focusInEvent(self, evt): + def focusInEvent(self, evt: QFocusEvent) -> None: QLineEdit.focusInEvent(self, evt) - def keyPressEvent(self, evt): + def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() in (Qt.Key_Up, Qt.Key_Down): # show completer on arrow key up/down if not self.completer.popup().isVisible(): @@ -85,7 +88,7 @@ class TagEdit(QLineEdit): self.showCompleter() gui_hooks.tag_editor_did_process_key(self, evt) - def showCompleter(self): + def showCompleter(self) -> None: self.completer.setCompletionPrefix(self.text()) self.completer.complete() @@ -94,20 +97,26 @@ class TagEdit(QLineEdit): self.lostFocus.emit() # type: ignore self.completer.popup().hide() - def hideCompleter(self): + def hideCompleter(self) -> None: if sip.isdeleted(self.completer): return self.completer.popup().hide() class TagCompleter(QCompleter): - def __init__(self, model, parent, edit, *args): + def __init__( + self, + model: QStringListModel, + parent: QWidget, + edit: TagEdit, + *args, + ) -> None: QCompleter.__init__(self, model, parent) - self.tags = [] + self.tags: List[str] = [] self.edit = edit - self.cursor = None + self.cursor: Optional[int] = None - def splitPath(self, tags): + def splitPath(self, tags: str) -> List[str]: stripped_tags = tags.strip() stripped_tags = re.sub(" +", " ", stripped_tags) self.tags = self.edit.col.tags.split(stripped_tags) @@ -119,7 +128,7 @@ class TagCompleter(QCompleter): self.cursor = stripped_tags.count(" ", 0, p) return [self.tags[self.cursor]] - def pathFromIndex(self, idx): + def pathFromIndex(self, idx: QModelIndex) -> str: if self.cursor is None: return self.edit.text() ret = QCompleter.pathFromIndex(self, idx) diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index cd79dedce..c28392a6d 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -3,14 +3,17 @@ from typing import List, Optional import aqt +from aqt.customstudy import CustomStudy +from aqt.main import AnkiQt from aqt.qt import * from aqt.utils import disable_help_button, restoreGeom, saveGeom class TagLimit(QDialog): - def __init__(self, mw, parent): + def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None: QDialog.__init__(self, parent, Qt.Window) - self.tags: Union[str, List] = "" + self.tags: str = "" + self.tags_list: List[str] = [] self.mw = mw self.parent: Optional[QWidget] = parent self.deck = self.parent.deck @@ -29,7 +32,7 @@ class TagLimit(QDialog): restoreGeom(self, "tagLimit") self.exec_() - def rebuildTagList(self): + def rebuildTagList(self) -> None: usertags = self.mw.col.tags.byDeck(self.deck["id"], True) yes = self.deck.get("activeTags", []) no = self.deck.get("inactiveTags", []) @@ -42,10 +45,10 @@ class TagLimit(QDialog): groupedTags = [] usertags.sort() groupedTags.append(usertags) - self.tags = [] + self.tags_list = [] for tags in groupedTags: for t in tags: - self.tags.append(t) + self.tags_list.append(t) item = QListWidgetItem(t.replace("_", " ")) self.dialog.activeList.addItem(item) if t in yesHash: @@ -69,7 +72,7 @@ class TagLimit(QDialog): self.tags = "" QDialog.reject(self) - def accept(self): + def accept(self) -> None: self.hide() # gather yes/no tags yes = [] @@ -80,12 +83,12 @@ class TagLimit(QDialog): item = self.dialog.activeList.item(c) idx = self.dialog.activeList.indexFromItem(item) if self.dialog.activeList.selectionModel().isSelected(idx): - yes.append(self.tags[c]) + yes.append(self.tags_list[c]) # inactive item = self.dialog.inactiveList.item(c) idx = self.dialog.inactiveList.indexFromItem(item) if self.dialog.inactiveList.selectionModel().isSelected(idx): - no.append(self.tags[c]) + no.append(self.tags_list[c]) # save in the deck for future invocations self.deck["activeTags"] = yes self.deck["inactiveTags"] = no diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index fa103199f..264b4f6c1 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -78,7 +78,7 @@ class TaskManager(QObject): self.run_in_background(task, wrapped_done) - def _on_closures_pending(self): + def _on_closures_pending(self) -> None: """Run any pending closures. This runs in the main thread.""" with self._closures_lock: closures = self._closures diff --git a/qt/aqt/update.py b/qt/aqt/update.py index d1178350a..32f956eb6 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -7,6 +7,7 @@ import requests import aqt from anki.utils import platDesc, versionWithBuild +from aqt.main import AnkiQt from aqt.qt import * from aqt.utils import TR, openLink, showText, tr @@ -17,7 +18,7 @@ class LatestVersionFinder(QThread): newMsg = pyqtSignal(dict) clockIsOff = pyqtSignal(float) - def __init__(self, main): + def __init__(self, main: AnkiQt) -> None: QThread.__init__(self) self.main = main self.config = main.pm.meta From a56b09b987515bb5f422d731cf93f2c0b385d10e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 1 Feb 2021 23:28:21 +1000 Subject: [PATCH 22/22] add a bunch of return types --- qt/aqt/__init__.py | 48 ++++++++------- qt/aqt/about.py | 10 ++-- qt/aqt/addcards.py | 10 ++-- qt/aqt/addons.py | 2 +- qt/aqt/clayout.py | 46 +++++++------- qt/aqt/customstudy.py | 2 +- qt/aqt/dbcheck.py | 6 +- qt/aqt/deckbrowser.py | 32 +++++----- qt/aqt/deckconf.py | 37 ++++++------ qt/aqt/dyndeckconf.py | 4 +- qt/aqt/editcurrent.py | 6 +- qt/aqt/emptycards.py | 10 ++-- qt/aqt/errors.py | 6 +- qt/aqt/exporting.py | 8 +-- qt/aqt/fields.py | 27 +++++---- qt/aqt/importing.py | 38 ++++++------ qt/aqt/legacy.py | 11 +++- qt/aqt/main.py | 102 +++++++++++++++++--------------- qt/aqt/mediacheck.py | 16 ++--- qt/aqt/mediasrv.py | 9 +-- qt/aqt/mediasync.py | 16 ++--- qt/aqt/modelchooser.py | 4 +- qt/aqt/models.py | 2 +- qt/aqt/overview.py | 8 +-- qt/aqt/preferences.py | 38 ++++++------ qt/aqt/previewer.py | 12 ++-- qt/aqt/profiles.py | 12 ++-- qt/aqt/progress.py | 6 +- qt/aqt/qt.py | 4 +- qt/aqt/reviewer.py | 10 ++-- qt/aqt/schema_change_tracker.py | 2 +- qt/aqt/sound.py | 44 +++++++------- qt/aqt/stats.py | 18 +++--- qt/aqt/studydeck.py | 2 +- qt/aqt/sync.py | 24 ++++---- qt/aqt/taglimit.py | 2 +- qt/aqt/taskman.py | 4 +- qt/aqt/tts.py | 6 +- qt/aqt/update.py | 9 +-- qt/aqt/webview.py | 42 ++++++------- qt/dmypy-watch.sh | 2 +- 41 files changed, 359 insertions(+), 338 deletions(-) diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 350a24446..03187804b 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -10,7 +10,7 @@ import os import sys import tempfile import traceback -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import anki.lang from anki import version as _version @@ -102,10 +102,10 @@ class DialogManager: self._dialogs[name][1] = instance return instance - def markClosed(self, name: str): + def markClosed(self, name: str) -> None: self._dialogs[name] = [self._dialogs[name][0], None] - def allClosed(self): + def allClosed(self) -> bool: return not any(x[1] for x in self._dialogs.values()) def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]: @@ -119,7 +119,7 @@ class DialogManager: if not instance: continue - def callback(): + def callback() -> None: if self.allClosed(): onsuccess() else: @@ -189,12 +189,12 @@ def setupLangAndBackend( pass # add _ and ngettext globals used by legacy code - def fn__(arg): + def fn__(arg) -> None: print("".join(traceback.format_stack()[-2])) print("_ global will break in the future; please see anki/lang.py") return arg - def fn_ngettext(a, b, c): + def fn_ngettext(a, b, c) -> None: print("".join(traceback.format_stack()[-2])) print("ngettext global will break in the future; please see anki/lang.py") return b @@ -244,11 +244,11 @@ class AnkiApp(QApplication): KEY = "anki" + checksum(getpass.getuser()) TMOUT = 30000 - def __init__(self, argv): + def __init__(self, argv) -> None: QApplication.__init__(self, argv) self._argv = argv - def secondInstance(self): + def secondInstance(self) -> bool: # we accept only one command line argument. if it's missing, send # a blank screen to just raise the existing window opts, args = parseArgs(self._argv) @@ -267,7 +267,7 @@ class AnkiApp(QApplication): self._srv.listen(self.KEY) return False - def sendMsg(self, txt): + def sendMsg(self, txt) -> bool: sock = QLocalSocket(self) sock.connectToServer(self.KEY, QIODevice.WriteOnly) if not sock.waitForConnected(self.TMOUT): @@ -286,7 +286,7 @@ class AnkiApp(QApplication): sock.disconnectFromServer() return True - def onRecv(self): + def onRecv(self) -> None: sock = self._srv.nextPendingConnection() if not sock.waitForReadyRead(self.TMOUT): sys.stderr.write(sock.errorString()) @@ -298,14 +298,14 @@ class AnkiApp(QApplication): # OS X file/url handler ################################################## - def event(self, evt): + def event(self, evt) -> bool: if evt.type() == QEvent.FileOpen: self.appMsg.emit(evt.file() or "raise") # type: ignore return True return QApplication.event(self, evt) -def parseArgs(argv): +def parseArgs(argv) -> Tuple[argparse.Namespace, List[str]]: "Returns (opts, args)." # py2app fails to strip this in some instances, then anki dies # as there's no such profile @@ -330,7 +330,7 @@ def parseArgs(argv): return parser.parse_known_args(argv[1:]) -def setupGL(pm): +def setupGL(pm) -> None: if isMac: return @@ -343,7 +343,7 @@ def setupGL(pm): ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL) # catch opengl errors - def msgHandler(category, ctx, msg): + def msgHandler(category, ctx, msg) -> None: if category == QtDebugMsg: category = "debug" elif category == QtInfoMsg: @@ -400,7 +400,7 @@ def setupGL(pm): PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE") -def write_profile_results(): +def write_profile_results() -> None: profiler.disable() profiler.dump_stats("anki.prof") @@ -408,7 +408,7 @@ def write_profile_results(): print("use 'bazel run qt:profile' to explore") -def run(): +def run() -> None: try: _run() except Exception as e: @@ -420,7 +420,7 @@ def run(): ) -def _run(argv=None, exec=True): +def _run(argv=None, exec=True) -> Optional[AnkiApp]: """Start AnkiQt application or reuse an existing instance if one exists. If the function is invoked with exec=False, the AnkiQt will not enter @@ -441,12 +441,12 @@ def _run(argv=None, exec=True): if opts.version: print(f"Anki {appVersion}") - return + return None elif opts.syncserver: from anki.syncserver import serve serve() - return + return None if PROFILE_CODE: @@ -465,7 +465,7 @@ def _run(argv=None, exec=True): except AnkiRestart as error: if error.exitcode: sys.exit(error.exitcode) - return + return None except: # will handle below traceback.print_exc() @@ -500,7 +500,7 @@ def _run(argv=None, exec=True): app = AnkiApp(argv) if app.secondInstance(): # we've signaled the primary instance, so we should close - return + return None if not pm: QMessageBox.critical( @@ -508,7 +508,7 @@ def _run(argv=None, exec=True): tr(TR.QT_MISC_ERROR), tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER), ) - return + return None # disable icons on mac; this must be done before window created if isMac: @@ -548,7 +548,7 @@ def _run(argv=None, exec=True): tr(TR.QT_MISC_ERROR), tr(TR.QT_MISC_NO_TEMP_FOLDER), ) - return + return None if pmLoadResult.firstTime: pm.setDefaultLang(lang[0]) @@ -590,3 +590,5 @@ def _run(argv=None, exec=True): if PROFILE_CODE: write_profile_results() + + return None diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 66165ee62..b014ab7a5 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -13,20 +13,20 @@ from aqt.utils import TR, disable_help_button, supportText, tooltip, tr class ClosableQDialog(QDialog): - def reject(self): + def reject(self) -> None: aqt.dialogs.markClosed("About") QDialog.reject(self) - def accept(self): + def accept(self) -> None: aqt.dialogs.markClosed("About") QDialog.accept(self) - def closeWithCallback(self, callback): + def closeWithCallback(self, callback) -> None: self.reject() callback() -def show(mw): +def show(mw) -> QDialog: dialog = ClosableQDialog(mw) disable_help_button(dialog) mw.setupDialogGC(dialog) @@ -55,7 +55,7 @@ def show(mw): modified = "mod" return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]" - def onCopy(): + def onCopy() -> None: addmgr = mw.addonManager active = [] activeids = [] diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 296dda0a0..bb1a80820 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -65,7 +65,7 @@ class AddCards(QDialog): ) self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea) - def helpRequested(self): + def helpRequested(self) -> None: openHelp(HelpPage.ADDING_CARD_AND_NOTE) def setupButtons(self) -> None: @@ -161,7 +161,7 @@ class AddCards(QDialog): gui_hooks.add_cards_will_show_history_menu(self, m) m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) - def editHistory(self, nid): + def editHistory(self, nid) -> None: self.mw.browser_search(SearchTerm(nid=nid)) def addNote(self, note) -> Optional[Note]: @@ -225,7 +225,7 @@ class AddCards(QDialog): QDialog.reject(self) def ifCanClose(self, onOk: Callable) -> None: - def afterSave(): + def afterSave() -> None: ok = self.editor.fieldsAreBlank(self.previousNote) or askUser( tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True ) @@ -234,8 +234,8 @@ class AddCards(QDialog): self.editor.saveNow(afterSave) - def closeWithCallback(self, cb): - def doClose(): + def closeWithCallback(self, cb) -> None: + def doClose() -> None: self._reject() cb() diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 5b32d21e2..e91317edb 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -605,7 +605,7 @@ class AddonManager: def _addon_schema_path(self, dir: str) -> str: return os.path.join(self.addonsFolder(dir), "config.schema.json") - def _addon_schema(self, dir: str): + def _addon_schema(self, dir: str) -> Any: path = self._addon_schema_path(dir) try: if not os.path.exists(path): diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 1525f9793..2386bb5b3 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -4,7 +4,7 @@ import copy import json import re -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Match, Optional import aqt from anki.cards import Card @@ -140,7 +140,7 @@ class CardLayout(QDialog): combo.setEnabled(not self._isCloze()) self.ignore_change_signals = False - def _summarizedName(self, idx: int, tmpl: Dict): + def _summarizedName(self, idx: int, tmpl: Dict) -> str: return "{}: {}: {} -> {}".format( idx + 1, tmpl["name"], @@ -284,7 +284,7 @@ class CardLayout(QDialog): self.fill_fields_from_template() - def on_search_changed(self, text: str): + def on_search_changed(self, text: str) -> None: editor = self.tform.edit_area if not editor.find(text): # try again from top @@ -294,7 +294,7 @@ class CardLayout(QDialog): if not editor.find(text): tooltip("No matches found.") - def on_search_next(self): + def on_search_next(self) -> None: text = self.tform.search_edit.text() self.on_search_changed(text) @@ -341,7 +341,7 @@ class CardLayout(QDialog): self.fill_empty_action_toggled = not self.fill_empty_action_toggled self.on_preview_toggled() - def on_night_mode_action_toggled(self): + def on_night_mode_action_toggled(self) -> None: self.night_mode_is_enabled = not self.night_mode_is_enabled self.on_preview_toggled() @@ -520,7 +520,7 @@ class CardLayout(QDialog): txt = txt.replace("
", "") hadHR = origLen != len(txt) - def answerRepl(match): + def answerRepl(match: Match) -> str: res = self.mw.reviewer.correct("exomple", "an example") if hadHR: res = "
" + res @@ -556,14 +556,15 @@ class CardLayout(QDialog): # Card operations ###################################################################### - def onRemove(self): + def onRemove(self) -> None: if len(self.templates) < 2: - return showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS)) + showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS)) + return - def get_count(): + def get_count() -> int: return self.mm.template_use_count(self.model["id"], self.ord) - def on_done(fut): + def on_done(fut) -> None: card_cnt = fut.result() template = self.current_template() @@ -593,7 +594,7 @@ class CardLayout(QDialog): self.redraw_everything() - def onRename(self): + def onRename(self) -> None: template = self.current_template() name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace( '"', "" @@ -604,7 +605,7 @@ class CardLayout(QDialog): template["name"] = name self.redraw_everything() - def onReorder(self): + def onReorder(self) -> None: n = len(self.templates) template = self.current_template() current_pos = self.templates.index(template) + 1 @@ -629,7 +630,7 @@ class CardLayout(QDialog): self.ord = new_idx self.redraw_everything() - def _newCardName(self): + def _newCardName(self) -> str: n = len(self.templates) + 1 while 1: name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n)) @@ -638,7 +639,7 @@ class CardLayout(QDialog): n += 1 return name - def onAddCard(self): + def onAddCard(self) -> None: cnt = self.mw.col.models.useCount(self.model) txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt) if not askUser(txt): @@ -654,12 +655,12 @@ class CardLayout(QDialog): self.ord = len(self.templates) - 1 self.redraw_everything() - def onFlip(self): + def onFlip(self) -> None: old = self.current_template() self._flipQA(old, old) self.redraw_everything() - def _flipQA(self, src, dst): + def _flipQA(self, src, dst) -> None: m = re.match("(?s)(.+)
(.+)", src["afmt"]) if not m: showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN)) @@ -667,7 +668,6 @@ class CardLayout(QDialog): self.change_tracker.mark_basic() dst["afmt"] = "{{FrontSide}}\n\n
\n\n%s" % src["qfmt"] dst["qfmt"] = m.group(2).strip() - return True def onMore(self) -> None: m = QMenu(self) @@ -728,7 +728,7 @@ class CardLayout(QDialog): if key in t: del t[key] - def onTargetDeck(self): + def onTargetDeck(self) -> None: from aqt.tagedit import TagEdit t = self.current_template() @@ -760,7 +760,7 @@ class CardLayout(QDialog): else: t["did"] = self.col.decks.id(te.text()) - def onAddField(self): + def onAddField(self) -> None: diag = QDialog(self) form = aqt.forms.addfield.Ui_Dialog() form.setupUi(diag) @@ -780,7 +780,7 @@ class CardLayout(QDialog): form.size.value(), ) - def _addField(self, field, font, size): + def _addField(self, field, font, size) -> None: text = self.tform.edit_area.toPlainText() text += "\n
{{%s}}
\n" % ( font, @@ -795,10 +795,10 @@ class CardLayout(QDialog): ###################################################################### def accept(self) -> None: - def save(): + def save() -> None: self.mm.save(self.model) - def on_done(fut): + def on_done(fut) -> None: try: fut.result() except TemplateError as e: @@ -829,5 +829,5 @@ class CardLayout(QDialog): self.rendered_card = None self.mw = None - def onHelp(self): + def onHelp(self) -> None: openHelp(HelpPage.TEMPLATES) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index ff0ed557c..70fe0483b 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -57,7 +57,7 @@ class CustomStudy(QDialog): typeShow = False ok = tr(TR.CUSTOM_STUDY_OK) - def plus(num): + def plus(num) -> str: if num == 1000: num = "1000+" return "" + str(num) + "" diff --git a/qt/aqt/dbcheck.py b/qt/aqt/dbcheck.py index ad46bf231..3024973b6 100644 --- a/qt/aqt/dbcheck.py +++ b/qt/aqt/dbcheck.py @@ -9,7 +9,7 @@ from aqt.qt import * from aqt.utils import showText, tooltip -def on_progress(mw: aqt.main.AnkiQt): +def on_progress(mw: aqt.main.AnkiQt) -> None: progress = mw.col.latest_progress() if progress.kind != ProgressKind.DatabaseCheck: return @@ -24,14 +24,14 @@ def on_progress(mw: aqt.main.AnkiQt): def check_db(mw: aqt.AnkiQt) -> None: - def on_timer(): + def on_timer() -> None: on_progress(mw) timer = QTimer(mw) qconnect(timer.timeout, on_timer) timer.start(100) - def on_future_done(fut): + def on_future_done(fut) -> None: timer.stop() ret, ok = fut.result() diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index ef8e0a621..1d0b28cdb 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -19,7 +19,7 @@ from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning, class DeckBrowserBottomBar: - def __init__(self, deck_browser: DeckBrowser): + def __init__(self, deck_browser: DeckBrowser) -> None: self.deck_browser = deck_browser @@ -51,14 +51,14 @@ class DeckBrowser: self.bottom = BottomBar(mw, mw.bottomWeb) self.scrollPos = QPoint(0, 0) - def show(self): + def show(self) -> None: av_player.stop_and_clear_queue() self.web.set_bridge_command(self._linkHandler, self) self._renderPage() # redraw top bar for theme change self.mw.toolbar.redraw() - def refresh(self): + def refresh(self) -> None: self._renderPage() # Event handlers @@ -90,7 +90,7 @@ class DeckBrowser: self._collapse(int(arg)) return False - def _selDeck(self, did): + def _selDeck(self, did) -> None: self.mw.col.decks.select(did) self.mw.onOverview() @@ -108,14 +108,14 @@ class DeckBrowser: """ - def _renderPage(self, reuse=False): + def _renderPage(self, reuse=False) -> None: if not reuse: self._dueTree = self.mw.col.sched.deck_due_tree() self.__renderPage(None) return self.web.evalWithCallback("window.pageYOffset", self.__renderPage) - def __renderPage(self, offset): + def __renderPage(self, offset) -> None: content = DeckBrowserContent( tree=self._renderDeckTree(self._dueTree), stats=self._renderStats(), @@ -137,10 +137,10 @@ class DeckBrowser: self._scrollToOffset(offset) gui_hooks.deck_browser_did_render(self) - def _scrollToOffset(self, offset): + def _scrollToOffset(self, offset) -> None: self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset) - def _renderStats(self): + def _renderStats(self) -> str: return '
{}
'.format( self.mw.col.studied_today(), ) @@ -170,7 +170,7 @@ class DeckBrowser: due = node.review_count + node.learn_count - def indent(): + def indent() -> str: return " " * 6 * (node.level - 1) if node.deck_id == ctx.current_deck_id: @@ -202,7 +202,7 @@ class DeckBrowser: node.name, ) # due counts - def nonzeroColour(cnt, klass): + def nonzeroColour(cnt: int, klass: str) -> str: if not cnt: klass = "zero-count" return f'{cnt}' @@ -222,7 +222,7 @@ class DeckBrowser: buf += self._render_deck_node(child, ctx) return buf - def _topLevelDragRow(self): + def _topLevelDragRow(self) -> str: return "
" # Options @@ -260,7 +260,7 @@ class DeckBrowser: return self.show() - def _options(self, did): + def _options(self, did) -> None: # select the deck first, because the dyn deck conf assumes the deck # we're editing is the current one self.mw.col.decks.select(did) @@ -297,10 +297,10 @@ class DeckBrowser: def _delete(self, did: int) -> None: if self.ask_delete_deck(did): - def do_delete(): + def do_delete() -> None: return self.mw.col.decks.rem(did, True) - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.show() res = fut.result() # Required to check for errors @@ -316,7 +316,7 @@ class DeckBrowser: ["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)], ] - def _drawButtons(self): + def _drawButtons(self) -> None: buf = "" drawLinks = deepcopy(self.drawLinks) for b in drawLinks: @@ -332,5 +332,5 @@ class DeckBrowser: web_context=DeckBrowserBottomBar(self), ) - def _onShared(self): + def _onShared(self) -> None: openLink(aqt.appShared + "decks/") diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index f5446fa85..66b792f94 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -2,12 +2,13 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from operator import itemgetter -from typing import Any, Dict +from typing import Any, Dict, Optional from PyQt5.QtWidgets import QLineEdit import aqt from anki.consts import NEW_CARDS_RANDOM +from anki.decks import DeckConfig from anki.lang import without_unicode_isolation from aqt import gui_hooks from aqt.qt import * @@ -28,7 +29,7 @@ from aqt.utils import ( class DeckConf(QDialog): - def __init__(self, mw: aqt.AnkiQt, deck: Dict): + def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None: QDialog.__init__(self, mw) self.mw = mw self.deck = deck @@ -60,7 +61,7 @@ class DeckConf(QDialog): self.exec_() saveGeom(self, "deckconf") - def setupCombos(self): + def setupCombos(self) -> None: import anki.consts as cs f = self.form @@ -70,12 +71,12 @@ class DeckConf(QDialog): # Conf list ###################################################################### - def setupConfs(self): + def setupConfs(self) -> None: qconnect(self.form.dconf.currentIndexChanged, self.onConfChange) - self.conf = None + self.conf: Optional[DeckConfig] = None self.loadConfs() - def loadConfs(self): + def loadConfs(self) -> None: current = self.deck["conf"] self.confList = self.mw.col.decks.allConf() self.confList.sort(key=itemgetter("name")) @@ -92,7 +93,7 @@ class DeckConf(QDialog): self._origNewOrder = self.confList[startOn]["new"]["order"] self.onConfChange(startOn) - def confOpts(self): + def confOpts(self) -> None: m = QMenu(self.mw) a = m.addAction(tr(TR.ACTIONS_ADD)) qconnect(a.triggered, self.addGroup) @@ -106,7 +107,7 @@ class DeckConf(QDialog): a.setEnabled(False) m.exec_(QCursor.pos()) - def onConfChange(self, idx): + def onConfChange(self, idx) -> None: if self.ignoreConfChange: return if self.conf: @@ -159,7 +160,7 @@ class DeckConf(QDialog): self.saveConf() self.loadConfs() - def setChildren(self): + def setChildren(self) -> None: if not askUser(tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val=self.deck["name"])): return for did in self.childDids: @@ -173,8 +174,8 @@ class DeckConf(QDialog): # Loading ################################################## - def listToUser(self, l): - def num_to_user(n: Union[int, float]): + def listToUser(self, l) -> str: + def num_to_user(n: Union[int, float]) -> str: if n == round(n): return str(int(n)) else: @@ -182,7 +183,7 @@ class DeckConf(QDialog): return " ".join(map(num_to_user, l)) - def parentLimText(self, type="new"): + def parentLimText(self, type="new") -> str: # top level? if "::" not in self.deck["name"]: return "" @@ -196,7 +197,7 @@ class DeckConf(QDialog): lim = min(x, lim) return tr(TR.SCHEDULING_PARENT_LIMIT, val=lim) - def loadConf(self): + def loadConf(self) -> None: self.conf = self.mw.col.decks.confForDid(self.deck["id"]) # new c = self.conf["new"] @@ -238,7 +239,7 @@ class DeckConf(QDialog): f.desc.setPlainText(self.deck["desc"]) gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf) - def onRestore(self): + def onRestore(self) -> None: self.mw.progress.start() self.mw.col.decks.restoreToDefault(self.conf) self.mw.progress.finish() @@ -247,7 +248,7 @@ class DeckConf(QDialog): # New order ################################################## - def onNewOrderChanged(self, new): + def onNewOrderChanged(self, new) -> None: old = self.conf["new"]["order"] if old == new: return @@ -280,7 +281,7 @@ class DeckConf(QDialog): return conf[key] = ret - def saveConf(self): + def saveConf(self) -> None: # new c = self.conf["new"] f = self.form @@ -324,10 +325,10 @@ class DeckConf(QDialog): self.mw.col.decks.save(self.deck) self.mw.col.decks.save(self.conf) - def reject(self): + def reject(self) -> None: self.accept() - def accept(self): + def accept(self) -> None: self.saveConf() self.mw.reset() QDialog.accept(self) diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 0082537d5..b63c638c7 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -145,7 +145,7 @@ class DeckConf(QDialog): self.mw.col.decks.save(d) - def reject(self): + def reject(self) -> None: self.ok = False QDialog.reject(self) @@ -164,7 +164,7 @@ class DeckConf(QDialog): # Step load/save - fixme: share with std options screen ######################################################## - def listToUser(self, l): + def listToUser(self, l) -> str: return " ".join([str(x) for x in l]) def userToList(self, w, minSize=1) -> Optional[List[Union[float, int]]]: diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index f066c76ae..25616d466 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -48,7 +48,7 @@ class EditCurrent(QDialog): return self.editor.setNote(n) - def reopen(self, mw): + def reopen(self, mw) -> None: tooltip("Please finish editing the existing card first.") self.onReset() @@ -74,8 +74,8 @@ class EditCurrent(QDialog): aqt.dialogs.markClosed("EditCurrent") QDialog.reject(self) - def closeWithCallback(self, onsuccess): - def callback(): + def closeWithCallback(self, onsuccess) -> None: + def callback() -> None: self._saveAndClose() onsuccess() diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index aa1016776..0c64e2e0b 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -15,7 +15,7 @@ from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, t def show_empty_cards(mw: aqt.main.AnkiQt) -> None: mw.progress.start() - def on_done(fut): + def on_done(fut) -> None: mw.progress.finish() report: EmptyCardsReport = fut.result() if not report.notes: @@ -54,7 +54,7 @@ class EmptyCardsDialog(QDialog): style = "" self.form.webview.stdHtml(style + html, context=self) - def on_finished(code): + def on_finished(code) -> None: saveGeom(self, "emptycards") qconnect(self.finished, on_finished) @@ -65,16 +65,16 @@ class EmptyCardsDialog(QDialog): self._delete_button.setAutoDefault(False) self._delete_button.clicked.connect(self._on_delete) - def _on_note_link_clicked(self, link): + def _on_note_link_clicked(self, link) -> None: self.mw.browser_search(link) def _on_delete(self) -> None: self.mw.progress.start() - def delete(): + def delete() -> int: return self._delete_cards(self.form.keep_notes.isChecked()) - def on_done(fut): + def on_done(fut) -> None: self.mw.progress.finish() try: count = fut.result() diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index 485ddc898..d085b4b6d 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -16,7 +16,7 @@ from aqt.utils import TR, showText, showWarning, supportText, tr if not os.environ.get("DEBUG"): - def excepthook(etype, val, tb): + def excepthook(etype, val, tb) -> None: sys.stderr.write( "Caught exception:\n%s\n" % ("".join(traceback.format_exception(etype, val, tb))) @@ -65,7 +65,7 @@ class ErrorHandler(QObject): self.timer.setSingleShot(True) self.timer.start() - def tempFolderMsg(self): + def tempFolderMsg(self) -> str: return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER) def onTimeout(self) -> None: @@ -105,7 +105,7 @@ class ErrorHandler(QObject): txt = txt + "
" + error + "
" showText(txt, type="html", copyBtn=True) - def _addonText(self, error): + def _addonText(self, error: str) -> str: matches = re.findall(r"addons21/(.*?)/", error) if not matches: return "" diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index dd3157365..c22c3019a 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -42,7 +42,7 @@ class ExportDialog(QDialog): self.setup(did) self.exec_() - def setup(self, did: Optional[int]): + def setup(self, did: Optional[int]) -> None: self.exporters = exporters(self.col) # if a deck specified, start with .apkg type selected idx = 0 @@ -155,17 +155,17 @@ class ExportDialog(QDialog): os.unlink(file) # progress handler - def exported_media(cnt): + def exported_media(cnt) -> None: self.mw.taskman.run_on_main( lambda: self.mw.progress.update( label=tr(TR.EXPORTING_EXPORTED_MEDIA_FILE, count=cnt) ) ) - def do_export(): + def do_export() -> None: self.exporter.exportInto(file) - def on_done(future: Future): + def on_done(future: Future) -> None: self.mw.progress.finish() hooks.media_files_did_export.remove(exported_media) # raises if exporter failed diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 33b448dba..1f7c2a804 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -23,7 +23,7 @@ from aqt.utils import ( class FieldDialog(QDialog): - def __init__(self, mw: AnkiQt, nt: NoteType, parent=None): + def __init__(self, mw: AnkiQt, nt: NoteType, parent=None) -> None: QDialog.__init__(self, parent or mw) self.mw = mw self.col = self.mw.col @@ -68,7 +68,7 @@ class FieldDialog(QDialog): qconnect(f.sortField.clicked, self.onSortField) qconnect(f.buttonBox.helpRequested, self.onHelp) - def onDrop(self, ev): + def onDrop(self, ev) -> None: fieldList = self.form.fieldList indicatorPos = fieldList.dropIndicatorPosition() dropPos = fieldList.indexAt(ev.pos()).row() @@ -113,7 +113,7 @@ class FieldDialog(QDialog): return None return txt - def onRename(self): + def onRename(self) -> None: idx = self.currentIdx f = self.model["flds"][idx] name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"]) @@ -141,9 +141,10 @@ class FieldDialog(QDialog): self.fillFields() self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1) - def onDelete(self): + def onDelete(self) -> None: if len(self.model["flds"]) < 2: - return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD)) + showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD)) + return count = self.mm.useCount(self.model) c = tr(TR.BROWSING_NOTE_COUNT, count=count) if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)): @@ -157,7 +158,7 @@ class FieldDialog(QDialog): self.fillFields() self.form.fieldList.setCurrentRow(0) - def onPosition(self, delta=-1): + def onPosition(self, delta=-1) -> None: idx = self.currentIdx l = len(self.model["flds"]) txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1)) @@ -171,16 +172,16 @@ class FieldDialog(QDialog): return self.moveField(pos) - def onSortField(self): + def onSortField(self) -> None: if not self.change_tracker.mark_schema(): - return False + return # don't allow user to disable; it makes no sense self.form.sortField.setChecked(True) self.mm.set_sort_index(self.model, self.form.fieldList.currentRow()) - def moveField(self, pos): + def moveField(self, pos) -> None: if not self.change_tracker.mark_schema(): - return False + return self.saveField() f = self.model["flds"][self.currentIdx] self.mm.reposition_field(self.model, f, pos - 1) @@ -231,10 +232,10 @@ class FieldDialog(QDialog): def accept(self) -> None: self.saveField() - def save(): + def save() -> None: self.mm.save(self.model) - def on_done(fut): + def on_done(fut) -> None: try: fut.result() except TemplateError as e: @@ -247,5 +248,5 @@ class FieldDialog(QDialog): self.mw.taskman.with_progress(save, on_done, self) - def onHelp(self): + def onHelp(self) -> None: openHelp(HelpPage.CUSTOMIZING_FIELDS) diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index e0b77df11..956c8374e 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -35,7 +35,7 @@ from aqt.utils import ( class ChangeMap(QDialog): - def __init__(self, mw: AnkiQt, model, current): + def __init__(self, mw: AnkiQt, model, current) -> None: QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.model = model @@ -60,11 +60,11 @@ class ChangeMap(QDialog): self.frm.fields.setCurrentRow(n + 1) self.field: Optional[str] = None - def getField(self): + def getField(self) -> str: self.exec_() return self.field - def accept(self): + def accept(self) -> None: row = self.frm.fields.currentRow() if row < len(self.model["flds"]): self.field = self.model["flds"][row]["name"] @@ -74,7 +74,7 @@ class ChangeMap(QDialog): self.field = None QDialog.accept(self) - def reject(self): + def reject(self) -> None: self.accept() @@ -119,7 +119,7 @@ class ImportDialog(QDialog): self.importer.initMapping() self.showMapping() - def onDelimiter(self): + def onDelimiter(self) -> None: str = ( getOnlyText( tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE), @@ -136,7 +136,7 @@ class ImportDialog(QDialog): return self.hideMapping() - def updateDelim(): + def updateDelim() -> None: self.importer.delimiter = str self.importer.updateDelimiter() @@ -183,7 +183,7 @@ class ImportDialog(QDialog): self.mw.progress.start() self.mw.checkpoint(tr(TR.ACTIONS_IMPORT)) - def on_done(future: Future): + def on_done(future: Future) -> None: self.mw.progress.finish() try: @@ -221,7 +221,7 @@ class ImportDialog(QDialog): self.mapbox.setContentsMargins(0, 0, 0, 0) self.mapwidget: Optional[QWidget] = None - def hideMapping(self): + def hideMapping(self) -> None: self.frm.mappingGroup.hide() def showMapping( @@ -258,7 +258,7 @@ class ImportDialog(QDialog): self.grid.addWidget(button, num, 2) qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n)) - def changeMappingNum(self, n): + def changeMappingNum(self, n) -> None: f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField() try: # make sure we don't have it twice @@ -270,7 +270,7 @@ class ImportDialog(QDialog): if getattr(self.importer, "delimiter", False): self.savedDelimiter = self.importer.delimiter - def updateDelim(): + def updateDelim() -> None: self.importer.delimiter = self.savedDelimiter self.showMapping(hook=updateDelim, keepMapping=True) @@ -283,17 +283,17 @@ class ImportDialog(QDialog): gui_hooks.current_note_type_did_change.remove(self.modelChanged) QDialog.reject(self) - def helpRequested(self): + def helpRequested(self) -> None: openHelp(HelpPage.IMPORTING) - def importModeChanged(self, newImportMode): + def importModeChanged(self, newImportMode) -> None: if newImportMode == 0: self.frm.tagModified.setEnabled(True) else: self.frm.tagModified.setEnabled(False) -def showUnicodeWarning(): +def showUnicodeWarning() -> None: """Shorthand to show a standard warning.""" showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8)) @@ -374,7 +374,7 @@ def importFile(mw: AnkiQt, file: str) -> None: # importing non-colpkg files mw.progress.start(immediate=True) - def on_done(future: Future): + def on_done(future: Future) -> None: mw.progress.finish() try: future.result() @@ -405,7 +405,7 @@ def importFile(mw: AnkiQt, file: str) -> None: mw.taskman.run_in_background(importer.run, on_done) -def invalidZipMsg(): +def invalidZipMsg() -> str: return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO) @@ -430,14 +430,14 @@ def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool: return False -def replaceWithApkg(mw, file, backup): +def replaceWithApkg(mw, file, backup) -> None: mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup)) -def _replaceWithApkg(mw, filename, backup): +def _replaceWithApkg(mw, filename, backup) -> None: mw.progress.start(immediate=True) - def do_import(): + def do_import() -> None: z = zipfile.ZipFile(filename) # v2 scheduler? @@ -472,7 +472,7 @@ def _replaceWithApkg(mw, filename, backup): z.close() - def on_done(future: Future): + def on_done(future: Future) -> None: mw.progress.finish() try: diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index 50729eb12..47a2242f3 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -6,7 +6,7 @@ Legacy support """ -from typing import List +from typing import Any, List import anki import aqt @@ -31,7 +31,14 @@ def stripSounds(text) -> str: return aqt.mw.col.media.strip_av_tags(text) -def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): +def fmtTimeSpan( + time: Any, + pad: Any = 0, + point: Any = 0, + short: Any = False, + inTime: Any = False, + unit: Any = 99, +) -> Any: print("fmtTimeSpan() has become col.format_timespan()") return aqt.mw.col.format_timespan(time) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index c549e5052..17e9619dd 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -87,7 +87,7 @@ class ResetReason(enum.Enum): class ResetRequired: - def __init__(self, mw: AnkiQt): + def __init__(self, mw: AnkiQt) -> None: self.mw = mw @@ -139,7 +139,7 @@ class AnkiQt(QMainWindow): else: fn = self.setupProfile - def on_window_init(): + def on_window_init() -> None: fn() gui_hooks.main_window_did_init() @@ -175,7 +175,7 @@ class AnkiQt(QMainWindow): "Actions that are deferred until after add-on loading." self.toolbar.draw() - def setupProfileAfterWebviewsLoaded(self): + def setupProfileAfterWebviewsLoaded(self) -> None: for w in (self.web, self.bottomWeb): if not w._domDone: self.progress.timer( @@ -206,7 +206,7 @@ class AnkiQt(QMainWindow): self.onClose.emit() # type: ignore evt.accept() - def closeWithoutQuitting(self): + def closeWithoutQuitting(self) -> None: self.closeFires = False self.close() self.closeFires = True @@ -275,9 +275,10 @@ class AnkiQt(QMainWindow): name = self.pm.profiles()[n] self.pm.load(name) - def openProfile(self): + def openProfile(self) -> None: name = self.pm.profiles()[self.profileForm.profiles.currentRow()] - return self.pm.load(name) + self.pm.load(name) + return def onOpenProfile(self) -> None: self.profileDiag.hide() @@ -288,34 +289,37 @@ class AnkiQt(QMainWindow): def profileNameOk(self, name: str) -> bool: return not checkInvalidFilename(name) and name != "addons21" - def onAddProfile(self): + def onAddProfile(self) -> None: name = getOnlyText(tr(TR.ACTIONS_NAME)).strip() if name: if name in self.pm.profiles(): - return showWarning(tr(TR.QT_MISC_NAME_EXISTS)) + showWarning(tr(TR.QT_MISC_NAME_EXISTS)) + return if not self.profileNameOk(name): return self.pm.create(name) self.pm.name = name self.refreshProfilesList() - def onRenameProfile(self): + def onRenameProfile(self) -> None: name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip() if not name: return if name == self.pm.name: return if name in self.pm.profiles(): - return showWarning(tr(TR.QT_MISC_NAME_EXISTS)) + showWarning(tr(TR.QT_MISC_NAME_EXISTS)) + return if not self.profileNameOk(name): return self.pm.rename(name) self.refreshProfilesList() - def onRemProfile(self): + def onRemProfile(self) -> None: profs = self.pm.profiles() if len(profs) < 2: - return showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE)) + showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE)) + return # sure? if not askUser( tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR), @@ -326,7 +330,7 @@ class AnkiQt(QMainWindow): self.pm.remove(self.pm.name) self.refreshProfilesList() - def onOpenBackup(self): + def onOpenBackup(self) -> None: if not askUser( tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER), msgfunc=QMessageBox.warning, @@ -334,7 +338,7 @@ class AnkiQt(QMainWindow): ): return - def doOpen(path): + def doOpen(path) -> None: self._openBackup(path) getFile( @@ -345,7 +349,7 @@ class AnkiQt(QMainWindow): dir=self.pm.backupFolder(), ) - def _openBackup(self, path): + def _openBackup(self, path) -> None: try: # move the existing collection to the trash, as it may not open self.pm.trashCollection() @@ -360,14 +364,14 @@ class AnkiQt(QMainWindow): self.onOpenProfile() - def _on_downgrade(self): + def _on_downgrade(self) -> None: self.progress.start() profiles = self.pm.profiles() - def downgrade(): + def downgrade() -> List[str]: return self.pm.downgrade(profiles) - def on_done(future): + def on_done(future) -> None: self.progress.finish() problems = future.result() if not problems: @@ -409,7 +413,7 @@ class AnkiQt(QMainWindow): self.pendingImport = None gui_hooks.profile_did_open() - def _onsuccess(): + def _onsuccess() -> None: self._refresh_after_sync() if onsuccess: onsuccess() @@ -417,7 +421,7 @@ class AnkiQt(QMainWindow): self.maybe_auto_sync_on_open_close(_onsuccess) def unloadProfile(self, onsuccess: Callable) -> None: - def callback(): + def callback() -> None: self._unloadProfile() onsuccess() @@ -447,7 +451,7 @@ class AnkiQt(QMainWindow): def unloadProfileAndExit(self) -> None: self.unloadProfile(self.cleanupAndExit) - def unloadProfileAndShowProfileManager(self): + def unloadProfileAndShowProfileManager(self) -> None: self.unloadProfile(self.showProfileManager) def cleanupAndExit(self) -> None: @@ -520,14 +524,14 @@ class AnkiQt(QMainWindow): self.col.reopen() def unloadCollection(self, onsuccess: Callable) -> None: - def after_media_sync(): + def after_media_sync() -> None: self._unloadCollection() onsuccess() - def after_sync(): + def after_sync() -> None: self.media_syncer.show_diag_until_finished(after_media_sync) - def before_sync(): + def before_sync() -> None: self.setEnabled(False) self.maybe_auto_sync_on_open_close(after_sync) @@ -565,7 +569,7 @@ class AnkiQt(QMainWindow): ########################################################################## class BackupThread(Thread): - def __init__(self, path, data): + def __init__(self, path, data) -> None: Thread.__init__(self) self.path = path self.data = data @@ -574,7 +578,7 @@ class AnkiQt(QMainWindow): with open(self.path, "wb") as file: pass - def run(self): + def run(self) -> None: z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED) z.writestr("collection.anki2", self.data) z.writestr("media", "{}") @@ -700,7 +704,7 @@ class AnkiQt(QMainWindow): self.state = self.returnState self.reset() - def delayedMaybeReset(self): + def delayedMaybeReset(self) -> None: # if we redraw the page in a button click event it will often crash on # windows self.progress.timer(100, self.maybeReset, False) @@ -801,9 +805,9 @@ title="%s" %s>%s""" % ( signal.signal(signal.SIGINT, self.onUnixSignal) signal.signal(signal.SIGTERM, self.onUnixSignal) - def onUnixSignal(self, signum, frame): + def onUnixSignal(self, signum, frame) -> None: # schedule a rollback & quit - def quit(): + def quit() -> None: self.col.db.rollback() self.close() @@ -888,13 +892,13 @@ title="%s" %s>%s""" % ( def _refresh_after_sync(self) -> None: self.toolbar.redraw() - def _sync_collection_and_media(self, after_sync: Callable[[], None]): + def _sync_collection_and_media(self, after_sync: Callable[[], None]) -> None: "Caller should ensure auth available." # start media sync if not already running if not self.media_syncer.is_syncing(): self.media_syncer.start() - def on_collection_sync_finished(): + def on_collection_sync_finished() -> None: self.col.clearUndo() self.col.models._clear_cache() gui_hooks.sync_did_finish() @@ -927,7 +931,7 @@ title="%s" %s>%s""" % ( ) # legacy - def _sync(self): + def _sync(self) -> None: pass onSync = on_sync_button_clicked @@ -1057,7 +1061,7 @@ title="%s" %s>%s""" % ( def onEditCurrent(self) -> None: aqt.dialogs.open("EditCurrent", self) - def onDeckConf(self, deck=None): + def onDeckConf(self, deck=None) -> None: if not deck: deck = self.col.decks.current() if deck["dyn"]: @@ -1069,7 +1073,7 @@ title="%s" %s>%s""" % ( aqt.deckconf.DeckConf(self, deck) - def onOverview(self): + def onOverview(self) -> None: self.col.reset() self.moveToState("overview") @@ -1083,7 +1087,7 @@ title="%s" %s>%s""" % ( else: aqt.dialogs.open("NewDeckStats", self) - def onPrefs(self): + def onPrefs(self) -> None: aqt.dialogs.open("Preferences", self) def onNoteTypes(self) -> None: @@ -1091,13 +1095,13 @@ title="%s" %s>%s""" % ( aqt.models.Models(self, self, fromMain=True) - def onAbout(self): + def onAbout(self) -> None: aqt.dialogs.open("About", self) - def onDonate(self): + def onDonate(self) -> None: openLink(aqt.appDonate) - def onDocumentation(self): + def onDocumentation(self) -> None: openHelp(HelpPage.INDEX) # Importing & exporting @@ -1126,7 +1130,7 @@ title="%s" %s>%s""" % ( # Installing add-ons from CLI / mimetype handler ########################################################################## - def installAddon(self, path: str, startup: bool = False): + def installAddon(self, path: str, startup: bool = False) -> None: from aqt.addons import installAddonPackages installAddonPackages( @@ -1201,14 +1205,14 @@ title="%s" %s>%s""" % ( qconnect(self.autoUpdate.clockIsOff, self.clockIsOff) self.autoUpdate.start() - def newVerAvail(self, ver): + def newVerAvail(self, ver) -> None: if self.pm.meta.get("suppressUpdate", None) != ver: aqt.update.askAndUpdate(self, ver) - def newMsg(self, data): + def newMsg(self, data) -> None: aqt.update.showMessages(self, data) - def clockIsOff(self, diff): + def clockIsOff(self, diff) -> None: if devMode: print("clock is off; ignoring") return @@ -1229,7 +1233,7 @@ title="%s" %s>%s""" % ( # SIGINT/SIGTERM is processed without a long delay self.progress.timer(1000, lambda: None, True, False) - def onRefreshTimer(self): + def onRefreshTimer(self) -> None: if self.state == "deckBrowser": self.deckBrowser.refresh() elif self.state == "overview": @@ -1256,7 +1260,7 @@ title="%s" %s>%s""" % ( self._activeWindowOnPlay: Optional[QWidget] = None - def onOdueInvalid(self): + def onOdueInvalid(self) -> None: showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE)) def _isVideo(self, tag: AVTag) -> bool: @@ -1343,11 +1347,11 @@ title="%s" %s>%s""" % ( # Debugging ###################################################################### - def onDebug(self): + def onDebug(self) -> None: frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog() class DebugDialog(QDialog): - def reject(self): + def reject(self) -> None: super().reject() saveSplitter(frm.splitter, "DebugConsoleWindow") saveGeom(self, "DebugConsoleWindow") @@ -1394,7 +1398,7 @@ title="%s" %s>%s""" % ( mw = self class Stream: - def write(self, data): + def write(self, data) -> None: mw._output += data if on: @@ -1445,7 +1449,7 @@ title="%s" %s>%s""" % ( self._card_repr(card) return card - def onDebugPrint(self, frm): + def onDebugPrint(self, frm) -> None: cursor = frm.text.textCursor() position = cursor.position() cursor.select(QTextCursor.LineUnderCursor) @@ -1458,7 +1462,7 @@ title="%s" %s>%s""" % ( frm.text.setTextCursor(cursor) self.onDebugRet(frm) - def onDebugRet(self, frm): + def onDebugRet(self, frm) -> None: import pprint import traceback diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 0f844a643..42cd96a60 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -136,7 +136,7 @@ class MediaChecker: diag.exec_() saveGeom(diag, "checkmediadb") - def _on_render_latex(self): + def _on_render_latex(self) -> None: self.progress_dialog = self.mw.progress.start() try: out = self.mw.col.media.render_all_latex(self._on_render_latex_progress) @@ -160,7 +160,7 @@ class MediaChecker: self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count)) return True - def _on_trash_files(self, fnames: Sequence[str]): + def _on_trash_files(self, fnames: Sequence[str]) -> None: if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)): return @@ -183,14 +183,14 @@ class MediaChecker: tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total)) - def _on_empty_trash(self): + def _on_empty_trash(self) -> None: self.progress_dialog = self.mw.progress.start() self._set_progress_enabled(True) - def empty_trash(): + def empty_trash() -> None: self.mw.col.media.empty_trash() - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.mw.progress.finish() self._set_progress_enabled(False) # check for errors @@ -200,14 +200,14 @@ class MediaChecker: self.mw.taskman.run_in_background(empty_trash, on_done) - def _on_restore_trash(self): + def _on_restore_trash(self) -> None: self.progress_dialog = self.mw.progress.start() self._set_progress_enabled(True) - def restore_trash(): + def restore_trash() -> None: self.mw.col.media.restore_trash() - def on_done(fut: Future): + def on_done(fut: Future) -> None: self.mw.progress.finish() self._set_progress_enabled(False) # check for errors diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 1af54e39d..45d52e01e 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -12,6 +12,7 @@ import threading import time import traceback from http import HTTPStatus +from typing import Tuple import flask import flask_cors # type: ignore @@ -52,11 +53,11 @@ class MediaServer(threading.Thread): _ready = threading.Event() daemon = True - def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs): + def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.is_shutdown = False - def run(self): + def run(self) -> None: try: if devMode: # idempotent if logging has already been set up @@ -97,7 +98,7 @@ class MediaServer(threading.Thread): @app.route("/", methods=["GET", "POST"]) -def allroutes(pathin): +def allroutes(pathin) -> Response: try: directory, path = _redirectWebExports(pathin) except TypeError: @@ -171,7 +172,7 @@ def allroutes(pathin): ) -def _redirectWebExports(path): +def _redirectWebExports(path) -> Tuple[str, str]: # catch /_anki references and rewrite them to web export folder targetPath = "_anki/" if path.startswith(targetPath): diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index ba5cd95d4..ad9eb9e9c 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -28,14 +28,14 @@ class LogEntryWithTime: class MediaSyncer: - def __init__(self, mw: aqt.main.AnkiQt): + def __init__(self, mw: aqt.main.AnkiQt) -> None: self.mw = mw self._syncing: bool = False self._log: List[LogEntryWithTime] = [] self._progress_timer: Optional[QTimer] = None gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop) - def _on_progress(self): + def _on_progress(self) -> None: progress = self.mw.col.latest_progress() if progress.kind != ProgressKind.MediaSync: return @@ -88,7 +88,7 @@ class MediaSyncer: else: self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE)) - def _handle_sync_error(self, exc: BaseException): + def _handle_sync_error(self, exc: BaseException) -> None: if isinstance(exc, Interrupted): self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED)) return @@ -116,10 +116,10 @@ class MediaSyncer: def _on_start_stop(self, running: bool) -> None: self.mw.toolbar.set_sync_active(running) - def show_sync_log(self): + def show_sync_log(self) -> None: aqt.dialogs.open("sync_log", self.mw, self) - def show_diag_until_finished(self, on_finished: Callable[[], None]): + def show_diag_until_finished(self, on_finished: Callable[[], None]) -> None: # nothing to do if not syncing if not self.is_syncing(): return on_finished() @@ -129,7 +129,7 @@ class MediaSyncer: timer: Optional[QTimer] = None - def check_finished(): + def check_finished() -> None: if not self.is_syncing(): timer.stop() on_finished() @@ -197,7 +197,7 @@ class MediaSyncDialog(QDialog): asctime = time.asctime(time.localtime(stamp)) return f"{asctime}: {text}" - def _entry_to_text(self, entry: LogEntryWithTime): + def _entry_to_text(self, entry: LogEntryWithTime) -> str: if isinstance(entry.entry, str): txt = entry.entry elif isinstance(entry.entry, MediaSyncProgress): @@ -209,7 +209,7 @@ class MediaSyncDialog(QDialog): def _logentry_to_text(self, e: MediaSyncProgress) -> str: return f"{e.added}, {e.removed}, {e.checked}" - def _on_log_entry(self, entry: LogEntryWithTime): + def _on_log_entry(self, entry: LogEntryWithTime) -> None: self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry)) if not self._syncer.is_syncing(): self.abort_button.setHidden(True) diff --git a/qt/aqt/modelchooser.py b/qt/aqt/modelchooser.py index 754d7b31a..d94dd6f03 100644 --- a/qt/aqt/modelchooser.py +++ b/qt/aqt/modelchooser.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from typing import Optional +from typing import List, Optional from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -74,7 +74,7 @@ class ModelChooser(QHBoxLayout): # edit button edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore - def nameFunc(): + def nameFunc() -> List[str]: return sorted(self.deck.models.allNames()) ret = StudyDeck( diff --git a/qt/aqt/models.py b/qt/aqt/models.py index df8154ee7..ef17820b2 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -219,7 +219,7 @@ class Models(QDialog): class AddModel(QDialog): model: Optional[NoteType] - def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None): + def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None) -> None: self.parent_ = parent or mw self.mw = mw self.col = mw.col diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index e2756bbc1..1bd461de4 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -15,7 +15,7 @@ from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr class OverviewBottomBar: - def __init__(self, overview: Overview): + def __init__(self, overview: Overview) -> None: self.overview = overview @@ -104,12 +104,12 @@ class Overview: def _filteredDeck(self) -> int: return self.mw.col.decks.current()["dyn"] - def onRebuildKey(self): + def onRebuildKey(self) -> None: if self._filteredDeck(): self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected()) self.mw.reset() - def onEmptyKey(self): + def onEmptyKey(self) -> None: if self._filteredDeck(): self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected()) self.mw.reset() @@ -118,7 +118,7 @@ class Overview: if not self._filteredDeck(): self.onStudyMore() - def onUnbury(self): + def onUnbury(self) -> None: if self.mw.col.schedVer() == 1: self.mw.col.sched.unburyCardsForDeck() self.mw.reset() diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 1248696d8..c00783a92 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -34,7 +34,7 @@ def video_driver_name_for_platform(driver: VideoDriver) -> str: class Preferences(QDialog): - def __init__(self, mw: AnkiQt): + def __init__(self, mw: AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.prof = self.mw.pm.profile @@ -55,7 +55,7 @@ class Preferences(QDialog): self.setupOptions() self.show() - def accept(self): + def accept(self) -> None: # avoid exception if main window is already closed if not self.mw.col: return @@ -68,19 +68,19 @@ class Preferences(QDialog): self.done(0) aqt.dialogs.markClosed("Preferences") - def reject(self): + def reject(self) -> None: self.accept() # Language ###################################################################### - def setupLang(self): + def setupLang(self) -> None: f = self.form f.lang.addItems([x[0] for x in anki.lang.langs]) f.lang.setCurrentIndex(self.langIdx()) qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged) - def langIdx(self): + def langIdx(self) -> int: codes = [x[1] for x in anki.lang.langs] lang = anki.lang.currentLang if lang in anki.lang.compatMap: @@ -92,7 +92,7 @@ class Preferences(QDialog): except: return codes.index("en_US") - def onLangIdxChanged(self, idx): + def onLangIdxChanged(self, idx) -> None: code = anki.lang.langs[idx][1] self.mw.pm.setLang(code) showInfo( @@ -102,7 +102,7 @@ class Preferences(QDialog): # Collection options ###################################################################### - def setupCollection(self): + def setupCollection(self) -> None: import anki.consts as c f = self.form @@ -130,7 +130,7 @@ class Preferences(QDialog): f.newSched.setChecked(True) f.new_timezone.setChecked(s.new_timezone) - def setup_video_driver(self): + def setup_video_driver(self) -> None: self.video_drivers = VideoDriver.all_for_platform() names = [ tr(TR.PREFERENCES_VIDEO_DRIVER, driver=video_driver_name_for_platform(d)) @@ -141,13 +141,13 @@ class Preferences(QDialog): self.video_drivers.index(self.mw.pm.video_driver()) ) - def update_video_driver(self): + def update_video_driver(self) -> None: new_driver = self.video_drivers[self.form.video_driver.currentIndex()] if new_driver != self.mw.pm.video_driver(): self.mw.pm.set_video_driver(new_driver) showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU)) - def updateCollection(self): + def updateCollection(self) -> None: f = self.form d = self.mw.col @@ -176,7 +176,7 @@ class Preferences(QDialog): # Scheduler version ###################################################################### - def _updateSchedVer(self, wantNew): + def _updateSchedVer(self, wantNew) -> None: haveNew = self.mw.col.schedVer() == 2 # nothing to do? @@ -194,7 +194,7 @@ class Preferences(QDialog): # Network ###################################################################### - def setupNetwork(self): + def setupNetwork(self) -> None: self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON)) qconnect(self.form.media_log.clicked, self.on_media_log) self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"]) @@ -207,10 +207,10 @@ class Preferences(QDialog): qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth) self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON)) - def on_media_log(self): + def on_media_log(self) -> None: self.mw.media_syncer.show_sync_log() - def _hideAuth(self): + def _hideAuth(self) -> None: self.form.syncDeauth.setVisible(False) self.form.syncUser.setText("") self.form.syncLabel.setText( @@ -225,7 +225,7 @@ class Preferences(QDialog): self.mw.col.media.force_resync() self._hideAuth() - def updateNetwork(self): + def updateNetwork(self) -> None: self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked() self.prof["syncMedia"] = self.form.syncMedia.isChecked() self.mw.pm.set_auto_sync_media_minutes( @@ -238,16 +238,16 @@ class Preferences(QDialog): # Backup ###################################################################### - def setupBackup(self): + def setupBackup(self) -> None: self.form.numBackups.setValue(self.prof["numBackups"]) - def updateBackup(self): + def updateBackup(self) -> None: self.prof["numBackups"] = self.form.numBackups.value() # Basic & Advanced Options ###################################################################### - def setupOptions(self): + def setupOptions(self) -> None: self.form.pastePNG.setChecked(self.prof.get("pastePNG", False)) self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100)) self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False)) @@ -270,7 +270,7 @@ class Preferences(QDialog): self._recording_drivers.index(self.mw.pm.recording_driver()) ) - def updateOptions(self): + def updateOptions(self) -> None: restart_required = False self.prof["pastePNG"] = self.form.pastePNG.isChecked() diff --git a/qt/aqt/previewer.py b/qt/aqt/previewer.py index 7557a6412..e0262562e 100644 --- a/qt/aqt/previewer.py +++ b/qt/aqt/previewer.py @@ -40,7 +40,9 @@ class Previewer(QDialog): _timer: Optional[QTimer] = None _show_both_sides = False - def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]): + def __init__( + self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None] + ) -> None: super().__init__(None, Qt.Window) self._open = True self._parent = parent @@ -259,14 +261,14 @@ class MultiCardPreviewer(Previewer): qconnect(self._prev.clicked, self._on_prev) qconnect(self._next.clicked, self._on_next) - def _on_prev(self): + def _on_prev(self) -> None: if self._state == "answer" and not self._show_both_sides: self._state = "question" self.render_card() else: self._on_prev_card() - def _on_prev_card(self): + def _on_prev_card(self) -> None: pass def _on_next(self) -> None: @@ -276,7 +278,7 @@ class MultiCardPreviewer(Previewer): else: self._on_next_card() - def _on_next_card(self): + def _on_next_card(self) -> None: pass def _updateButtons(self) -> None: @@ -315,7 +317,7 @@ class BrowserPreviewer(MultiCardPreviewer): self._last_card_id = c.id return changed - def _on_prev_card(self): + def _on_prev_card(self) -> None: self._parent.editor.saveNow( lambda: self._parent._moveCur(QAbstractItemView.MoveUp) ) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 6a42e4938..efecd2525 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -113,13 +113,13 @@ class LoadMetaResult: class AnkiRestart(SystemExit): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: self.exitcode = kwargs.pop("exitcode", 0) super().__init__(*args, **kwargs) # type: ignore class ProfileManager: - def __init__(self, base: Optional[str] = None) -> None: + def __init__(self, base: Optional[str] = None) -> None: # ## Settings which should be forgotten each Anki restart self.session: Dict[str, Any] = {} self.name: Optional[str] = None @@ -185,7 +185,7 @@ class ProfileManager: self.base = newBase shutil.move(oldBase, self.base) - def _tryToMigrateFolder(self, oldBase): + def _tryToMigrateFolder(self, oldBase) -> None: from PyQt5 import QtGui, QtWidgets app = QtWidgets.QApplication([]) @@ -269,7 +269,7 @@ class ProfileManager: fn = super().find_class(module, name) if module == "sip" and name == "_unpickle_type": - def wrapper(mod, obj, args): + def wrapper(mod, obj, args) -> Any: if mod.startswith("PyQt4") and obj == "QByteArray": # can't trust str objects from python 2 return QByteArray() @@ -534,7 +534,7 @@ create table if not exists profiles def setDefaultLang(self, idx: int) -> None: # create dialog class NoCloseDiag(QDialog): - def reject(self): + def reject(self) -> None: pass d = self.langDiag = NoCloseDiag() @@ -665,7 +665,7 @@ create table if not exists profiles pass return RecordingDriver.QtAudioInput - def set_recording_driver(self, driver: RecordingDriver): + def set_recording_driver(self, driver: RecordingDriver) -> None: self.profile["recordingDriver"] = driver.value ###################################################################### diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index 47229a3ee..ad351d8ba 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -43,7 +43,7 @@ class ProgressManager: timer to fire even when there is no collection, but will still only fire when there is no current progress dialog.""" - def handler(): + def handler() -> None: if requiresCollection and not self.mw.col: # no current collection; timer is no longer valid print("Ignored progress func as collection unloaded: %s" % repr(func)) @@ -225,14 +225,14 @@ class ProgressDialog(QDialog): self._closingDown = True self.hide() - def closeEvent(self, evt): + def closeEvent(self, evt) -> None: if self._closingDown: evt.accept() else: self.wantCancel = True evt.ignore() - def keyPressEvent(self, evt): + def keyPressEvent(self, evt) -> None: if evt.key() == Qt.Key_Escape: evt.ignore() self.wantCancel = True diff --git a/qt/aqt/qt.py b/qt/aqt/qt.py index 43c42b17d..c31e3346b 100644 --- a/qt/aqt/qt.py +++ b/qt/aqt/qt.py @@ -30,7 +30,7 @@ except ImportError: import sip # type: ignore -def debug(): +def debug() -> None: from pdb import set_trace pyqtRemoveInputHook() @@ -39,7 +39,7 @@ def debug(): if os.environ.get("DEBUG"): - def info(type, value, tb): + def info(type, value, tb) -> None: for line in traceback.format_exception(type, value, tb): sys.stdout.write(line) pyqtRemoveInputHook() diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 8a3e05998..1a806a864 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -8,7 +8,7 @@ import html import json import re import unicodedata as ucd -from typing import Any, Callable, List, Optional, Tuple, Union +from typing import Any, Callable, List, Match, Optional, Tuple, Union from PyQt5.QtCore import Qt @@ -426,7 +426,7 @@ class Reviewer: # compare with typed answer res = self.correct(given, cor, showBad=False) # and update the type answer area - def repl(match): + def repl(match: Match) -> str: # can't pass a string in directly, and can't use re.escape as it # escapes too much s = """ @@ -448,7 +448,7 @@ class Reviewer: if not matches: return None - def noHint(txt): + def noHint(txt) -> str: if "::" in txt: return txt.split("::")[0] return txt @@ -652,7 +652,7 @@ time = %(time)d; def _answerButtons(self) -> str: default = self._defaultEase() - def but(i, label): + def but(i, label) -> str: if i == default: extra = """id="defease" class="focus" """ else: @@ -834,7 +834,7 @@ time = %(time)d; tooltip(tr(TR.STUDYING_NOTE_BURIED)) def onRecordVoice(self) -> None: - def after_record(path: str): + def after_record(path: str) -> None: self._recordedAudio = path self.onReplayRecorded() diff --git a/qt/aqt/schema_change_tracker.py b/qt/aqt/schema_change_tracker.py index e489f6de6..3d61fe516 100644 --- a/qt/aqt/schema_change_tracker.py +++ b/qt/aqt/schema_change_tracker.py @@ -17,7 +17,7 @@ class Change(enum.Enum): class ChangeTracker: _changed = Change.NO_CHANGE - def __init__(self, mw: AnkiQt): + def __init__(self, mw: AnkiQt) -> None: self.mw = mw def mark_basic(self) -> None: diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 6e53bf8fa..04b835e3f 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -438,7 +438,7 @@ class MpvManager(MPV, SoundOrVideoPlayer): class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer): - def __init__(self, taskman: TaskManager): + def __init__(self, taskman: TaskManager) -> None: super().__init__(taskman) self.args.append("-slave") @@ -494,7 +494,7 @@ def encode_mp3(mw: aqt.AnkiQt, src_wav: str, on_done: Callable[[str], None]) -> "Encode the provided wav file to .mp3, and call on_done() with the path." dst_mp3 = src_wav.replace(".wav", "%d.mp3" % time.time()) - def _on_done(fut: Future): + def _on_done(fut: Future) -> None: fut.result() on_done(dst_mp3) @@ -509,7 +509,7 @@ class Recorder(ABC): # seconds to wait before recording STARTUP_DELAY = 0.3 - def __init__(self, output_path: str): + def __init__(self, output_path: str) -> None: self.output_path = output_path def start(self, on_done: Callable[[], None]) -> None: @@ -517,7 +517,7 @@ class Recorder(ABC): self._started_at = time.time() on_done() - def stop(self, on_done: Callable[[str], None]): + def stop(self, on_done: Callable[[str], None]) -> None: "Stop recording, then call on_done() when finished." on_done(self.output_path) @@ -525,7 +525,7 @@ class Recorder(ABC): "Seconds since recording started." return time.time() - self._started_at - def on_timer(self): + def on_timer(self) -> None: "Will be called periodically." @@ -534,7 +534,7 @@ class Recorder(ABC): class QtAudioInputRecorder(Recorder): - def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget): + def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: super().__init__(output_path) self.mw = mw @@ -567,11 +567,11 @@ class QtAudioInputRecorder(Recorder): self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore super().start(on_done) - def _on_read_ready(self): + def _on_read_ready(self) -> None: self._buffer += self._iodevice.readAll() - def stop(self, on_done: Callable[[str], None]): - def on_stop_timer(): + def stop(self, on_done: Callable[[str], None]) -> None: + def on_stop_timer() -> None: # read anything remaining in buffer & stop self._on_read_ready() self._audio_input.stop() @@ -580,7 +580,7 @@ class QtAudioInputRecorder(Recorder): showWarning(f"recording failed: {err}") return - def write_file(): + def write_file() -> None: # swallow the first 300ms to allow audio device to quiesce wait = int(44100 * self.STARTUP_DELAY) if len(self._buffer) <= wait: @@ -595,7 +595,7 @@ class QtAudioInputRecorder(Recorder): wf.writeframes(self._buffer) wf.close() - def and_then(fut): + def and_then(fut) -> None: fut.result() Recorder.stop(self, on_done) @@ -672,7 +672,7 @@ class PyAudioThreadedRecorder(threading.Thread): class PyAudioRecorder(Recorder): - def __init__(self, mw: aqt.AnkiQt, output_path: str): + def __init__(self, mw: aqt.AnkiQt, output_path: str) -> None: super().__init__(output_path) self.mw = mw @@ -686,7 +686,7 @@ class PyAudioRecorder(Recorder): while self.duration() < 1: time.sleep(0.1) - def func(fut): + def func(fut) -> None: Recorder.stop(self, on_done) self.thread.finish = True @@ -715,7 +715,7 @@ class RecordDialog(QDialog): self._start_recording() self._setup_dialog() - def _setup_dialog(self): + def _setup_dialog(self) -> None: self.setWindowTitle("Anki") icon = QLabel() icon.setPixmap(QPixmap(":/icons/media-record.png")) @@ -740,10 +740,10 @@ class RecordDialog(QDialog): restoreGeom(self, "audioRecorder2") self.show() - def _save_diag(self): + def _save_diag(self) -> None: saveGeom(self, "audioRecorder2") - def _start_recording(self): + def _start_recording(self) -> None: driver = self.mw.pm.recording_driver() if driver is RecordingDriver.PyAudio: self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav")) @@ -755,18 +755,18 @@ class RecordDialog(QDialog): assert_exhaustive(driver) self._recorder.start(self._start_timer) - def _start_timer(self): + def _start_timer(self) -> None: self._timer = t = QTimer(self._parent) t.timeout.connect(self._on_timer) # type: ignore t.setSingleShot(False) t.start(100) - def _on_timer(self): + def _on_timer(self) -> None: self._recorder.on_timer() duration = self._recorder.duration() self.label.setText(tr(TR.MEDIA_RECORDINGTIME, secs="%0.1f" % duration)) - def accept(self): + def accept(self) -> None: self._timer.stop() try: @@ -775,10 +775,10 @@ class RecordDialog(QDialog): finally: QDialog.accept(self) - def reject(self): + def reject(self) -> None: self._timer.stop() - def cleanup(out: str): + def cleanup(out: str) -> None: os.unlink(out) try: @@ -790,7 +790,7 @@ class RecordDialog(QDialog): def record_audio( parent: QWidget, mw: aqt.AnkiQt, encode: bool, on_done: Callable[[str], None] ): - def after_record(path: str): + def after_record(path: str) -> None: if not encode: on_done(path) else: diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index d9fe1c870..a1c290fc6 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -25,7 +25,7 @@ from aqt.utils import ( class NewDeckStats(QDialog): """New deck stats.""" - def __init__(self, mw: aqt.main.AnkiQt): + def __init__(self, mw: aqt.main.AnkiQt) -> None: QDialog.__init__(self, mw, Qt.Window) mw.setupDialogGC(self) self.mw = mw @@ -60,11 +60,11 @@ class NewDeckStats(QDialog): aqt.dialogs.markClosed("NewDeckStats") QDialog.reject(self) - def closeWithCallback(self, callback): + def closeWithCallback(self, callback) -> None: self.reject() callback() - def _imagePath(self): + def _imagePath(self) -> str: name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time())) name = "anki-" + tr(TR.STATISTICS_STATS) + name file = getSaveFile( @@ -77,17 +77,17 @@ class NewDeckStats(QDialog): ) return file - def saveImage(self): + def saveImage(self) -> None: path = self._imagePath() if not path: return self.form.web.page().printToPdf(path) tooltip(tr(TR.STATISTICS_SAVED)) - def changePeriod(self, n): + def changePeriod(self, n) -> None: pass - def changeScope(self, type): + def changeScope(self, type) -> None: pass def _on_bridge_cmd(self, cmd: str) -> bool: @@ -149,11 +149,11 @@ class DeckStats(QDialog): aqt.dialogs.markClosed("DeckStats") QDialog.reject(self) - def closeWithCallback(self, callback): + def closeWithCallback(self, callback) -> None: self.reject() callback() - def _imagePath(self): + def _imagePath(self) -> str: name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time())) name = "anki-" + tr(TR.STATISTICS_STATS) + name file = getSaveFile( @@ -166,7 +166,7 @@ class DeckStats(QDialog): ) return file - def saveImage(self): + def saveImage(self) -> None: path = self._imagePath() if not path: return diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 5ee750687..61f0ec38c 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -135,7 +135,7 @@ class StudyDeck(QDialog): return False return True - def onReset(self): + def onReset(self) -> None: # model updated? if self.nameFunc: self.origNames = self.nameFunc() diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 695fe8d02..9e81277cd 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -40,12 +40,14 @@ class FullSyncChoice(enum.Enum): DOWNLOAD = 2 -def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]): +def get_sync_status( + mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None] +) -> None: auth = mw.pm.sync_auth() if not auth: - return SyncStatus(required=SyncStatus.NO_CHANGES) # pylint:disable=no-member + callback(SyncStatus(required=SyncStatus.NO_CHANGES)) # pylint:disable=no-member - def on_future_done(fut): + def on_future_done(fut) -> None: try: out = fut.result() except Exception as e: @@ -57,7 +59,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]) mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done) -def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception): +def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None: if isinstance(err, SyncError): if err.is_auth_error(): mw.pm.clear_sync_auth() @@ -87,14 +89,14 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: auth = mw.pm.sync_auth() assert auth - def on_timer(): + def on_timer() -> None: on_normal_sync_timer(mw) timer = QTimer(mw) qconnect(timer.timeout, on_timer) timer.start(150) - def on_future_done(fut): + def on_future_done(fut) -> None: mw.col.db.begin() timer.stop() try: @@ -171,14 +173,14 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None: def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: mw.col.close_for_full_sync() - def on_timer(): + def on_timer() -> None: on_full_sync_timer(mw) timer = QTimer(mw) qconnect(timer.timeout, on_timer) timer.start(150) - def on_future_done(fut): + def on_future_done(fut) -> None: timer.stop() mw.col.reopen(after_full_sync=True) mw.reset() @@ -199,14 +201,14 @@ def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: mw.col.close_for_full_sync() - def on_timer(): + def on_timer() -> None: on_full_sync_timer(mw) timer = QTimer(mw) qconnect(timer.timeout, on_timer) timer.start(150) - def on_future_done(fut): + def on_future_done(fut) -> None: timer.stop() mw.col.reopen(after_full_sync=True) mw.reset() @@ -235,7 +237,7 @@ def sync_login( if username and password: break - def on_future_done(fut): + def on_future_done(fut) -> None: try: auth = fut.result() except SyncError as e: diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index c28392a6d..e9e182ebf 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -68,7 +68,7 @@ class TagLimit(QDialog): idx = self.dialog.inactiveList.indexFromItem(item) self.dialog.inactiveList.selectionModel().select(idx, mode) - def reject(self): + def reject(self) -> None: self.tags = "" QDialog.reject(self) diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index 264b4f6c1..53040d7b4 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -31,7 +31,7 @@ class TaskManager(QObject): self._closures_lock = Lock() qconnect(self._closures_pending, self._on_closures_pending) - def run_on_main(self, closure: Closure): + def run_on_main(self, closure: Closure) -> None: "Run the provided closure on the main thread." with self._closures_lock: self._closures.append(closure) @@ -71,7 +71,7 @@ class TaskManager(QObject): ): self.mw.progress.start(parent=parent, label=label, immediate=immediate) - def wrapped_done(fut): + def wrapped_done(fut) -> None: self.mw.progress.finish() if on_done: on_done(fut) diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index 0bf9f9342..875983685 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -481,7 +481,7 @@ if isWin: return [] return list(map(self._voice_to_object, self.speaker.GetVoices())) - def _voice_to_object(self, voice: Any): + def _voice_to_object(self, voice: Any) -> WindowsVoice: lang = voice.GetAttribute("language") lang = lcid_hex_str_to_lang_code(lang) name = self._tidy_name(voice.GetAttribute("name")) @@ -561,7 +561,7 @@ if isWin: ) asyncio.run(self.speakText(tag, voice.id)) - def _on_done(self, ret: Future, cb: OnDoneCallback): + def _on_done(self, ret: Future, cb: OnDoneCallback) -> None: ret.result() # inject file into the top of the audio queue @@ -572,7 +572,7 @@ if isWin: # then tell player to advance, which will cause the file to be played cb() - async def speakText(self, tag: TTSTag, voice_id): + async def speakText(self, tag: TTSTag, voice_id) -> None: import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore import winrt.windows.storage.streams as streams # type: ignore diff --git a/qt/aqt/update.py b/qt/aqt/update.py index 32f956eb6..9c940fe3c 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -2,6 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import time +from typing import Any, Dict import requests @@ -23,7 +24,7 @@ class LatestVersionFinder(QThread): self.main = main self.config = main.pm.meta - def _data(self): + def _data(self) -> Dict[str, Any]: return { "ver": versionWithBuild(), "os": platDesc(), @@ -32,7 +33,7 @@ class LatestVersionFinder(QThread): "crt": self.config["created"], } - def run(self): + def run(self) -> None: if not self.config["updates"]: return d = self._data() @@ -55,7 +56,7 @@ class LatestVersionFinder(QThread): self.clockIsOff.emit(diff) # type: ignore -def askAndUpdate(mw, ver): +def askAndUpdate(mw, ver) -> None: baseStr = tr(TR.QT_MISC_ANKI_UPDATEDANKI_HAS_BEEN_RELEASED, val=ver) msg = QMessageBox(mw) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore @@ -72,6 +73,6 @@ def askAndUpdate(mw, ver): openLink(aqt.appWebsite) -def showMessages(mw, data): +def showMessages(mw, data) -> None: showText(data["msg"], parent=mw, type="html") mw.pm.meta["lastMsg"] = data["msgId"] diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 8fe1b8438..869f30928 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -22,7 +22,7 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+") class AnkiWebPage(QWebEnginePage): - def __init__(self, onBridgeCmd): + def __init__(self, onBridgeCmd) -> None: QWebEnginePage.__init__(self) self._onBridgeCmd = onBridgeCmd self._setupBridge() @@ -31,7 +31,7 @@ class AnkiWebPage(QWebEnginePage): def _setupBridge(self) -> None: class Bridge(QObject): @pyqtSlot(str, result=str) # type: ignore - def cmd(self, str): + def cmd(self, str) -> Any: return json.dumps(self.onCmd(str)) self._bridge = Bridge() @@ -74,7 +74,7 @@ class AnkiWebPage(QWebEnginePage): script.setRunsOnSubFrames(False) self.profile().scripts().insert(script) - def javaScriptConsoleMessage(self, level, msg, line, srcID): + def javaScriptConsoleMessage(self, level, msg, line, srcID) -> None: # not translated because console usually not visible, # and may only accept ascii text if srcID.startswith("data"): @@ -101,7 +101,7 @@ class AnkiWebPage(QWebEnginePage): # https://github.com/ankitects/anki/pull/560 sys.stdout.write(buf) - def acceptNavigationRequest(self, url, navType, isMainFrame): + def acceptNavigationRequest(self, url, navType, isMainFrame) -> bool: if not self.open_links_externally: return super().acceptNavigationRequest(url, navType, isMainFrame) @@ -120,10 +120,10 @@ class AnkiWebPage(QWebEnginePage): openLink(url) return False - def _onCmd(self, str): + def _onCmd(self, str) -> None: return self._onBridgeCmd(str) - def javaScriptAlert(self, url: QUrl, text: str): + def javaScriptAlert(self, url: QUrl, text: str) -> None: showInfo(text) @@ -150,7 +150,7 @@ class WebContent: You should avoid overwriting or interfering with existing data as much as possible, instead opting to append your own changes, e.g.: - def on_webview_will_set_content(web_content: WebContent, context): + def on_webview_will_set_content(web_content: WebContent, context) -> None: web_content.body += "" web_content.head += "" @@ -173,7 +173,7 @@ class WebContent: Then append the subpaths to the corresponding web_content fields within a function subscribing to gui_hooks.webview_will_set_content: - def on_webview_will_set_content(web_content: WebContent, context): + def on_webview_will_set_content(web_content: WebContent, context) -> None: addon_package = mw.addonManager.addonFromModule(__name__) web_content.css.append( f"/_addons/{addon_package}/web/my-addon.css") @@ -251,7 +251,7 @@ class AnkiWebView(QWebEngineView): def set_open_links_externally(self, enable: bool) -> None: self._page.open_links_externally = enable - def onEsc(self): + def onEsc(self) -> None: w = self.parent() while w: if isinstance(w, QDialog) or isinstance(w, QMainWindow): @@ -266,7 +266,7 @@ class AnkiWebView(QWebEngineView): break w = w.parent() - def onCopy(self): + def onCopy(self) -> None: if not self.selectedText(): ctx = self._page.contextMenuData() if ctx and ctx.mediaType() == QWebEngineContextMenuData.MediaTypeImage: @@ -274,16 +274,16 @@ class AnkiWebView(QWebEngineView): else: self.triggerPageAction(QWebEnginePage.Copy) - def onCut(self): + def onCut(self) -> None: self.triggerPageAction(QWebEnginePage.Cut) - def onPaste(self): + def onPaste(self) -> None: self.triggerPageAction(QWebEnginePage.Paste) - def onMiddleClickPaste(self): + def onMiddleClickPaste(self) -> None: self.triggerPageAction(QWebEnginePage.Paste) - def onSelectAll(self): + def onSelectAll(self) -> None: self.triggerPageAction(QWebEnginePage.SelectAll) def contextMenuEvent(self, evt: QContextMenuEvent) -> None: @@ -293,7 +293,7 @@ class AnkiWebView(QWebEngineView): gui_hooks.webview_will_show_context_menu(self, m) m.popup(QCursor.pos()) - def dropEvent(self, evt): + def dropEvent(self, evt) -> None: pass def setHtml(self, html: str) -> None: # type: ignore @@ -312,7 +312,7 @@ class AnkiWebView(QWebEngineView): if oldFocus: oldFocus.setFocus() - def load(self, url: QUrl): + def load(self, url: QUrl) -> None: # allow queuing actions when loading url directly self._domDone = False super().load(url) @@ -364,7 +364,7 @@ class AnkiWebView(QWebEngineView): else: return 3 - def _getWindowColor(self): + def _getWindowColor(self) -> QColor: if theme_manager.night_mode: return theme_manager.qcolor("window-bg") if isMac: @@ -508,7 +508,7 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }} def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None: if cb: - def handler(val): + def handler(val) -> None: if self._shouldIgnoreWebEvent(): print("ignored late js callback", cb) return @@ -597,18 +597,18 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }} self.onBridgeCmd = func self._bridge_context = context - def hide_while_preserving_layout(self): + def hide_while_preserving_layout(self) -> None: "Hide but keep existing size." sp = self.sizePolicy() sp.setRetainSizeWhenHidden(True) self.setSizePolicy(sp) self.hide() - def inject_dynamic_style_and_show(self): + def inject_dynamic_style_and_show(self) -> None: "Add dynamic styling, and reveal." css = self.standard_css() - def after_style(arg): + def after_style(arg) -> None: gui_hooks.webview_did_inject_style_into_page(self) self.show() diff --git a/qt/dmypy-watch.sh b/qt/dmypy-watch.sh index d5e2b3a00..86109eb62 100755 --- a/qt/dmypy-watch.sh +++ b/qt/dmypy-watch.sh @@ -9,5 +9,5 @@ (sleep 1 && touch aqt) . ~/pyenv/bin/activate -fswatch -o aqt | xargs -n1 -I{} sh -c 'printf \\033c; dmypy run aqt' +fswatch -o aqt | xargs -n1 -I{} sh -c 'printf \\033c\\n; dmypy run aqt'