Added typehints for qt profiles

* `Any` used for pickle methods, this could probably be improved
with some kind of Callable
* str used for self.base, though this may be a problem for
different OSes. Some type of os.PathLike might be good.
* Line 75, type ignored: mypy was complaining about no. of args,
and kwargs there didn't seem to be needed. Separate issue to test,
though.
This commit is contained in:
Fabian Wood 2020-07-31 00:56:48 +10:00
parent 215413ce25
commit 79e8076685
2 changed files with 39 additions and 36 deletions

View file

@ -72,7 +72,7 @@ class LoadMetaResult:
class AnkiRestart(SystemExit): class AnkiRestart(SystemExit):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exitcode = kwargs.pop("exitcode", 0) self.exitcode = kwargs.pop("exitcode", 0)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) # type: ignore
class ProfileManager: class ProfileManager:
@ -83,6 +83,7 @@ class ProfileManager:
self.db = None self.db = None
self.profile: Optional[Dict] = None self.profile: Optional[Dict] = None
# instantiate base folder # instantiate base folder
self.base: str
self._setBaseFolder(base) self._setBaseFolder(base)
def setupMeta(self) -> LoadMetaResult: def setupMeta(self) -> LoadMetaResult:
@ -92,7 +93,7 @@ class ProfileManager:
return res return res
# profile load on startup # profile load on startup
def openProfile(self, profile): def openProfile(self, profile) -> None:
if profile: if profile:
if profile not in self.profiles(): if profile not in self.profiles():
QMessageBox.critical(None, "Error", "Requested profile does not exist.") QMessageBox.critical(None, "Error", "Requested profile does not exist.")
@ -105,13 +106,13 @@ class ProfileManager:
# Base creation # Base creation
###################################################################### ######################################################################
def ensureBaseExists(self): def ensureBaseExists(self) -> None:
self._ensureExists(self.base) self._ensureExists(self.base)
# Folder migration # Folder migration
###################################################################### ######################################################################
def _oldFolderLocation(self): def _oldFolderLocation(self) -> str:
if isMac: if isMac:
return os.path.expanduser("~/Documents/Anki") return os.path.expanduser("~/Documents/Anki")
elif isWin: elif isWin:
@ -153,7 +154,7 @@ class ProfileManager:
confirmation = QMessageBox() confirmation = QMessageBox()
confirmation.setIcon(QMessageBox.Warning) confirmation.setIcon(QMessageBox.Warning)
confirmation.setWindowIcon(icon) confirmation.setWindowIcon(icon)
confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
confirmation.setWindowTitle(window_title) confirmation.setWindowTitle(window_title)
confirmation.setText( confirmation.setText(
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?" "Anki needs to move its data folder from Documents/Anki to a new location. Proceed?"
@ -168,7 +169,7 @@ class ProfileManager:
progress.setWindowTitle(window_title) progress.setWindowTitle(window_title)
progress.setText("Please wait...") progress.setText("Please wait...")
progress.show() progress.show()
app.processEvents() app.processEvents() # type: ignore
shutil.move(oldBase, self.base) shutil.move(oldBase, self.base)
progress.hide() progress.hide()
@ -198,8 +199,8 @@ class ProfileManager:
# Profile load/save # Profile load/save
###################################################################### ######################################################################
def profiles(self): def profiles(self) -> List:
def names(): def names() -> List:
return self.db.list("select name from profiles where name != '_global'") return self.db.list("select name from profiles where name != '_global'")
n = names() n = names()
@ -209,9 +210,9 @@ class ProfileManager:
return n return n
def _unpickle(self, data): def _unpickle(self, data) -> Any:
class Unpickler(pickle.Unpickler): class Unpickler(pickle.Unpickler):
def find_class(self, module, name): def find_class(self, module: str, name: str) -> Any:
if module == "PyQt5.sip": if module == "PyQt5.sip":
try: try:
import PyQt5.sip # pylint: disable=unused-import import PyQt5.sip # pylint: disable=unused-import
@ -234,10 +235,10 @@ class ProfileManager:
up = Unpickler(io.BytesIO(data), errors="ignore") up = Unpickler(io.BytesIO(data), errors="ignore")
return up.load() return up.load()
def _pickle(self, obj): def _pickle(self, obj) -> Any:
return pickle.dumps(obj, protocol=0) return pickle.dumps(obj, protocol=0)
def load(self, name): def load(self, name) -> bool:
assert name != "_global" assert name != "_global"
data = self.db.scalar( data = self.db.scalar(
"select cast(data as blob) from profiles where name = ?", name "select cast(data as blob) from profiles where name = ?", name
@ -261,32 +262,32 @@ details have been forgotten."""
self.save() self.save()
return True return True
def save(self): def save(self) -> None:
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, self._pickle(self.profile), self.name) self.db.execute(sql, self._pickle(self.profile), self.name)
self.db.execute(sql, self._pickle(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
self.db.commit() self.db.commit()
def create(self, name): def create(self, name) -> None:
prof = profileConf.copy() prof = profileConf.copy()
self.db.execute( self.db.execute(
"insert or ignore into profiles values (?, ?)", name, self._pickle(prof) "insert or ignore into profiles values (?, ?)", name, self._pickle(prof)
) )
self.db.commit() self.db.commit()
def remove(self, name): def remove(self, name) -> None:
p = self.profileFolder() p = self.profileFolder()
if os.path.exists(p): if os.path.exists(p):
send2trash(p) send2trash(p)
self.db.execute("delete from profiles where name = ?", name) self.db.execute("delete from profiles where name = ?", name)
self.db.commit() self.db.commit()
def trashCollection(self): def trashCollection(self) -> None:
p = self.collectionPath() p = self.collectionPath()
if os.path.exists(p): if os.path.exists(p):
send2trash(p) send2trash(p)
def rename(self, name): def rename(self, name) -> None:
oldName = self.name oldName = self.name
oldFolder = self.profileFolder() oldFolder = self.profileFolder()
self.name = name self.name = name
@ -337,19 +338,19 @@ and no other programs are accessing your profile folders, then try again."""
# Folder handling # Folder handling
###################################################################### ######################################################################
def profileFolder(self, create=True): def profileFolder(self, create=True) -> str:
path = os.path.join(self.base, self.name) path = os.path.join(self.base, self.name)
if create: if create:
self._ensureExists(path) self._ensureExists(path)
return path return path
def addonFolder(self): def addonFolder(self) -> str:
return self._ensureExists(os.path.join(self.base, "addons21")) return self._ensureExists(os.path.join(self.base, "addons21"))
def backupFolder(self): def backupFolder(self) -> str:
return self._ensureExists(os.path.join(self.profileFolder(), "backups")) return self._ensureExists(os.path.join(self.profileFolder(), "backups"))
def collectionPath(self): def collectionPath(self) -> str:
return os.path.join(self.profileFolder(), "collection.anki2") return os.path.join(self.profileFolder(), "collection.anki2")
# Downgrade # Downgrade
@ -377,12 +378,12 @@ and no other programs are accessing your profile folders, then try again."""
# Helpers # Helpers
###################################################################### ######################################################################
def _ensureExists(self, path): def _ensureExists(self, path: str) -> str:
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
return path return path
def _setBaseFolder(self, cmdlineBase): def _setBaseFolder(self, cmdlineBase: None) -> None:
if cmdlineBase: if cmdlineBase:
self.base = os.path.abspath(cmdlineBase) self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"): elif os.environ.get("ANKI_BASE"):
@ -392,7 +393,7 @@ and no other programs are accessing your profile folders, then try again."""
self.maybeMigrateFolder() self.maybeMigrateFolder()
self.ensureBaseExists() self.ensureBaseExists()
def _defaultBase(self): def _defaultBase(self) -> str:
if isWin: if isWin:
from aqt.winpaths import get_appdata from aqt.winpaths import get_appdata
@ -419,7 +420,7 @@ and no other programs are accessing your profile folders, then try again."""
result.firstTime = not os.path.exists(path) result.firstTime = not os.path.exists(path)
def recover(): def recover() -> None:
# if we can't load profile, start with a new one # if we can't load profile, start with a new one
if self.db: if self.db:
try: try:
@ -471,7 +472,7 @@ create table if not exists profiles
) )
return result return result
def _ensureProfile(self): def _ensureProfile(self) -> None:
"Create a new profile if none exists." "Create a new profile if none exists."
self.create(_("User 1")) self.create(_("User 1"))
p = os.path.join(self.base, "README.txt") p = os.path.join(self.base, "README.txt")
@ -486,7 +487,7 @@ create table if not exists profiles
###################################################################### ######################################################################
# On first run, allow the user to choose the default language # On first run, allow the user to choose the default language
def setDefaultLang(self): def setDefaultLang(self) -> None:
# create dialog # create dialog
class NoCloseDiag(QDialog): class NoCloseDiag(QDialog):
def reject(self): def reject(self):
@ -519,20 +520,20 @@ create table if not exists profiles
f.lang.setCurrentRow(idx) f.lang.setCurrentRow(idx)
d.exec_() d.exec_()
def _onLangSelected(self): def _onLangSelected(self) -> None:
f = self.langForm f = self.langForm
obj = anki.lang.langs[f.lang.currentRow()] obj = anki.lang.langs[f.lang.currentRow()]
code = obj[1] code = obj[1]
name = obj[0] name = obj[0]
en = "Are you sure you wish to display Anki's interface in %s?" en = "Are you sure you wish to display Anki's interface in %s?"
r = QMessageBox.question( r = QMessageBox.question(
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No # type: ignore
) )
if r != QMessageBox.Yes: if r != QMessageBox.Yes:
return self.setDefaultLang() return self.setDefaultLang()
self.setLang(code) self.setLang(code)
def setLang(self, code): def setLang(self, code) -> None:
self.meta["defaultLang"] = code self.meta["defaultLang"] = code
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, self._pickle(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
@ -542,10 +543,10 @@ create table if not exists profiles
# OpenGL # OpenGL
###################################################################### ######################################################################
def _glPath(self): def _glPath(self) -> str:
return os.path.join(self.base, "gldriver") return os.path.join(self.base, "gldriver")
def glMode(self): def glMode(self) -> str:
if isMac: if isMac:
return "auto" return "auto"
@ -562,11 +563,11 @@ create table if not exists profiles
return mode return mode
return "auto" return "auto"
def setGlMode(self, mode): def setGlMode(self, mode) -> None:
with open(self._glPath(), "w") as file: with open(self._glPath(), "w") as file:
file.write(mode) file.write(mode)
def nextGlMode(self): def nextGlMode(self) -> None:
mode = self.glMode() mode = self.glMode()
if mode == "software": if mode == "software":
self.setGlMode("auto") self.setGlMode("auto")
@ -591,7 +592,7 @@ create table if not exists profiles
def last_addon_update_check(self) -> int: def last_addon_update_check(self) -> int:
return self.meta.get("last_addon_update_check", 0) return self.meta.get("last_addon_update_check", 0)
def set_last_addon_update_check(self, secs): def set_last_addon_update_check(self, secs) -> None:
self.meta["last_addon_update_check"] = secs self.meta["last_addon_update_check"] = secs
def night_mode(self) -> bool: def night_mode(self) -> bool:
@ -642,7 +643,7 @@ create table if not exists profiles
def auto_sync_media_minutes(self) -> int: def auto_sync_media_minutes(self) -> int:
return self.profile.get("autoSyncMediaMinutes", 15) return self.profile.get("autoSyncMediaMinutes", 15)
def set_auto_sync_media_minutes(self, val: int): def set_auto_sync_media_minutes(self, val: int) -> None:
self.profile["autoSyncMediaMinutes"] = val self.profile["autoSyncMediaMinutes"] = val
###################################################################### ######################################################################

View file

@ -88,3 +88,5 @@ check_untyped_defs=true
check_untyped_defs=true check_untyped_defs=true
[mypy-aqt.modelchooser] [mypy-aqt.modelchooser]
check_untyped_defs=true check_untyped_defs=true
[mypy-aqt.profiles]
check_untyped_defs=true