DataModel: allow valid values in non-strict mode for TypeOfList and TypeOfDict

This commit is contained in:
Ivan Kravets
2019-10-04 18:30:48 +03:00
parent 47e297fecb
commit 36acdd7797
3 changed files with 213 additions and 85 deletions

View File

@@ -74,8 +74,8 @@ class DataField(object):
self.validate_factory = validate_factory self.validate_factory = validate_factory
self.title = title self.title = title
self.parent = None self._parent = None
self.name = None self._name = None
self.value = None self.value = None
def __repr__(self): def __repr__(self):
@@ -84,13 +84,24 @@ class DataField(object):
self.default if self.value is None else self.value, self.default if self.value is None else self.value,
) )
def validate( @property
self, parent, name, value def parent(self):
): # pylint: disable=too-many-return-statements return self._parent
self.parent = parent
self.name = name
self.title = self.title or name.title()
@parent.setter
def parent(self, value):
self._parent = value
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
self.title = self.title or value.title()
def validate(self, value):
try: try:
if self.required and value is None: if self.required and value is None:
raise ValueError("Missed value") raise ValueError("Missed value")
@@ -99,38 +110,19 @@ class DataField(object):
if value is None: if value is None:
return self.default return self.default
if inspect.isclass(self.type) and issubclass(self.type, DataModel): if inspect.isclass(self.type) and issubclass(self.type, DataModel):
return self.type(**self._ensure_value_is_dict(value)) return self.type(**self.ensure_value_is_dict(value))
if isinstance(self.type, ListOfType): if inspect.isclass(self.type) and issubclass(self.type, (str, bool)):
return self._validate_list_of_type(self.type.type, value)
if isinstance(self.type, DictOfType):
return self._validate_dict_of_type(self.type.type, value)
if issubclass(self.type, (str, bool)):
return getattr(self, "_validate_%s_value" % self.type.__name__)(value) return getattr(self, "_validate_%s_value" % self.type.__name__)(value)
except ValueError as e: except ValueError as e:
raise DataFieldException(self, str(e)) raise DataFieldException(self, str(e))
return value return value
@staticmethod @staticmethod
def _ensure_value_is_dict(value): def ensure_value_is_dict(value):
if not isinstance(value, dict): if not isinstance(value, dict):
raise ValueError("Value should be type of dict, not `%s`" % type(value)) raise ValueError("Value should be type of dict, not `%s`" % type(value))
return value return value
def _validate_list_of_type(self, list_of_type, value):
if not isinstance(value, list):
raise ValueError("Value should be a list")
if isinstance(list_of_type, DataField):
return [list_of_type.validate(self.parent, self.name, v) for v in value]
assert issubclass(list_of_type, DataModel)
return [list_of_type(**self._ensure_value_is_dict(v)) for v in value]
def _validate_dict_of_type(self, dict_of_type, value):
assert issubclass(dict_of_type, DataModel)
value = self._ensure_value_is_dict(value)
return {
k: dict_of_type(**self._ensure_value_is_dict(v)) for k, v in value.items()
}
def _validate_str_value(self, value): def _validate_str_value(self, value):
if not isinstance(value, string_types): if not isinstance(value, string_types):
value = str(value) value = str(value)
@@ -162,21 +154,94 @@ class DataModel(object):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._field_names = [] self._field_names = []
self._exceptions = [] self._exceptions = set()
for name, field in get_class_attributes(self).items(): for name, field in get_class_attributes(self).items():
if not isinstance(field, DataField): if not isinstance(field, DataField):
continue continue
field.parent = self
field.name = name
self._field_names.append(name) self._field_names.append(name)
raw_value = kwargs.get(name)
value = None value = None
try: try:
value = field.validate(self, name, kwargs.get(name)) if isinstance(field.type, ListOfType):
value = self._validate_list_of_type(field, name, raw_value)
elif isinstance(field.type, DictOfType):
value = self._validate_dict_of_type(field, name, raw_value)
else:
value = field.validate(raw_value)
except DataFieldException as e: except DataFieldException as e:
self._exceptions.append(e) self._exceptions.add(e)
if isinstance(self, StrictDataModel): if isinstance(self, StrictDataModel):
raise e raise e
finally: finally:
setattr(self, name, value) setattr(self, name, value)
def _validate_list_of_type(self, field, name, value):
data_type = field.type.type
# check if ListOfType is not required
value = field.validate(value)
if not value:
return None
if not isinstance(value, list):
raise DataFieldException(field, "Value should be a list")
if isinstance(data_type, DataField):
result = []
data_type.parent = self
data_type.name = name
for v in value:
try:
result.append(data_type.validate(v))
except DataFieldException as e:
self._exceptions.add(e)
if isinstance(self, StrictDataModel):
raise e
return result
assert issubclass(data_type, DataModel)
result = []
for v in value:
try:
if not isinstance(v, dict):
raise DataFieldException(
field, "Value `%s` should be type of dictionary" % v
)
result.append(data_type(**v))
except DataFieldException as e:
self._exceptions.add(e)
if isinstance(self, StrictDataModel):
raise e
return result
def _validate_dict_of_type(self, field, _, value):
data_type = field.type.type
assert issubclass(data_type, DataModel)
# check if DictOfType is not required
value = field.validate(value)
if not value:
return None
if not isinstance(value, dict):
raise DataFieldException(
field, "Value `%s` should be type of dictionary" % value
)
result = {}
for k, v in value.items():
try:
if not isinstance(v, dict):
raise DataFieldException(
field, "Value `%s` should be type of dictionary" % v
)
result[k] = data_type(**v)
except DataFieldException as e:
self._exceptions.add(e)
if isinstance(self, StrictDataModel):
raise e
return result
def __eq__(self, other): def __eq__(self, other):
assert isinstance(other, DataModel) assert isinstance(other, DataModel)
if self.get_field_names() != other.get_field_names(): if self.get_field_names() != other.get_field_names():
@@ -195,7 +260,19 @@ class DataModel(object):
return self._field_names return self._field_names
def get_exceptions(self): def get_exceptions(self):
return self._exceptions result = list(self._exceptions)
for name in self._field_names:
value = getattr(self, name)
if isinstance(value, DataModel):
result.extend(value.get_exceptions())
continue
if not isinstance(value, (dict, list)):
continue
for v in value.values() if isinstance(value, dict) else value:
if not isinstance(v, DataModel):
continue
result.extend(v.get_exceptions())
return result
def as_dict(self): def as_dict(self):
result = {} result = {}

