Use jsonschema for add-on manifests
This commit is contained in:
parent
ed0fae3c0c
commit
2f75d1758d
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
83
tests/test_addons.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user