mirror of https://github.com/streamlink/streamlink
plugin.api.validate: implement ValidationError
- Implement `ValidationError` - Inherit from `ValueError` to preserve backwards compatiblity - Allow collecting multiple errors (AnySchema) - Keep an error stack of parent `ValidationError`s or other exceptions - Format error stack when converting error to string - Raise `ValidationError` instead of `ValueError` - Add error contexts where it makes sense - Add schema names to error instances - Add and update tests
This commit is contained in:
parent
120c103023
commit
3d44da082b
|
@ -1,3 +1,4 @@
|
|||
from streamlink.plugin.api.validate._exception import ValidationError # noqa: F401
|
||||
# noinspection PyPep8Naming,PyShadowingBuiltins
|
||||
from streamlink.plugin.api.validate._schemas import ( # noqa: I101, F401
|
||||
SchemaContainer,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
from textwrap import indent
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
def __init__(self, *errors, schema=None, context: "ValidationError" = None):
|
||||
self.schema = schema
|
||||
self.errors = errors
|
||||
self.context = context
|
||||
|
||||
def _get_schema_name(self) -> str:
|
||||
if not self.schema:
|
||||
return ""
|
||||
if type(self.schema) is str:
|
||||
return f"({self.schema})"
|
||||
return f"({self.schema.__name__})"
|
||||
|
||||
def __str__(self):
|
||||
cls = self.__class__
|
||||
ret = []
|
||||
seen = set()
|
||||
|
||||
def append(indentation, error):
|
||||
if error:
|
||||
ret.append(indent(f"{error}", indentation))
|
||||
|
||||
def add(level, error):
|
||||
indentation = " " * level
|
||||
|
||||
if error in seen:
|
||||
append(indentation, "...")
|
||||
return
|
||||
seen.add(error)
|
||||
|
||||
for err in error.errors:
|
||||
if not isinstance(err, cls):
|
||||
append(indentation, f"{err}")
|
||||
else:
|
||||
append(indentation, f"{err.__class__.__name__}{err._get_schema_name()}:")
|
||||
add(level + 1, err)
|
||||
|
||||
context = error.context
|
||||
if context:
|
||||
if not isinstance(context, cls):
|
||||
append(indentation, "Context:")
|
||||
append(f"{indentation} ", context)
|
||||
else:
|
||||
append(indentation, f"Context{context._get_schema_name()}:")
|
||||
add(level + 1, context)
|
||||
|
||||
append("", f"{cls.__name__}{self._get_schema_name()}:")
|
||||
add(1, self)
|
||||
|
||||
return "\n".join(ret)
|
|
@ -6,6 +6,7 @@ from re import Match
|
|||
from lxml.etree import Element, iselement
|
||||
|
||||
from streamlink.exceptions import PluginError
|
||||
from streamlink.plugin.api.validate._exception import ValidationError
|
||||
from streamlink.plugin.api.validate._schemas import (
|
||||
AllSchema,
|
||||
AnySchema,
|
||||
|
@ -27,7 +28,7 @@ class Schema(AllSchema):
|
|||
def validate(self, value, name="result", exception=PluginError):
|
||||
try:
|
||||
return validate(self, value)
|
||||
except ValueError as err:
|
||||
except ValidationError as err:
|
||||
raise exception(f"Unable to validate {name}: {err}")
|
||||
|
||||
|
||||
|
@ -37,7 +38,7 @@ class Schema(AllSchema):
|
|||
@singledispatch
|
||||
def validate(schema, value):
|
||||
if schema != value:
|
||||
raise ValueError(f"{value!r} does not equal {schema!r}")
|
||||
raise ValidationError(f"{value!r} does not equal {schema!r}", schema="equality")
|
||||
|
||||
return value
|
||||
|
||||
|
@ -45,7 +46,7 @@ def validate(schema, value):
|
|||
@validate.register(type)
|
||||
def _validate_type(schema, value):
|
||||
if not isinstance(value, schema):
|
||||
raise ValueError(f"Type of {value!r} should be '{schema.__name__}', but is '{type(value).__name__}'")
|
||||
raise ValidationError(f"Type of {value!r} should be '{schema.__name__}', but is '{type(value).__name__}'", schema=type)
|
||||
|
||||
return value
|
||||
|
||||
|
@ -77,16 +78,24 @@ def _validate_dict(schema, value):
|
|||
|
||||
if type(key) in (type, AllSchema, AnySchema, TransformSchema, UnionSchema):
|
||||
for subkey, subvalue in value.items():
|
||||
new[validate(key, subkey)] = validate(subschema, subvalue)
|
||||
try:
|
||||
newkey = validate(key, subkey)
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate key", schema=dict, context=err)
|
||||
try:
|
||||
newvalue = validate(subschema, subvalue)
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate value", schema=dict, context=err)
|
||||
new[newkey] = newvalue
|
||||
break
|
||||
else:
|
||||
if key not in value:
|
||||
raise ValueError(f"Key '{key}' not found in {value!r}")
|
||||
|
||||
try:
|
||||
new[key] = validate(subschema, value[key])
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate key '{key}': {err}")
|
||||
if key not in value:
|
||||
raise ValidationError(f"Key '{key}' not found in {value!r}", schema=dict)
|
||||
|
||||
try:
|
||||
new[key] = validate(subschema, value[key])
|
||||
except ValidationError as err:
|
||||
raise ValidationError(f"Unable to validate value of key '{key}'", schema=dict, context=err)
|
||||
|
||||
return new
|
||||
|
||||
|
@ -94,7 +103,7 @@ def _validate_dict(schema, value):
|
|||
@validate.register(abc.Callable)
|
||||
def _validate_callable(schema: abc.Callable, value):
|
||||
if not schema(value):
|
||||
raise ValueError(f"{schema.__name__}({value!r}) is not true")
|
||||
raise ValidationError(f"{schema.__name__}({value!r}) is not true", schema=abc.Callable)
|
||||
|
||||
return value
|
||||
|
||||
|
@ -113,11 +122,10 @@ def _validate_anyschema(schema: AnySchema, value):
|
|||
for subschema in schema.schema:
|
||||
try:
|
||||
return validate(subschema, value)
|
||||
except ValueError as err:
|
||||
except ValidationError as err:
|
||||
errors.append(err)
|
||||
else:
|
||||
err = " or ".join(map(str, errors))
|
||||
raise ValueError(err)
|
||||
|
||||
raise ValidationError(*errors, schema=AnySchema)
|
||||
|
||||
|
||||
@validate.register(TransformSchema)
|
||||
|
@ -144,21 +152,25 @@ def _validate_getitemschema(schema: GetItemSchema, value):
|
|||
except (KeyError, IndexError):
|
||||
# only return default value on last item in nested lookup
|
||||
if idx < len(item) - 1:
|
||||
raise ValueError(f"Item \"{key}\" was not found in object \"{value}\"")
|
||||
raise ValidationError(f"Item \"{key}\" was not found in object \"{value}\"", schema=GetItemSchema)
|
||||
return schema.default
|
||||
except (TypeError, AttributeError) as err:
|
||||
raise ValueError(err)
|
||||
raise ValidationError(f"Could not get key \"{key}\" from object \"{value}\"", schema=GetItemSchema, context=err)
|
||||
|
||||
|
||||
@validate.register(AttrSchema)
|
||||
def _validate_attrschema(schema: AttrSchema, value):
|
||||
new = copy(value)
|
||||
|
||||
for key, schema in schema.schema.items():
|
||||
for key, subschema in schema.schema.items():
|
||||
if not hasattr(value, key):
|
||||
raise ValueError(f"Attribute \"{key}\" not found on object \"{value}\"")
|
||||
raise ValidationError(f"Attribute \"{key}\" not found on object \"{value}\"", schema=AttrSchema)
|
||||
|
||||
try:
|
||||
value = validate(subschema, getattr(value, key))
|
||||
except ValidationError as err:
|
||||
raise ValidationError(f"Could not validate attribute \"{key}\"", schema=AttrSchema, context=err)
|
||||
|
||||
value = validate(schema, getattr(value, key))
|
||||
setattr(new, key, value)
|
||||
|
||||
return new
|
||||
|
@ -175,26 +187,26 @@ def _validate_xmlelementschema(schema: XmlElementSchema, value):
|
|||
if schema.tag is not None:
|
||||
try:
|
||||
tag = validate(schema.tag, value.tag)
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate XML tag: {err}")
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate XML tag", schema=XmlElementSchema, context=err)
|
||||
|
||||
if schema.attrib is not None:
|
||||
try:
|
||||
attrib = validate(schema.attrib, dict(value.attrib))
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate XML attributes: {err}")
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate XML attributes", schema=XmlElementSchema, context=err)
|
||||
|
||||
if schema.text is not None:
|
||||
try:
|
||||
text = validate(schema.text, value.text)
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate XML text: {err}")
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate XML text", schema=XmlElementSchema, context=err)
|
||||
|
||||
if schema.tail is not None:
|
||||
try:
|
||||
tail = validate(schema.tail, value.tail)
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate XML tail: {err}")
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Unable to validate XML tail", schema=XmlElementSchema, context=err)
|
||||
|
||||
new = Element(tag, attrib)
|
||||
new.text = text
|
||||
|
@ -214,7 +226,10 @@ def _validate_uniongetschema(schema: UnionGetSchema, value):
|
|||
|
||||
@validate.register(UnionSchema)
|
||||
def _validate_unionschema(schema: UnionSchema, value):
|
||||
return validate_union(schema.schema, value)
|
||||
try:
|
||||
return validate_union(schema.schema, value)
|
||||
except ValidationError as err:
|
||||
raise ValidationError("Could not validate union", schema=UnionSchema, context=err)
|
||||
|
||||
|
||||
# ----
|
||||
|
@ -223,7 +238,7 @@ def _validate_unionschema(schema: UnionSchema, value):
|
|||
# noinspection PyUnusedLocal
|
||||
@singledispatch
|
||||
def validate_union(schema, value):
|
||||
raise ValueError(f"Invalid union type: {type(schema).__name__}")
|
||||
raise ValidationError(f"Invalid union type: {type(schema).__name__}")
|
||||
|
||||
|
||||
@validate_union.register(dict)
|
||||
|
@ -236,11 +251,11 @@ def _validate_union_dict(schema, value):
|
|||
|
||||
try:
|
||||
new[key] = validate(schema, value)
|
||||
except ValueError as err:
|
||||
except ValidationError as err:
|
||||
if is_optional:
|
||||
continue
|
||||
|
||||
raise ValueError(f"Unable to validate union '{key}': {err}")
|
||||
raise ValidationError(f"Unable to validate union \"{key}\"", schema=dict, context=err)
|
||||
|
||||
return new
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from urllib.parse import urlparse
|
|||
|
||||
from lxml.etree import iselement
|
||||
|
||||
from streamlink.plugin.api.validate._exception import ValidationError
|
||||
from streamlink.plugin.api.validate._schemas import AllSchema, AnySchema, TransformSchema
|
||||
from streamlink.plugin.api.validate._validate import validate
|
||||
from streamlink.utils.parse import (
|
||||
|
@ -22,7 +23,7 @@ def validator_length(number: int) -> Callable[[str], bool]:
|
|||
|
||||
def min_len(value):
|
||||
if not len(value) >= number:
|
||||
raise ValueError(f"Minimum length is {number} but value is {len(value)}")
|
||||
raise ValidationError(f"Minimum length is {number}, but value is {len(value)}", schema="length")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -37,7 +38,7 @@ def validator_startswith(string: str) -> Callable[[str], bool]:
|
|||
def starts_with(value):
|
||||
validate(str, value)
|
||||
if not value.startswith(string):
|
||||
raise ValueError(f"'{value}' does not start with '{string}'")
|
||||
raise ValidationError(f"'{value}' does not start with '{string}'", schema="startswith")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -52,7 +53,7 @@ def validator_endswith(string: str) -> Callable[[str], bool]:
|
|||
def ends_with(value):
|
||||
validate(str, value)
|
||||
if not value.endswith(string):
|
||||
raise ValueError(f"'{value}' does not end with '{string}'")
|
||||
raise ValidationError(f"'{value}' does not end with '{string}'", schema="endswith")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -67,7 +68,7 @@ def validator_contains(string: str) -> Callable[[str], bool]:
|
|||
def contains_str(value):
|
||||
validate(str, value)
|
||||
if string not in value:
|
||||
raise ValueError(f"'{value}' does not contain '{string}'")
|
||||
raise ValidationError(f"'{value}' does not contain '{string}'", schema="contains")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -87,16 +88,16 @@ def validator_url(**attributes) -> Callable[[str], bool]:
|
|||
validate(str, value)
|
||||
parsed = urlparse(value)
|
||||
if not parsed.netloc:
|
||||
raise ValueError(f"'{value}' is not a valid URL")
|
||||
raise ValidationError(f"'{value}' is not a valid URL", schema="url")
|
||||
|
||||
for name, schema in attributes.items():
|
||||
if not hasattr(parsed, name):
|
||||
raise ValueError(f"Invalid URL attribute '{name}'")
|
||||
raise ValidationError(f"Invalid URL attribute '{name}'", schema="url")
|
||||
|
||||
try:
|
||||
validate(schema, getattr(parsed, name))
|
||||
except ValueError as err:
|
||||
raise ValueError(f"Unable to validate URL attribute '{name}': {err}")
|
||||
except ValidationError as err:
|
||||
raise ValidationError(f"Unable to validate URL attribute '{name}'", schema="url", context=err)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -185,7 +186,7 @@ def validator_xml_find(xpath: str) -> TransformSchema:
|
|||
validate(iselement, value)
|
||||
value = value.find(xpath)
|
||||
if value is None:
|
||||
raise ValueError(f"XPath '{xpath}' did not return an element")
|
||||
raise ValidationError(f"XPath '{xpath}' did not return an element", schema="xml_find")
|
||||
|
||||
return validate(iselement, value)
|
||||
|
||||
|
@ -244,7 +245,7 @@ def validator_parse_json(*args, **kwargs) -> TransformSchema:
|
|||
Parse JSON data via the :func:`streamlink.utils.parse.parse_json` utility function.
|
||||
"""
|
||||
|
||||
return TransformSchema(_parse_json, *args, **kwargs, exception=ValueError, schema=None)
|
||||
return TransformSchema(_parse_json, *args, **kwargs, exception=ValidationError, schema=None)
|
||||
|
||||
|
||||
def validator_parse_html(*args, **kwargs) -> TransformSchema:
|
||||
|
@ -252,7 +253,7 @@ def validator_parse_html(*args, **kwargs) -> TransformSchema:
|
|||
Parse HTML data via the :func:`streamlink.utils.parse.parse_html` utility function.
|
||||
"""
|
||||
|
||||
return TransformSchema(_parse_html, *args, **kwargs, exception=ValueError, schema=None)
|
||||
return TransformSchema(_parse_html, *args, **kwargs, exception=ValidationError, schema=None)
|
||||
|
||||
|
||||
def validator_parse_xml(*args, **kwargs) -> TransformSchema:
|
||||
|
@ -260,7 +261,7 @@ def validator_parse_xml(*args, **kwargs) -> TransformSchema:
|
|||
Parse XML data via the :func:`streamlink.utils.parse.parse_xml` utility function.
|
||||
"""
|
||||
|
||||
return TransformSchema(_parse_xml, *args, **kwargs, exception=ValueError, schema=None)
|
||||
return TransformSchema(_parse_xml, *args, **kwargs, exception=ValidationError, schema=None)
|
||||
|
||||
|
||||
def validator_parse_qsd(*args, **kwargs) -> TransformSchema:
|
||||
|
@ -268,4 +269,4 @@ def validator_parse_qsd(*args, **kwargs) -> TransformSchema:
|
|||
Parse a query string via the :func:`streamlink.utils.parse.parse_qsd` utility function.
|
||||
"""
|
||||
|
||||
return TransformSchema(_parse_qsd, *args, **kwargs, exception=ValueError, schema=None)
|
||||
return TransformSchema(_parse_qsd, *args, **kwargs, exception=ValidationError, schema=None)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import re
|
||||
import unittest
|
||||
from textwrap import dedent
|
||||
|
||||
from lxml.etree import Element
|
||||
|
||||
from streamlink.plugin.api.validate import (
|
||||
Schema,
|
||||
ValidationError,
|
||||
all,
|
||||
any,
|
||||
attr,
|
||||
|
@ -37,6 +39,10 @@ from streamlink.plugin.api.validate import (
|
|||
)
|
||||
|
||||
|
||||
def assert_validationerror(exception, expected):
|
||||
assert str(exception) == dedent(expected).strip("\n")
|
||||
|
||||
|
||||
def test_text_is_str():
|
||||
assert text is str, "Exports text as str alias for backwards compatiblity"
|
||||
|
||||
|
@ -79,7 +85,10 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(B, a)
|
||||
assert str(cm.exception) == "Type of a should be 'B', but is 'A'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(type):
|
||||
Type of a should be 'B', but is 'A'
|
||||
""")
|
||||
|
||||
def test_callable(self):
|
||||
def check(n):
|
||||
|
@ -89,15 +98,22 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(check, 0)
|
||||
assert str(cm.exception) == "check(0) is not true"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(Callable):
|
||||
check(0) is not true
|
||||
""")
|
||||
|
||||
def test_all(self):
|
||||
assert validate(all(int, lambda n: 0 < n < 5), 3) == 3
|
||||
|
||||
assert validate(all(transform(int), lambda n: 0 < n < 5), 3.33) == 3
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(all(int, float), 123)
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(type):
|
||||
Type of 123 should be 'float', but is 'int'
|
||||
""")
|
||||
|
||||
def test_any(self):
|
||||
assert validate(any(int, dict), 5) == 5
|
||||
|
@ -105,8 +121,15 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
assert validate(any(int), 4) == 4
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(any(int, float), "123")
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(AnySchema):
|
||||
ValidationError(type):
|
||||
Type of '123' should be 'int', but is 'str'
|
||||
ValidationError(type):
|
||||
Type of '123' should be 'float', but is 'str'
|
||||
""")
|
||||
|
||||
def test_transform(self):
|
||||
assert validate(transform(int), "1") == 1
|
||||
|
@ -184,8 +207,14 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
assert validate(get(3, "default"), [0, 1, 2]) == "default"
|
||||
assert validate(get("attr"), Element("foo", {"attr": "value"})) == "value"
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "'NoneType' object is not subscriptable"):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(get("key"), None)
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(GetItemSchema):
|
||||
Could not get key "key" from object "None"
|
||||
Context:
|
||||
'NoneType' object is not subscriptable
|
||||
""")
|
||||
|
||||
data = {"one": {"two": {"three": "value1"}},
|
||||
("one", "two", "three"): "value2"}
|
||||
|
@ -194,10 +223,21 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
assert validate(get(("one", "two", "invalidkey")), data) is None, "Default value is None"
|
||||
assert validate(get(("one", "two", "invalidkey"), "default"), data) == "default", "Custom default value"
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Item \"invalidkey\" was not found in object \"{'two': {'three': 'value1'}}\""):
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(get(("one", "invalidkey", "three")), data)
|
||||
with self.assertRaisesRegex(ValueError, "'NoneType' object is not subscriptable"):
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(GetItemSchema):
|
||||
Item "invalidkey" was not found in object "{'two': {'three': 'value1'}}"
|
||||
""")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(all(get("one"), get("invalidkey"), get("three")), data)
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(GetItemSchema):
|
||||
Could not get key "three" from object "None"
|
||||
Context:
|
||||
'NoneType' object is not subscriptable
|
||||
""")
|
||||
|
||||
def test_get_re(self):
|
||||
m = re.match(r"(\d+)p", "720p")
|
||||
|
@ -225,7 +265,7 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
def test_xml_element(self):
|
||||
el = Element("tag")
|
||||
el.set("key", "value")
|
||||
el.text = "test"
|
||||
el.text = "text"
|
||||
childA = Element("childA")
|
||||
childB = Element("childB")
|
||||
el.append(childA)
|
||||
|
@ -236,7 +276,7 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
assert newelem is not el
|
||||
assert newelem.tag == "TAG"
|
||||
assert newelem.text == "TEST"
|
||||
assert newelem.text == "TEXT"
|
||||
assert newelem.attrib == {"KEY": "VALUE"}
|
||||
assert newelem[0].tag == "childA"
|
||||
assert newelem[1].tag == "childB"
|
||||
|
@ -245,15 +285,32 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(xml_element(tag="invalid"), el)
|
||||
assert str(cm.exception).startswith("Unable to validate XML tag: ")
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(XmlElementSchema):
|
||||
Unable to validate XML tag
|
||||
Context(equality):
|
||||
'tag' does not equal 'invalid'
|
||||
""")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(xml_element(text="invalid"), el)
|
||||
assert str(cm.exception).startswith("Unable to validate XML text: ")
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(XmlElementSchema):
|
||||
Unable to validate XML text
|
||||
Context(equality):
|
||||
'text' does not equal 'invalid'
|
||||
""")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(xml_element(attrib={"key": "invalid"}), el)
|
||||
assert str(cm.exception).startswith("Unable to validate XML attributes: ")
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(XmlElementSchema):
|
||||
Unable to validate XML attributes
|
||||
Context(dict):
|
||||
Unable to validate value of key 'key'
|
||||
Context(equality):
|
||||
'value' does not equal 'invalid'
|
||||
""")
|
||||
|
||||
def test_xml_find(self):
|
||||
el = Element("parent")
|
||||
|
@ -264,7 +321,10 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(xml_find("baz"), el)
|
||||
assert str(cm.exception) == "XPath 'baz' did not return an element"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(xml_find):
|
||||
XPath 'baz' did not return an element
|
||||
""")
|
||||
|
||||
def test_xml_findtext(self):
|
||||
el = Element("foo")
|
||||
|
@ -319,7 +379,10 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(attr({"foo": str}), {"bar": "baz"})
|
||||
assert str(cm.exception) == "Attribute \"foo\" not found on object \"{'bar': 'baz'}\""
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(AttrSchema):
|
||||
Attribute "foo" not found on object "{'bar': 'baz'}"
|
||||
""")
|
||||
|
||||
def test_url(self):
|
||||
url_ = "https://google.se/path"
|
||||
|
@ -330,57 +393,219 @@ class TestPluginAPIValidate(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(url(), "foo")
|
||||
assert str(cm.exception) == "'foo' is not a valid URL"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(url):
|
||||
'foo' is not a valid URL
|
||||
""")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(url(foo="bar"), "https://foo")
|
||||
assert str(cm.exception) == "Invalid URL attribute 'foo'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(url):
|
||||
Invalid URL attribute 'foo'
|
||||
""")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(url(path=endswith(".m3u8")), "https://foo/bar.mpd")
|
||||
assert str(cm.exception) == "Unable to validate URL attribute 'path': '/bar.mpd' does not end with '.m3u8'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(url):
|
||||
Unable to validate URL attribute 'path'
|
||||
Context(endswith):
|
||||
'/bar.mpd' does not end with '.m3u8'
|
||||
""")
|
||||
|
||||
def test_startswith(self):
|
||||
assert validate(startswith("abc"), "abcedf")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(startswith("bar"), "foo")
|
||||
assert str(cm.exception) == "'foo' does not start with 'bar'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(startswith):
|
||||
'foo' does not start with 'bar'
|
||||
""")
|
||||
|
||||
def test_endswith(self):
|
||||
assert validate(endswith("åäö"), "xyzåäö")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(endswith("bar"), "foo")
|
||||
assert str(cm.exception) == "'foo' does not end with 'bar'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(endswith):
|
||||
'foo' does not end with 'bar'
|
||||
""")
|
||||
|
||||
def test_contains(self):
|
||||
assert validate(contains("foo"), "foobar")
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(contains("bar"), "foo")
|
||||
assert str(cm.exception) == "'foo' does not contain 'bar'"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError(contains):
|
||||
'foo' does not contain 'bar'
|
||||
""")
|
||||
|
||||
def test_parse_json(self):
|
||||
assert validate(parse_json(), '{"a": ["b", true, false, null, 1, 2.3]}') == {"a": ["b", True, False, None, 1, 2.3]}
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(parse_json(), "invalid")
|
||||
assert str(cm.exception) == "Unable to parse JSON: Expecting value: line 1 column 1 (char 0) ('invalid')"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError:
|
||||
Unable to parse JSON: Expecting value: line 1 column 1 (char 0) ('invalid')
|
||||
""")
|
||||
|
||||
def test_parse_html(self):
|
||||
assert validate(parse_html(), '<!DOCTYPE html><body>"perfectly"<a>valid<div>HTML').tag == "html"
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(parse_html(), None)
|
||||
assert str(cm.exception) == "Unable to parse HTML: can only parse strings (None)"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError:
|
||||
Unable to parse HTML: can only parse strings (None)
|
||||
""")
|
||||
|
||||
def test_parse_xml(self):
|
||||
assert validate(parse_xml(), '<?xml version="1.0" encoding="utf-8"?><root></root>').tag == "root"
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(parse_xml(), None)
|
||||
assert str(cm.exception) == "Unable to parse XML: can only parse strings (None)"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError:
|
||||
Unable to parse XML: can only parse strings (None)
|
||||
""")
|
||||
|
||||
def test_parse_qsd(self):
|
||||
assert validate(parse_qsd(), 'foo=bar&foo=baz') == {"foo": "baz"}
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
validate(parse_qsd(), 123)
|
||||
assert str(cm.exception) == "Unable to parse query string: 'int' object has no attribute 'decode' (123)"
|
||||
assert_validationerror(cm.exception, """
|
||||
ValidationError:
|
||||
Unable to parse query string: 'int' object has no attribute 'decode' (123)
|
||||
""")
|
||||
|
||||
|
||||
class TestValidationError:
|
||||
def test_subclass(self):
|
||||
assert issubclass(ValidationError, ValueError)
|
||||
|
||||
def test_empty(self):
|
||||
assert str(ValidationError()) == "ValidationError:"
|
||||
assert str(ValidationError("")) == "ValidationError:"
|
||||
assert str(ValidationError(ValidationError())) == "ValidationError:\n ValidationError:"
|
||||
assert str(ValidationError(ValidationError(""))) == "ValidationError:\n ValidationError:"
|
||||
|
||||
def test_single(self):
|
||||
assert str(ValidationError("foo")) == "ValidationError:\n foo"
|
||||
assert str(ValidationError(ValueError("bar"))) == "ValidationError:\n bar"
|
||||
|
||||
def test_single_nested(self):
|
||||
err = ValidationError(ValidationError("baz"))
|
||||
assert_validationerror(err, """
|
||||
ValidationError:
|
||||
ValidationError:
|
||||
baz
|
||||
""")
|
||||
|
||||
def test_multiple_nested(self):
|
||||
err = ValidationError(
|
||||
"a",
|
||||
ValidationError("b", "c"),
|
||||
"d",
|
||||
ValidationError("e"),
|
||||
"f",
|
||||
)
|
||||
assert_validationerror(err, """
|
||||
ValidationError:
|
||||
a
|
||||
ValidationError:
|
||||
b
|
||||
c
|
||||
d
|
||||
ValidationError:
|
||||
e
|
||||
f
|
||||
""")
|
||||
|
||||
def test_context(self):
|
||||
err = ValidationError(
|
||||
"a",
|
||||
context=ValidationError(
|
||||
"b",
|
||||
context=ValidationError(
|
||||
"c",
|
||||
)
|
||||
)
|
||||
)
|
||||
assert_validationerror(err, """
|
||||
ValidationError:
|
||||
a
|
||||
Context:
|
||||
b
|
||||
Context:
|
||||
c
|
||||
""")
|
||||
|
||||
def test_multiple_nested_context(self):
|
||||
err = ValidationError(
|
||||
"a",
|
||||
"b",
|
||||
context=ValidationError(
|
||||
ValidationError(
|
||||
"c",
|
||||
context=ValidationError("d", "e")
|
||||
),
|
||||
ValidationError(
|
||||
"f",
|
||||
context=ValidationError("g")
|
||||
),
|
||||
context=ValidationError("h", "i")
|
||||
)
|
||||
)
|
||||
assert_validationerror(err, """
|
||||
ValidationError:
|
||||
a
|
||||
b
|
||||
Context:
|
||||
ValidationError:
|
||||
c
|
||||
Context:
|
||||
d
|
||||
e
|
||||
ValidationError:
|
||||
f
|
||||
Context:
|
||||
g
|
||||
Context:
|
||||
h
|
||||
i
|
||||
""")
|
||||
|
||||
def test_schema(self):
|
||||
err = ValidationError(
|
||||
ValidationError(
|
||||
"foo",
|
||||
schema=dict
|
||||
),
|
||||
ValidationError(
|
||||
"bar",
|
||||
schema="something"
|
||||
),
|
||||
schema=any
|
||||
)
|
||||
assert_validationerror(err, """
|
||||
ValidationError(AnySchema):
|
||||
ValidationError(dict):
|
||||
foo
|
||||
ValidationError(something):
|
||||
bar
|
||||
""")
|
||||
|
||||
def test_recursion(self):
|
||||
err1 = ValidationError("foo")
|
||||
err2 = ValidationError("bar", context=err1)
|
||||
err1.context = err2
|
||||
assert_validationerror(err1, """
|
||||
ValidationError:
|
||||
foo
|
||||
Context:
|
||||
bar
|
||||
Context:
|
||||
...
|
||||
""")
|
||||
|
|
Loading…
Reference in New Issue