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:
bastimeyer 2022-05-07 13:20:46 +02:00 committed by back-to
parent 120c103023
commit 3d44da082b
5 changed files with 365 additions and 70 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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>&quot;perfectly&quot;<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:
...
""")