View File

@@ -30,6 +30,10 @@ class AuthorModel(DataModel):
url = DataField(max_length=255) url = DataField(max_length=255)
class StrictAuthorModel(AuthorModel, StrictDataModel):
pass
class RepositoryModel(DataModel): class RepositoryModel(DataModel):
type = DataField(max_length=3, required=True) type = DataField(max_length=3, required=True)
url = DataField(max_length=255, required=True) url = DataField(max_length=255, required=True)
@@ -44,7 +48,7 @@ class ExportModel(DataModel):
class ExampleModel(DataModel): class ExampleModel(DataModel):
name = DataField(max_length=100, regex=r"^[a-zA-Z\d\-\_/]+$", required=True) name = DataField(max_length=100, regex=r"^[a-zA-Z\d\-\_/]+$", required=True)
base = DataField(required=True) base = DataField(required=True)
files = DataField(type=ListOfType(DataField())) files = DataField(type=ListOfType(DataField()), required=True)
class ManifestModel(DataModel): class ManifestModel(DataModel):
@@ -84,4 +88,4 @@ class ManifestModel(DataModel):
class StrictManifestModel(ManifestModel, StrictDataModel): class StrictManifestModel(ManifestModel, StrictDataModel):
pass authors = DataField(type=ListOfType(StrictAuthorModel), required=True)

View File

