Use jsonschema for add-on manifests

This commit is contained in:
Erez Volk 2019-04-24 22:44:11 +03:00
parent ed0fae3c0c
commit 2f75d1758d
4 changed files with 109 additions and 19 deletions

View File

@ -2,11 +2,11 @@
set -e set -e
echo "running unit tests..."
nosetests ./tests
echo "building ui..." echo "building ui..."
./tools/build_ui.sh ./tools/build_ui.sh
echo "running unit tests..."
nosetests ./tests
echo "linting..." echo "linting..."
./tools/lint.sh ./tools/lint.sh

View File

@ -8,6 +8,8 @@ import zipfile
from collections import defaultdict from collections import defaultdict
import markdown import markdown
from send2trash import send2trash from send2trash import send2trash
import jsonschema
from jsonschema.exceptions import ValidationError
from aqt.qt import * from aqt.qt import *
from aqt.utils import showInfo, openFolder, isWin, openLink, \ from aqt.utils import showInfo, openFolder, isWin, openLink, \
@ -24,12 +26,19 @@ from anki.sync import AnkiRequestsClient
class AddonManager: class AddonManager:
ext = ".ankiaddon" ext = ".ankiaddon"
# todo?: use jsonschema package
_manifest_schema = { _manifest_schema = {
"package": {"type": str, "req": True, "meta": False}, "type": "object",
"name": {"type": str, "req": True, "meta": True}, "properties": {
"mod": {"type": int, "req": False, "meta": True}, "package": {"type": "string", "meta": False},
"conflicts": {"type": list, "req": False, "meta": True} "name": {"type": "string", "meta": True},
"mod": {"type": "number", "meta": True},
"conflicts": {
"type": "array",
"items": {"type": "string"},
"meta": True
}
},
"required": ["package", "name"]
} }
def __init__(self, mw): def __init__(self, mw):
@ -161,18 +170,15 @@ and have been disabled: %(found)s") % dict(name=self.addonName(dir), found=addon
# Installing and deleting add-ons # Installing and deleting add-ons
###################################################################### ######################################################################
def _readManifestFile(self, zfile): def readManifestFile(self, zfile):
try: try:
with zfile.open("manifest.json") as f: with zfile.open("manifest.json") as f:
data = json.loads(f.read()) data = json.loads(f.read())
manifest = {} # build new manifest from recognized keys jsonschema.validate(data, self._manifest_schema)
for key, attrs in self._manifest_schema.items(): # build new manifest from recognized keys
if not attrs["req"] and key not in data: schema = self._manifest_schema["properties"]
continue manifest = {key: data[key] for key in data.keys() & schema.keys()}
val = data[key] except (KeyError, json.decoder.JSONDecodeError, ValidationError):
assert isinstance(val, attrs["type"])
manifest[key] = val
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 {} return {}
return manifest return manifest
@ -187,7 +193,7 @@ and have been disabled: %(found)s") % dict(name=self.addonName(dir), found=addon
return False, "zip" return False, "zip"
with zfile: with zfile:
file_manifest = self._readManifestFile(zfile) file_manifest = self.readManifestFile(zfile)
if manifest: if manifest:
file_manifest.update(manifest) file_manifest.update(manifest)
manifest = file_manifest manifest = file_manifest
@ -200,7 +206,7 @@ and have been disabled: %(found)s") % dict(name=self.addonName(dir), found=addon
meta = self.addonMeta(package) meta = self.addonMeta(package)
self._install(package, zfile) self._install(package, zfile)
schema = self._manifest_schema schema = self._manifest_schema["properties"]
manifest_meta = {k: v for k, v in manifest.items() manifest_meta = {k: v for k, v in manifest.items()
if k in schema and schema[k]["meta"]} if k in schema and schema[k]["meta"]}
meta.update(manifest_meta) meta.update(manifest_meta)

View File

@ -4,5 +4,6 @@ pyaudio
requests requests
decorator decorator
markdown markdown
jsonschema
psutil; sys_platform == "win32" psutil; sys_platform == "win32"
distro; sys_platform != "win32" and sys_platform != "darwin" distro; sys_platform != "win32" and sys_platform != "darwin"

83
tests/test_addons.py Normal file
View File

@ -0,0 +1,83 @@
import os.path
from nose.tools import assert_equals
from mock import MagicMock
from tempfile import TemporaryDirectory
from zipfile import ZipFile
from aqt.addons import AddonManager
def test_readMinimalManifest():
assertReadManifest(
'{"package": "yes", "name": "no"}',
{"package": "yes", "name": "no"}
)
def test_readExtraKeys():
assertReadManifest(
'{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}',
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}
)
def test_invalidManifest():
assertReadManifest(
'{"one": 1}',
{}
)
def test_mustHaveName():
assertReadManifest(
'{"package": "something"}',
{}
)
def test_mustHavePackage():
assertReadManifest(
'{"name": "something"}',
{}
)
def test_invalidJson():
assertReadManifest(
'this is not a JSON dictionary',
{}
)
def test_missingManifest():
assertReadManifest(
'{"package": "what", "name": "ever"}',
{},
nameInZip="not-manifest.bin"
)
def test_ignoreExtraKeys():
assertReadManifest(
'{"package": "a", "name": "b", "game": "c"}',
{"package": "a", "name": "b"}
)
def test_conflictsMustBeStrings():
assertReadManifest(
'{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}',
{}
)
def assertReadManifest(contents, expectedManifest, nameInZip="manifest.json"):
with TemporaryDirectory() as td:
zfn = os.path.join(td, "addon.zip")
with ZipFile(zfn, "w") as zfile:
zfile.writestr(nameInZip, contents)
adm = AddonManager(MagicMock())
with ZipFile(zfn, "r") as zfile:
assert_equals(adm.readManifestFile(zfile), expectedManifest)