diff --git a/schema/components.json b/schema/components.json index 0888a4d743..8cc5f979f2 100644 --- a/schema/components.json +++ b/schema/components.json @@ -605,7 +605,7 @@ "value": { "type": "number", "minimum": 0, - "multipleOfPrecision": 0.01 + "multipleOf": 0.01 }, "unit": { "type": "string", diff --git a/tests/definitions_test.py b/tests/definitions_test.py index 318704f935..1138e9b9ab 100644 --- a/tests/definitions_test.py +++ b/tests/definitions_test.py @@ -1,11 +1,14 @@ +import decimal import glob import json import os +from urllib.request import urlopen + import pytest import yaml -from jsonschema import RefResolver, Draft4Validator +from jsonschema import Draft4Validator, RefResolver from jsonschema.exceptions import ValidationError - +from yaml_loader import DecimalSafeLoader SCHEMAS = ( ('device-types', 'devicetype.json'), @@ -35,7 +38,7 @@ def _get_definition_files(): # Initialize the schema with open(f"schema/{schema}") as schema_file: - schema = json.loads(schema_file.read()) + schema = json.loads(schema_file.read(), parse_float=decimal.Decimal) # Validate that the schema exists assert schema, f"Schema definition for {path} is empty!" @@ -51,6 +54,12 @@ def _get_definition_files(): known_slugs = set() +def _decimal_file_handler(uri): + with urlopen(uri) as url: + result = json.loads(url.read().decode("utf-8"), parse_float=decimal.Decimal) + return result + + def test_environment(): """ Run basic sanity checks on the environment to ensure tests are running correctly. @@ -75,11 +84,15 @@ def test_definitions(file_path, schema): assert content.endswith('\n'), "Missing trailing newline" # Load YAML data from file - definition = yaml.load(content, Loader=yaml.SafeLoader) + definition = yaml.load(content, Loader=DecimalSafeLoader) # Validate YAML definition against the supplied schema try: - resolver = RefResolver(f'file://{os.getcwd()}/schema/devicetype.json', schema) + resolver = RefResolver( + f"file://{os.getcwd()}/schema/devicetype.json", + schema, + handlers={"file": _decimal_file_handler}, + ) Draft4Validator(schema, resolver=resolver).validate(definition) except ValidationError as e: pytest.fail(f"{file_path} failed validation: {e}", False) diff --git a/tests/yaml_loader.py b/tests/yaml_loader.py new file mode 100644 index 0000000000..dac9d522d4 --- /dev/null +++ b/tests/yaml_loader.py @@ -0,0 +1,34 @@ +import decimal + +from yaml.composer import Composer +from yaml.constructor import SafeConstructor +from yaml.parser import Parser +from yaml.reader import Reader +from yaml.resolver import Resolver +from yaml.scanner import Scanner + + +class DecimalSafeConstructor(SafeConstructor): + """Special constructor to override construct_yaml_float() in order to cast "Decimal" types to the value""" + + def construct_yaml_float(self, node): + value = super().construct_yaml_float(node) + # We force the string representation of the float here to avoid things like: + # In [11]: decimal.Decimal(10.11) + # Out[11]: Decimal('10.1099999999999994315658113919198513031005859375') + return decimal.Decimal(f"{value}") + + +DecimalSafeConstructor.add_constructor( + "tag:yaml.org,2002:float", DecimalSafeConstructor.construct_yaml_float +) + + +class DecimalSafeLoader(Reader, Scanner, Parser, Composer, DecimalSafeConstructor, Resolver): + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + DecimalSafeConstructor.__init__(self) + Resolver.__init__(self)