Skip to content

Commit 0e63a26

Browse files
committed
[PATCH] plugin.api.validate: truncate error messages (streamlink#4514)
- Change signature of `ValidationError` to be able to pass template strings and template variables which can get truncated individually. - Make error messages consistent by using string representations for template variables where it makes sense
1 parent 55811fc commit 0e63a26

File tree

6 files changed

+152
-37
lines changed

6 files changed

+152
-37
lines changed

src/streamlink/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def indent(text, prefix, predicate=None):
3333
if predicate is None:
3434
def predicate(line):
3535
return line.strip()
36-
36+
3737
def prefixed_lines():
3838
for line in text.splitlines(True):
3939
yield (prefix + line if predicate(line) else line)

src/streamlink/plugin/api/validate/_exception.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1+
from typing import Optional, Sequence, Union
2+
13
from streamlink.compat import indent
24

35

46
class ValidationError(ValueError):
5-
def __init__(self, *errors, **kwargs):
6-
# type: ("ValidationError")
7-
self.schema = kwargs.get("schema")
8-
self.errors = errors
9-
self.context = kwargs.get("context")
7+
MAX_LENGTH = 60
8+
9+
def __init__(self, *error, **kwargs):
10+
# type: (Union[str, Exception, Sequence[Union[str, Exception]]])
11+
self.schema = kwargs.pop("schema", None)
12+
# type: Optional[Union[str, object]]
13+
self.context = kwargs.pop("context", None)
14+
# type: Optional[Union[Exception]]
15+
if len(error) == 1 and type(error[0]) is str:
16+
self.errors = (self._truncate(error[0], **kwargs), )
17+
else:
18+
self.errors = error
19+
20+
def _ellipsis(self, string):
21+
# type: (str)
22+
return string if len(string) <= self.MAX_LENGTH else "<{}...>".format(string[:self.MAX_LENGTH - 5])
23+
24+
def _truncate(self, template, **kwargs):
25+
# type: (str)
26+
return str(template).format(
27+
**{k: self._ellipsis(str(v)) for k, v in kwargs.items()}
28+
)
1029

1130
def _get_schema_name(self):
1231
# type: () -> str

src/streamlink/plugin/api/validate/_schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class UnionGetSchema(object):
110110
def __init__(self, *getters, **kw):
111111
self.getters = tuple(GetItemSchema(getter) for getter in getters)
112112
self.seq = kw.get("seq", tuple)
113+
# type: Type[Union[List, FrozenSet, Set, Tuple]]
113114

114115

115116
class UnionSchema(SchemaContainer):

src/streamlink/plugin/api/validate/_validate.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from copy import copy, deepcopy
23

34
from lxml.etree import Element, iselement
@@ -36,7 +37,12 @@ def validate(self, value, name="result", exception=PluginError):
3637
@singledispatch
3738
def validate(schema, value):
3839
if schema != value:
39-
raise ValidationError("{0!r} does not equal {1!r}".format(value, schema), schema="equality")
40+
raise ValidationError(
41+
"{value} does not equal {expected}",
42+
value=repr(value),
43+
expected=repr(schema),
44+
schema="equality",
45+
)
4046

4147
return value
4248

@@ -49,7 +55,11 @@ def _validate_type(schema, value):
4955
schema = str
5056
if not isinstance(value, schema):
5157
raise ValidationError(
52-
"Type of {0!r} should be '{1}', but is '{2}'".format(value, schema.__name__, type(value).__name__), schema=type
58+
"Type of {value} should be {expected}, but is {actual}",
59+
value=repr(value),
60+
expected=schema.__name__,
61+
actual=type(value).__name__,
62+
schema=type,
5363
)
5464

5565
return value
@@ -94,12 +104,22 @@ def _validate_dict(schema, value):
94104
break
95105

96106
if key not in value:
97-
raise ValidationError("Key '{0}' not found in {1!r}".format(key, value), schema=dict)
107+
raise ValidationError(
108+
"Key {key} not found in {value}",
109+
key=repr(key),
110+
value=repr(value),
111+
schema=dict,
112+
)
98113

99114
try:
100115
new[key] = validate(subschema, value[key])
101116
except ValidationError as err:
102-
raise ValidationError("Unable to validate value of key '{0}'".format(key), schema=dict, context=err)
117+
raise ValidationError(
118+
"Unable to validate value of key {key}",
119+
key=repr(key),
120+
schema=dict,
121+
context=err,
122+
)
103123

104124
return new
105125

@@ -108,7 +128,11 @@ def _validate_dict(schema, value):
108128
def _validate_callable(schema, value):
109129
# type: (Callable)
110130
if not schema(value):
111-
raise ValidationError("{0}({1!r}) is not true".format(schema.__name__, value), schema=Callable)
131+
raise ValidationError(
132+
"{callable} is not true",
133+
callable="{0}({1!r})".format(schema.__name__, value),
134+
schema=Callable,
135+
)
112136

113137
return value
114138

@@ -161,12 +185,20 @@ def _validate_getitemschema(schema, value):
161185
except (KeyError, IndexError):
162186
# only return default value on last item in nested lookup
163187
if idx < len(item) - 1:
164-
raise ValidationError("Item \"{0}\" was not found in object \"{1}\"".format(key, value), schema=GetItemSchema)
188+
raise ValidationError(
189+
"Item {key} was not found in object {value}",
190+
key=repr(key),
191+
value=repr(value),
192+
schema=GetItemSchema,
193+
)
165194
return schema.default
166195
except (TypeError, AttributeError) as err:
167196
raise ValidationError(
168-
"Could not get key \"{0}\" from object \"{1}\"".format(key, value), schema=GetItemSchema,
169-
context=err
197+
"Could not get key {key} from object {value}",
198+
key=repr(key),
199+
value=repr(value),
200+
schema=GetItemSchema,
201+
context=err,
170202
)
171203

172204

@@ -177,12 +209,22 @@ def _validate_attrschema(schema, value):
177209

178210
for key, subschema in schema.schema.items():
179211
if not hasattr(value, key):
180-
raise ValidationError("Attribute \"{0}\" not found on object \"{1}\"".format(key, value), schema=AttrSchema)
212+
raise ValidationError(
213+
"Attribute {key} not found on object {value}",
214+
key=repr(key),
215+
value=repr(value),
216+
schema=AttrSchema,
217+
)
181218

182219
try:
183220
value = validate(subschema, getattr(value, key))
184221
except ValidationError as err:
185-
raise ValidationError("Could not validate attribute \"{0}\"".format(key), schema=AttrSchema, context=err)
222+
raise ValidationError(
223+
"Could not validate attribute {key}",
224+
key=repr(key),
225+
schema=AttrSchema,
226+
context=err,
227+
)
186228

187229
setattr(new, key, value)
188230

@@ -206,7 +248,7 @@ def _validate_xmlelementschema(schema, value):
206248

207249
if schema.attrib is not None:
208250
try:
209-
attrib = validate(schema.attrib, dict(value.attrib))
251+
attrib = validate(schema.attrib, OrderedDict(value.attrib))
210252
except ValidationError as err:
211253
raise ValidationError("Unable to validate XML attributes: {0}".format(err), schema=XmlElementSchema, context=err)
212254

@@ -254,7 +296,10 @@ def _validate_unionschema(schema, value):
254296
# noinspection PyUnusedLocal
255297
@singledispatch
256298
def validate_union(schema, value):
257-
raise ValidationError("Invalid union type: {0}".format(type(schema).__name__))
299+
raise ValidationError(
300+
"Invalid union type: {type}",
301+
type=type(schema).__name__,
302+
)
258303

259304

260305
@validate_union.register(dict)
@@ -271,7 +316,12 @@ def _validate_union_dict(schema, value):
271316
if is_optional:
272317
continue
273318

274-
raise ValidationError("Unable to validate union '{0}': {1}".format(key, err), schema=dict, context=err)
319+
raise ValidationError(
320+
"Unable to validate union {key}",
321+
key=repr(key),
322+
schema=dict,
323+
context=err,
324+
)
275325

276326
return new
277327

src/streamlink/plugin/api/validate/_validators.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ def validator_length(number):
2424

2525
def min_len(value):
2626
if not len(value) >= number:
27-
raise ValidationError("Minimum length is {0} but value is {1}".format(number, len(value)), schema="length")
27+
raise ValidationError(
28+
"Minimum length is {number}, but value is {value}",
29+
number=repr(number),
30+
value=len(value),
31+
schema="length",
32+
)
2833

2934
return True
3035

@@ -40,7 +45,12 @@ def validator_startswith(string):
4045
def starts_with(value):
4146
validate(str, value)
4247
if not value.startswith(string):
43-
raise ValidationError("'{0}' does not start with '{1}'".format(value, string), schema="startswith")
48+
raise ValidationError(
49+
"{value} does not start with {string}",
50+
value=repr(value),
51+
string=repr(string),
52+
schema="startswith",
53+
)
4454

4555
return True
4656

@@ -56,7 +66,12 @@ def validator_endswith(string):
5666
def ends_with(value):
5767
validate(str, value)
5868
if not value.endswith(string):
59-
raise ValidationError("'{0}' does not end with '{1}'".format(value, string), schema="endswith")
69+
raise ValidationError(
70+
"{value} does not end with {string}",
71+
value=repr(value),
72+
string=repr(string),
73+
schema="endswith",
74+
)
6075

6176
return True
6277

@@ -72,7 +87,12 @@ def validator_contains(string):
7287
def contains_str(value):
7388
validate(str, value)
7489
if string not in value:
75-
raise ValidationError("'{0}' does not contain '{1}'".format(value, string), schema="contains")
90+
raise ValidationError(
91+
"{value} does not contain {string}",
92+
value=repr(value),
93+
string=repr(string),
94+
schema="contains",
95+
)
7696

7797
return True
7898

@@ -93,16 +113,29 @@ def check_url(value):
93113
validate(str, value)
94114
parsed = urlparse(value)
95115
if not parsed.netloc:
96-
raise ValidationError("'{0}' is not a valid URL".format(value))
116+
raise ValidationError(
117+
"{value} is not a valid URL",
118+
value=repr(value),
119+
schema="url",
120+
)
97121

98122
for name, schema in attributes.items():
99123
if not hasattr(parsed, name):
100-
raise ValidationError("Invalid URL attribute '{0}'".format(name), schema="url")
124+
raise ValidationError(
125+
"Invalid URL attribute {name}",
126+
name=repr(name),
127+
schema="url",
128+
)
101129

102130
try:
103131
validate(schema, getattr(parsed, name))
104132
except ValidationError as err:
105-
raise ValidationError("Unable to validate URL attribute '{0}': {1}".format(name, err), context=err)
133+
raise ValidationError(
134+
"Unable to validate URL attribute {name}",
135+
name=repr(name),
136+
schema="url",
137+
context=err,
138+
)
106139

107140
return True
108141

@@ -196,7 +229,11 @@ def xpath_find(value):
196229
validate(iselement, value)
197230
value = value.find(xpath)
198231
if value is None:
199-
raise ValidationError("XPath '{0}' did not return an element".format(xpath), schema="xml_find")
232+
raise ValidationError(
233+
"XPath {xpath} did not return an element",
234+
xpath=repr(xpath),
235+
schema="xml_find",
236+
)
200237

201238
return validate(iselement, value)
202239

tests/test_api_validate.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import unittest
44
from textwrap import dedent
55

6-
import six
76
from lxml.etree import Element
87

98
from streamlink.compat import is_py2
@@ -90,7 +89,7 @@ class B(A):
9089
validate(B, a)
9190
assert_validationerror(cm.exception, """
9291
ValidationError(type):
93-
Type of a should be 'B', but is 'A'
92+
Type of a should be B, but is A
9493
""")
9594

9695
def test_callable(self):
@@ -115,7 +114,7 @@ def test_all(self):
115114
validate(all(int, float), 123)
116115
assert_validationerror(cm.exception, """
117116
ValidationError(type):
118-
Type of 123 should be 'float', but is 'int'
117+
Type of 123 should be float, but is int
119118
""")
120119

121120
def test_any(self):
@@ -129,9 +128,7 @@ def test_any(self):
129128
assert_validationerror(cm.exception, """
130129
ValidationError(AnySchema):
131130
ValidationError(type):
132-
Type of '123' should be 'int', but is 'str'
133-
ValidationError(type):
134-
Type of '123' should be 'float', but is 'str'
131+
Type of 123 should be int, but is str
135132
""")
136133

137134
def test_transform(self):
@@ -233,22 +230,22 @@ def test_get(self):
233230
validate(get(("one", "invalidkey", "three")), data)
234231
assert_validationerror(cm.exception, """
235232
ValidationError(GetItemSchema):
236-
Item "invalidkey" was not found in object "{'two': {'three': 'value1'}}"
233+
Item 'invalidkey' was not found in object {'two': {'three': 'value1'}}
237234
""")
238235

239236
with self.assertRaises(ValueError) as cm:
240237
validate(all(get("one"), get("invalidkey"), get("three")), data)
241238
if is_py2:
242239
assert_validationerror(cm.exception, """
243240
ValidationError(GetItemSchema):
244-
Could not get key "three" from object "None"
241+
Could not get key 'three' from object None
245242
Context:
246243
'NoneType' object has no attribute \'__getitem__\'
247244
""")
248245
else:
249246
assert_validationerror(cm.exception, """
250247
ValidationError(GetItemSchema):
251-
Could not get key "three" from object "None"
248+
Could not get key 'three' from object None
252249
Context:
253250
'NoneType' object is not subscriptable
254251
""")
@@ -411,7 +408,7 @@ def test_attr(self):
411408
validate(attr({"foo": text}), {"bar": "baz"})
412409
assert_validationerror(cm.exception, """
413410
ValidationError(AttrSchema):
414-
Attribute "foo" not found on object "{'bar': 'baz'}"
411+
Attribute 'foo' not found on object {'bar': 'baz'}
415412
""")
416413

417414
def test_url(self):
@@ -659,3 +656,14 @@ def test_recursion(self):
659656
Context:
660657
...
661658
""")
659+
660+
def test_truncate(self):
661+
err = ValidationError(
662+
"foo {foo} bar {bar} baz",
663+
foo="Some really long error message that exceeds the maximum error message length",
664+
bar=repr("Some really long error message that exceeds the maximum error message length"),
665+
)
666+
assert_validationerror(err, """
667+
ValidationError:
668+
foo <Some really long error message that exceeds the maximum...> bar <'Some really long error message that exceeds the maximu...> baz
669+
""") # noqa: 501

0 commit comments

Comments
 (0)