@@ -14,9 +14,8 @@
import pytest import pytest
from platformio.datamodel import DataFieldException from platformio import datamodel
from platformio.package.manifest import parser from platformio.package.manifest import model, parser
from platformio.package.manifest.model import ManifestModel, StrictManifestModel
def test_library_json_parser(): def test_library_json_parser():
@@ -139,31 +138,39 @@ sentence=This is Arduino library
) )
# Platforms ALL # Platforms ALL
mp = parser.LibraryPropertiesManifestParser("architectures=*\n" + contents) data = parser.LibraryPropertiesManifestParser(
assert mp.as_dict()["platforms"] == ["*"] "architectures=*\n" + contents
).as_dict()
assert data["platforms"] == ["*"]
# Platforms specific # Platforms specific
mp = parser.LibraryPropertiesManifestParser("architectures=avr, esp32\n" + contents) data = parser.LibraryPropertiesManifestParser(
assert mp.as_dict()["platforms"] == ["atmelavr", "espressif32"] "architectures=avr, esp32\n" + contents
).as_dict()
assert data["platforms"] == ["atmelavr", "espressif32"]
# Remote URL # Remote URL
mp = parser.LibraryPropertiesManifestParser( data = parser.LibraryPropertiesManifestParser(
contents, contents,
remote_url=( remote_url=(
"https://raw.githubusercontent.com/username/reponame/master/" "https://raw.githubusercontent.com/username/reponame/master/"
"libraries/TestPackage/library.properties" "libraries/TestPackage/library.properties"
), ),
) ).as_dict()
assert mp.as_dict()["export"] == { assert data["export"] == {
"exclude": ["extras", "docs", "tests", "test", "*.doxyfile", "*.pdf"], "exclude": ["extras", "docs", "tests", "test", "*.doxyfile", "*.pdf"],
"include": "libraries/TestPackage", "include": "libraries/TestPackage",
} }
assert data["repository"] == {
"url": "https://github.com/username/reponame",
"type": "git",
}
# Hope page # Hope page
mp = parser.LibraryPropertiesManifestParser( data = parser.LibraryPropertiesManifestParser(
"url=https://github.com/username/reponame.git\n" + contents "url=https://github.com/username/reponame.git\n" + contents
) ).as_dict()
assert mp.as_dict()["homepage"] is None assert data["homepage"] is None
assert mp.as_dict()["repository"] == { assert data["repository"] == {
"type": "git", "type": "git",
"url": "https://github.com/username/reponame.git", "url": "https://github.com/username/reponame.git",
} }
@@ -208,14 +215,14 @@ def test_library_json_model():
] ]
} }
""" """
mp = parser.ManifestParserFactory.new( data = parser.ManifestParserFactory.new(
contents, parser.ManifestFileType.LIBRARY_JSON contents, parser.ManifestFileType.LIBRARY_JSON
) ).as_dict()
model = StrictManifestModel(**mp.as_dict()) m = model.StrictManifestModel(**data)
assert model.repository.url == "https://github.com/bblanchon/ArduinoJson.git" assert m.repository.url == "https://github.com/bblanchon/ArduinoJson.git"
assert model.examples[1].base == "examples/JsonHttpClient" assert m.examples[1].base == "examples/JsonHttpClient"
assert model.examples[1].files == ["JsonHttpClient.ino"] assert m.examples[1].files == ["JsonHttpClient.ino"]
assert model == StrictManifestModel( assert m == model.StrictManifestModel(
**{ **{
"name": "ArduinoJson", "name": "ArduinoJson",
"keywords": ["json", "rest", "http", "web"], "keywords": ["json", "rest", "http", "web"],
@@ -270,12 +277,12 @@ category=Display
url=https://github.com/olikraus/u8glib url=https://github.com/olikraus/u8glib
architectures=avr,sam architectures=avr,sam
""" """
mp = parser.ManifestParserFactory.new( data = parser.ManifestParserFactory.new(
contents, parser.ManifestFileType.LIBRARY_PROPERTIES contents, parser.ManifestFileType.LIBRARY_PROPERTIES
) ).as_dict()
model = StrictManifestModel(**mp.as_dict()) m = model.StrictManifestModel(**data)
assert not model.get_exceptions() assert not m.get_exceptions()
assert model == StrictManifestModel( assert m == model.StrictManifestModel(
**{ **{
"license": None, "license": None,
"description": ( "description": (
@@ -359,14 +366,13 @@ def test_platform_json_model():
} }
} }
""" """
mp = parser.ManifestParserFactory.new( data = parser.ManifestParserFactory.new(
contents, parser.ManifestFileType.PLATFORM_JSON contents, parser.ManifestFileType.PLATFORM_JSON
) ).as_dict()
data = mp.as_dict()
data["frameworks"] = sorted(data["frameworks"]) data["frameworks"] = sorted(data["frameworks"])
model = ManifestModel(**mp.as_dict()) m = model.ManifestModel(**data)
assert model.frameworks == ["arduino", "simba"] assert m.frameworks == ["arduino", "simba"]
assert model == ManifestModel( assert m == model.ManifestModel(
**{ **{
"name": "atmelavr", "name": "atmelavr",
"title": "Atmel AVR", "title": "Atmel AVR",
@@ -399,13 +405,13 @@ def test_package_json_model():
"version": "3.30101.0" "version": "3.30101.0"
} }
""" """
mp = parser.ManifestParserFactory.new( data = parser.ManifestParserFactory.new(
contents, parser.ManifestFileType.PACKAGE_JSON contents, parser.ManifestFileType.PACKAGE_JSON
) ).as_dict()
model = ManifestModel(**mp.as_dict()) m = model.ManifestModel(**data)
assert model.system is None assert m.system is None
assert model.homepage == "http://www.scons.org" assert m.homepage == "http://www.scons.org"
assert model == ManifestModel( assert m == model.ManifestModel(
**{ **{
"name": "tool-scons", "name": "tool-scons",
"description": "SCons software construction tool", "description": "SCons software construction tool",
@@ -491,9 +497,9 @@ def test_examples_from_dir(tmpdir_factory):
return sorted(items, key=lambda item: item["name"]) return sorted(items, key=lambda item: item["name"])
data["examples"] = _sort_examples(data["examples"]) data["examples"] = _sort_examples(data["examples"])
model = ManifestModel(**data) m = model.ManifestModel(**data)
assert model.examples[3].name == "PlatformIO/hello" assert m.examples[3].name == "PlatformIO/hello"
assert model == ManifestModel( assert m == model.ManifestModel(
**{ **{
"version": "1.0.0", "version": "1.0.0",
"name": "pkg", "name": "pkg",
@@ -541,26 +547,64 @@ def test_examples_from_dir(tmpdir_factory):
) )
def test_dict_of_type():
class TestModel(datamodel.DataModel):
examples = datamodel.DataField(type=datamodel.DictOfType(model.ExampleModel))
class StrictTestModel(TestModel, datamodel.StrictDataModel):
pass
# valid
m = TestModel(
examples={
"valid": dict(name="Valid", base="valid", files=["valid.h"]),
"invalid": "test",
}
)
assert list(m.examples.keys()) == ["valid"]
# invalid
with pytest.raises(datamodel.DataFieldException):
StrictTestModel(examples=[dict(name="Valid", base="valid", files=["valid.h"])])
with pytest.raises(datamodel.DataFieldException):
StrictTestModel(
examples={
"valid": dict(name="Valid", base="valid", files=["valid.h"]),
"invalid": "test",
}
)
def test_broken_models(): def test_broken_models():
# non-strict mode # non-strict mode
assert len(ManifestModel(name="MyPackage").get_exceptions()) == 4 assert len(model.ManifestModel(name="MyPackage").get_exceptions()) == 4
assert ManifestModel(name="MyPackage", version="broken_version").version is None assert (
model.ManifestModel(name="MyPackage", version="broken_version").version is None
)
# invalid keywords
m = model.ManifestModel(keywords=["kw1", "*^[]"])
assert any(
"Value `*^[]` does not match RegExp" in str(e) for e in m.get_exceptions()
)
assert m.keywords == ["kw1"]
# strict mode # strict mode
with pytest.raises(DataFieldException) as excinfo: with pytest.raises(datamodel.DataFieldException) as excinfo:
assert StrictManifestModel(name="MyPackage") assert model.StrictManifestModel(name="MyPackage")
assert excinfo.match(r"Missed value for `StrictManifestModel.[a-z]+` field") assert excinfo.match(r"Missed value for `StrictManifestModel.[a-z]+` field")
# broken SemVer # broken SemVer
with pytest.raises( with pytest.raises(
DataFieldException, datamodel.DataFieldException,
match=( match=(
"Invalid semantic versioning format for " "Invalid semantic versioning format for "
"`StrictManifestModel.version` field" "`StrictManifestModel.version` field"
), ),
): ):
assert StrictManifestModel( assert model.StrictManifestModel(
name="MyPackage", name="MyPackage",
description="MyDescription", description="MyDescription",
keywords=["a", "b"], keywords=["a", "b"],
@@ -569,8 +613,11 @@ def test_broken_models():
) )
# broken value for DataModel # broken value for DataModel
with pytest.raises(DataFieldException, match="Value should be type of dict"): with pytest.raises(
assert StrictManifestModel( datamodel.DataFieldException,
match=("Value `should be dict here` should be type of dictionary"),
):
assert model.StrictManifestModel(
name="MyPackage", name="MyPackage",
description="MyDescription", description="MyDescription",
keywords=["a", "b"], keywords=["a", "b"],