Refactor: Add manifest schema, unify install paths, use context manager

Sets the foundation for more elaborate additions to the manifest.

Manifest files are still only being read for local imports, but with
this commit that could be easily changed in the future.
This commit is contained in:
Glutanimate 2019-02-22 17:04:07 +01:00
parent 2ed61c9c99
commit 8fceccf4b7

View file

@ -22,6 +22,12 @@ from anki.sync import AnkiRequestsClient
class AddonManager: class AddonManager:
ext = ".ankiaddon" ext = ".ankiaddon"
# todo?: use jsonschema package
_manifest_schema = {
"package": {"type": str, "req": True, "meta": False},
"name": {"type": str, "req": True, "meta": True},
"mod": {"type": int, "req": False, "meta": True}
}
def __init__(self, mw): def __init__(self, mw):
self.mw = mw self.mw = mw
@ -102,42 +108,49 @@ When loading '%(name)s':
# Installing and deleting add-ons # Installing and deleting add-ons
###################################################################### ######################################################################
def installDownload(self, sid, data, name): def _readManifestFile(self, zfile):
try:
zfile = ZipFile(io.BytesIO(data))
except zipfile.BadZipfile:
showWarning(_("The download was corrupt. Please try again."))
return False
mod = intTime()
self.install(sid, zfile, name, mod)
def installLocal(self, path):
try:
zfile = ZipFile(path)
except zipfile.BadZipfile:
return False, _("Corrupt add-on archive.")
try: try:
with zfile.open("manifest.json") as f: with zfile.open("manifest.json") as f:
info = json.loads(f.read()) data = json.loads(f.read())
package = info["package"] manifest = {} # build new manifest from recognized keys
name = info["name"] for key, attrs in self._manifest_schema.items():
mod = info.get("mod", None) if not attrs["req"] and key not in data:
assert isinstance(package, str) and isinstance(name, str) continue
assert isinstance(mod, int) or mod is None val = data[key]
assert isinstance(val, attrs["type"])
manifest[key] = val
except (KeyError, json.decoder.JSONDecodeError, AssertionError): except (KeyError, json.decoder.JSONDecodeError, AssertionError):
# raised for missing manifest, invalid json, missing/invalid keys # raised for missing manifest, invalid json, missing/invalid keys
return False, _("Invalid add-on manifest.") return {}
return manifest
self.install(package, zfile, name, mod) def install(self, file, manifest=None):
"""Install add-on from path or file-like object. Metadata is read
from the manifest file by default, but this may me bypassed
by supplying a 'manifest' dictionary"""
try:
zfile = ZipFile(file)
except zipfile.BadZipfile:
return False, "zip"
return name, None with zfile:
manifest = manifest or self._readManifestFile(zfile)
if not manifest:
return False, "manifest"
package = manifest["package"]
meta = self.addonMeta(package)
self._install(package, zfile)
def install(self, dir, zfile, name, mod): schema = self._manifest_schema
manifest_meta = {k: v for k, v in manifest.items()
if k in schema and schema[k]["meta"]}
meta.update(manifest_meta)
self.writeAddonMeta(package, meta)
return True, meta["name"]
def _install(self, dir, zfile):
# previously installed? # previously installed?
meta = self.addonMeta(dir)
base = self.addonsFolder(dir) base = self.addonsFolder(dir)
if os.path.exists(base): if os.path.exists(base):
self.backupUserFiles(dir) self.backupUserFiles(dir)
@ -158,12 +171,6 @@ When loading '%(name)s':
continue continue
zfile.extract(n, base) zfile.extract(n, base)
# update metadata
meta['name'] = name
if mod is not None: # allow packages to skip updating mod
meta['mod'] = mod
self.writeAddonMeta(dir, meta)
def deleteAddon(self, dir): def deleteAddon(self, dir):
send2trash(self.addonsFolder(dir)) send2trash(self.addonsFolder(dir))
@ -176,12 +183,16 @@ When loading '%(name)s':
self.mw.progress.start(immediate=True) self.mw.progress.start(immediate=True)
for path in paths: for path in paths:
base = os.path.basename(path) base = os.path.basename(path)
name, error = self.installLocal(path) ret = self.install(path)
if error: if ret[0] is False:
if ret[1] == "zip":
msg = _("Corrupt add-on file.")
elif ret[1] == "manifest":
msg = _("Invalid add-on manifest.")
errs.append(_("Error installing <i>%(base)s</i>: %(error)s" errs.append(_("Error installing <i>%(base)s</i>: %(error)s"
% dict(base=base, error=error))) % dict(base=base, error=msg)))
else: else:
log.append(_("Installed %(name)s" % dict(name=name))) log.append(_("Installed %(name)s" % dict(name=ret[1])))
self.mw.progress.finish() self.mw.progress.finish()
return log, errs return log, errs
@ -200,7 +211,14 @@ When loading '%(name)s':
data, fname = ret data, fname = ret
fname = fname.replace("_", " ") fname = fname.replace("_", " ")
name = os.path.splitext(fname)[0] name = os.path.splitext(fname)[0]
self.installDownload(str(n), data, name) ret = self.install(io.BytesIO(data),
manifest={"package": str(n), "name": name,
"mod": intTime()})
if ret[0] is False:
if ret[1] == "zip":
showWarning(_("The download was corrupt. Please try again."))
elif ret[1] == "manifest":
showWarning(_("Invalid add-on manifest."))
log.append(_("Downloaded %(fname)s" % dict(fname=name))) log.append(_("Downloaded %(fname)s" % dict(fname=name)))
self.mw.progress.finish() self.mw.progress.finish()
return log, errs return log, errs