def test_validate_json(): validator = Object(properties=Integer()) text = '{\n "a": "123",\n "b": "456"}' value = validate_json(text, validator=validator) assert value == {"a": 123, "b": 456} validator = Object(properties=Integer()) text = '{\n "a": "123",\n "b": "abc"}' with pytest.raises(ValidationError) as exc_info: validate_json(text, validator=validator) exc = exc_info.value assert exc.messages() == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31), ) ] assert ( repr(exc.messages()[0]) == "Message(text='Must be a number.', code='type', index=['b'], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31))" ) class Validator(Schema): a = Integer() b = Integer() text = '{\n "a": "123",\n "b": "abc"}' with pytest.raises(ValidationError) as exc_info: validate_json(text, validator=Validator) exc = exc_info.value assert exc.messages() == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31), ) ] text = '{"a": "123"}' with pytest.raises(ValidationError) as exc_info: validate_json(text, validator=Validator) exc = exc_info.value assert exc.messages() == [ Message( text="The field 'b' is required.", code="required", index=["b"], start_position=Position(line_no=1, column_no=1, char_index=0), end_position=Position(line_no=1, column_no=12, char_index=11), ) ]
def test_validate_yaml(): validator = Object(properties=Integer()) text = "a: 123\nb: 456\n" value = validate_yaml(text, validator=validator) assert value == {"a": 123, "b": 456} validator = Object(properties=Integer()) text = "a: 123\nb: abc\n" with pytest.raises(ValidationError) as exc_info: validate_yaml(text, validator=validator) exc = exc_info.value assert exc.messages() == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=2, column_no=4, char_index=10), end_position=Position(line_no=2, column_no=6, char_index=12), ) ] class Validator(Schema): a = Integer() b = Integer() text = "a: 123\nb: abc\n" with pytest.raises(ValidationError) as exc_info: validate_yaml(text, validator=Validator) exc = exc_info.value assert exc.messages() == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=2, column_no=4, char_index=10), end_position=Position(line_no=2, column_no=6, char_index=12), ) ] text = "a: 123" with pytest.raises(ValidationError) as exc_info: validate_yaml(text, validator=Validator) exc = exc_info.value assert exc.messages() == [ Message( text="The field 'b' is required.", code="required", index=["b"], start_position=Position(line_no=1, column_no=1, char_index=0), end_position=Position(line_no=1, column_no=6, char_index=5), ) ]
def test_error_messages_interface(): """ `errors.messages()` should return a list of Message instances. """ validator = Integer() value, error = validator.validate_or_error("abc") assert error.messages() == [Message(text="Must be a number.", code="type")]
def validate(self, value: typing.Any, *, strict: bool = False) -> typing.Any: if value is None and self.allow_null: return None elif value is None: raise self.validation_error("null") elif not isinstance(value, list): raise self.validation_error("type") if (self.min_items is not None and self.min_items == self.max_items and len(value) != self.min_items): raise self.validation_error("exact_items") if self.min_items is not None and len(value) < self.min_items: if self.min_items == 1: raise self.validation_error("empty") raise self.validation_error("min_items") elif self.max_items is not None and len(value) > self.max_items: raise self.validation_error("max_items") # Ensure all items are of the right type. validated = [] error_messages = [] # type: typing.List[Message] if self.unique_items: seen_items = set() # type: typing.Set[typing.Any] for pos, item in enumerate(value): validator = None if isinstance(self.items, list): if pos < len(self.items): validator = self.items[pos] elif isinstance(self.additional_items, Field): validator = self.additional_items elif self.items is not None: validator = self.items if validator is None: validated.append(item) else: item, error = validator.validate_or_error(item, strict=strict) if error: error_messages += error.messages(add_prefix=pos) else: validated.append(item) if self.unique_items: if item in seen_items: text = self.get_error_text("unique_items") message = Message(text=text, code="unique_items", index=[pos]) error_messages.append(message) else: seen_items.add(item) if error_messages: raise ValidationError(error_messages) return validated
def validate_with_positions( *, token: Token, validator: typing.Union[Field, typing.Type[Schema]]) -> typing.Any: try: return validator.validate(token.value) except ValidationError as error: messages = [] for message in error.messages(): if message.code == "required": field = message.index[-1] token = token.lookup(message.index[:-1]) text = f"The field {field!r} is required." else: token = token.lookup(message.index) text = message.text positional_message = Message( text=text, code=message.code, index=message.index, start_position=token.start, end_position=token.end, ) messages.append(positional_message) messages = sorted( messages, key=lambda m: m.start_position.char_index # type: ignore ) raise ValidationError(messages=messages)
def validate(self, value: typing.Any, *, strict: bool = False) -> typing.Any: if value is None and self.allow_null: return None elif value is None: raise self.validation_error("null") elif not isinstance(value, (dict, typing.Mapping)): raise self.validation_error("type") validated = {} error_messages = [] # Ensure all property keys are strings. for key in value.keys(): if not isinstance(key, str): text = self.get_error_text("invalid_key") message = Message(text=text, code="invalid_key", index=[key]) error_messages.append(message) elif self.property_names is not None: _, error = self.property_names.validate_or_error(key) if error is not None: text = self.get_error_text("invalid_property") message = Message(text=text, code="invalid_property", index=[key]) error_messages.append(message) # Min/Max properties if self.min_properties is not None: if len(value) < self.min_properties: if self.min_properties == 1: raise self.validation_error("empty") else: raise self.validation_error("min_properties") if self.max_properties is not None: if len(value) > self.max_properties: raise self.validation_error("max_properties") # Required properties for key in self.required: if key not in value: text = self.get_error_text("required") message = Message(text=text, code="required", index=[key]) error_messages.append(message) # Properties for key, child_schema in self.properties.items(): if key not in value: if child_schema.has_default(): validated[key] = child_schema.get_default_value() continue item = value[key] child_value, error = child_schema.validate_or_error(item, strict=strict) if not error: validated[key] = child_value else: error_messages += error.messages(add_prefix=key) # Pattern properties if self.pattern_properties: for key in list(value.keys()): for pattern, child_schema in self.pattern_properties.items(): if isinstance(key, str) and re.search(pattern, key): item = value[key] child_value, error = child_schema.validate_or_error( item, strict=strict) if not error: validated[key] = child_value else: error_messages += error.messages(add_prefix=key) # Additional properties validated_keys = set(validated.keys()) error_keys = set( [message.index[0] for message in error_messages if message.index]) remaining = [ key for key in value.keys() if key not in validated_keys | error_keys ] if self.additional_properties is True: for key in remaining: validated[key] = value[key] elif self.additional_properties is False: for key in remaining: text = self.get_error_text("invalid_property") message = Message(text=text, code="invalid_property", key=key) error_messages.append(message) elif self.additional_properties is not None: assert isinstance(self.additional_properties, Field) child_schema = self.additional_properties for key in remaining: item = value[key] child_value, error = child_schema.validate_or_error( item, strict=strict) if not error: validated[key] = child_value else: error_messages += error.messages(add_prefix=key) if error_messages: raise ValidationError(messages=error_messages) return validated
def test_validate_json(): value, messages = validate_json("") assert value is None assert messages == [ Message( text="No content.", code="no_content", position=Position(line_no=1, column_no=1, char_index=0), ) ] assert ( repr(messages[0]) == "Message(text='No content.', code='no_content', position=Position(line_no=1, column_no=1, char_index=0))" ) value, messages = validate_json('{"a": 123}') assert value == {"a": 123} assert messages == [] value, messages = validate_json(b'{"a": 123}') assert value == {"a": 123} assert messages == [] value, messages = validate_json('{"a" 123}') assert value is None assert messages == [ Message( text="Expecting ':' delimiter.", code="parse_error", position=Position(line_no=1, column_no=6, char_index=5), ) ] validator = Object(properties=Integer()) text = '{\n "a": "123",\n "b": "abc"}' value, messages = validate_json(text, validator=validator) assert value is None assert messages == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31), ) ] assert ( repr(messages[0]) == "Message(text='Must be a number.', code='type', index=['b'], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31))" ) validator = Object(properties=Integer()) text = '{\n "a": "123",\n "b": "456"}' value, messages = validate_json(text, validator=validator) assert value == {"a": 123, "b": 456} assert messages == [] class Validator(Schema): a = Integer() b = Integer() text = '{\n "a": "123",\n "b": "abc"}' value, messages = validate_json(text, validator=Validator) assert value is None assert messages == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=3, column_no=10, char_index=27), end_position=Position(line_no=3, column_no=14, char_index=31), ) ] text = '{"a": "123"}' value, messages = validate_json(text, validator=Validator) assert value is None assert messages == [ Message( text="The field 'b' is required.", code="required", index=["b"], start_position=Position(line_no=1, column_no=1, char_index=0), end_position=Position(line_no=1, column_no=12, char_index=11), ) ]
def test_validate_yaml(): value, messages = validate_yaml("") assert value is None assert messages == [ Message( text="No content.", code="no_content", position=Position(line_no=1, column_no=1, char_index=0), ) ] value, messages = validate_yaml("a: 123") assert value == {"a": 123} assert messages == [] value, messages = validate_yaml(b"a: 123") assert value == {"a": 123} assert messages == [] value, messages = validate_yaml('{"a" 1}') assert value is None assert messages == [ Message( text="expected ',' or '}', but got '<scalar>'.", code="parse_error", position=Position(line_no=1, column_no=6, char_index=5), ) ] validator = Object(properties=Integer()) text = "a: 123\nb: abc\n" value, messages = validate_yaml(text, validator=validator) assert value is None assert messages == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=2, column_no=4, char_index=10), end_position=Position(line_no=2, column_no=6, char_index=12), ) ] validator = Object(properties=Integer()) text = "a: 123\nb: 456\n" value, messages = validate_yaml(text, validator=validator) assert value == {"a": 123, "b": 456} assert messages == [] class Validator(Schema): a = Integer() b = Integer() text = "a: 123\nb: abc\n" value, messages = validate_yaml(text, validator=Validator) assert value is None assert messages == [ Message( text="Must be a number.", code="type", index=["b"], start_position=Position(line_no=2, column_no=4, char_index=10), end_position=Position(line_no=2, column_no=6, char_index=12), ) ] text = "a: 123" value, messages = validate_yaml(text, validator=Validator) assert value is None assert messages == [ Message( text="The field 'b' is required.", code="required", index=["b"], start_position=Position(line_no=1, column_no=1, char_index=0), end_position=Position(line_no=1, column_no=6, char_index=5), ) ]
def validate_yaml( content: typing.Union[str, bytes], validator: typing.Union[Field, typing.Type[Schema]] = None, ) -> typing.Tuple[typing.Any, typing.List[Message]]: """ Parse and validate a YAML string, returning positionally marked error messages on parse or validation failures. content - A YAML string or bytestring. validator - A Field instance or Schema class to validate against. Returns a two-tuple of (value, error_messages) """ assert yaml is not None, "'pyyaml' must be installed." if isinstance(content, bytes): content = content.decode("utf-8", "ignore") if not content.strip(): # Handle the empty string case explicitly for clear error messaging. position = Position(column_no=1, line_no=1, char_index=0) message = Message( text="No content.", code="no_content", index=None, position=position ) return (None, [message]) try: root_token = tokenize_yaml(content) except (yaml.scanner.ScannerError, yaml.parser.ParserError) as exc: # type: ignore # Handle cases that result in a YAML parse error. position = _get_position(content, index=exc.problem_mark.index) message = Message( text=exc.problem + ".", code="parse_error", index=None, position=position ) return (None, [message]) if validator is None: return (root_token.value, []) value, error = validator.validate_or_error(root_token.value) if error: messages = [] for message in error.messages(): if message.code == "required": token = root_token.lookup(message.index[:-1]) text = "The field '%s' is required." % message.index[-1] else: text = message.text token = root_token.lookup(message.index) positional_message = Message( text=text, code=message.code, index=message.index, start_position=token.start, end_position=token.end, ) messages.append(positional_message) messages = sorted( messages, key=lambda m: m.start_position.char_index # type: ignore ) return (None, messages) return (value, [])