Skip to content

Commit ae8757e

Browse files
committed
plugin.api.validate: truncate error messages
- 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 072958e commit ae8757e

File tree

4 files changed

+151
-29
lines changed

4 files changed

+151
-29
lines changed

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
from textwrap import indent
2+
from typing import Optional, Sequence, Union
23

34

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

1030
def _get_schema_name(self) -> str:
1131
if not self.schema:

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

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ def validate(self, value, name="result", exception=PluginError):
4141
@singledispatch
4242
def validate(schema, value):
4343
if schema != value:
44-
raise ValidationError(f"{value!r} does not equal {schema!r}", schema="equality")
44+
raise ValidationError(
45+
"{value} does not equal {expected}",
46+
value=repr(value),
47+
expected=repr(schema),
48+
schema="equality",
49+
)
4550

4651
return value
4752

@@ -54,15 +59,25 @@ def _validate_schema(schema: Schema, value):
5459
@validate.register(type)
5560
def _validate_type(schema, value):
5661
if not isinstance(value, schema):
57-
raise ValidationError(f"Type of {value!r} should be '{schema.__name__}', but is '{type(value).__name__}'", schema=type)
62+
raise ValidationError(
63+
"Type of {value} should be {expected}, but is {actual}",
64+
value=repr(value),
65+
expected=schema.__name__,
66+
actual=type(value).__name__,
67+
schema=type,
68+
)
5869

5970
return value
6071

6172

6273
@validate.register(abc.Callable)
6374
def _validate_callable(schema: abc.Callable, value):
6475
if not schema(value):
65-
raise ValidationError(f"{schema.__name__}({value!r}) is not true", schema=abc.Callable)
76+
raise ValidationError(
77+
"{callable} is not true",
78+
callable=f"{schema.__name__}({value!r})",
79+
schema=abc.Callable,
80+
)
6681

6782
return value
6883

@@ -111,10 +126,21 @@ def _validate_getitem(schema: GetItemSchema, value):
111126
except (KeyError, IndexError):
112127
# only return default value on last item in nested lookup
113128
if idx < len(item) - 1:
114-
raise ValidationError(f"Item \"{key}\" was not found in object \"{value}\"", schema=GetItemSchema)
129+
raise ValidationError(
130+
"Item {key} was not found in object {value}",
131+
key=repr(key),
132+
value=repr(value),
133+
schema=GetItemSchema,
134+
)
115135
return schema.default
116136
except (TypeError, AttributeError) as err:
117-
raise ValidationError(f"Could not get key \"{key}\" from object \"{value}\"", schema=GetItemSchema, context=err)
137+
raise ValidationError(
138+
"Could not get key {key} from object {value}",
139+
key=repr(key),
140+
value=repr(value),
141+
schema=GetItemSchema,
142+
context=err,
143+
)
118144

119145

120146
@validate.register(list)
@@ -156,12 +182,22 @@ def _validate_dict(schema, value):
156182
break
157183

158184
if key not in value:
159-
raise ValidationError(f"Key '{key}' not found in {value!r}", schema=dict)
185+
raise ValidationError(
186+
"Key {key} not found in {value}",
187+
key=repr(key),
188+
value=repr(value),
189+
schema=dict,
190+
)
160191

161192
try:
162193
new[key] = validate(subschema, value[key])
163194
except ValidationError as err:
164-
raise ValidationError(f"Unable to validate value of key '{key}'", schema=dict, context=err)
195+
raise ValidationError(
196+
"Unable to validate value of key {key}",
197+
key=repr(key),
198+
schema=dict,
199+
context=err,
200+
)
165201

166202
return new
167203

@@ -205,12 +241,22 @@ def _validate_attr(schema: AttrSchema, value):
205241

206242
for key, subschema in schema.schema.items():
207243
if not hasattr(value, key):
208-
raise ValidationError(f"Attribute \"{key}\" not found on object \"{value}\"", schema=AttrSchema)
244+
raise ValidationError(
245+
"Attribute {key} not found on object {value}",
246+
key=repr(key),
247+
value=repr(value),
248+
schema=AttrSchema,
249+
)
209250

210251
try:
211252
value = validate(subschema, getattr(value, key))
212253
except ValidationError as err:
213-
raise ValidationError(f"Could not validate attribute \"{key}\"", schema=AttrSchema, context=err)
254+
raise ValidationError(
255+
"Could not validate attribute {key}",
256+
key=repr(key),
257+
schema=AttrSchema,
258+
context=err,
259+
)
214260

215261
setattr(new, key, value)
216262

@@ -235,7 +281,10 @@ def _validate_unions(schema, value):
235281
# noinspection PyUnusedLocal
236282
@singledispatch
237283
def validate_union(schema, value):
238-
raise ValidationError(f"Invalid union type: {type(schema).__name__}")
284+
raise ValidationError(
285+
"Invalid union type: {type}",
286+
type=type(schema).__name__,
287+
)
239288

240289

241290
@validate_union.register(dict)
@@ -252,7 +301,12 @@ def _validate_union_dict(schema, value):
252301
if is_optional:
253302
continue
254303

255-
raise ValidationError(f"Unable to validate union \"{key}\"", schema=dict, context=err)
304+
raise ValidationError(
305+
"Unable to validate union {key}",
306+
key=repr(key),
307+
schema=dict,
308+
context=err,
309+
)
256310

257311
return new
258312

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

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ def validator_length(number: int) -> Callable[[str], bool]:
2323

2424
def min_len(value):
2525
if not len(value) >= number:
26-
raise ValidationError(f"Minimum length is {number}, but value is {len(value)}", schema="length")
26+
raise ValidationError(
27+
"Minimum length is {number}, but value is {value}",
28+
number=repr(number),
29+
value=len(value),
30+
schema="length",
31+
)
2732

2833
return True
2934

@@ -38,7 +43,12 @@ def validator_startswith(string: str) -> Callable[[str], bool]:
3843
def starts_with(value):
3944
validate(str, value)
4045
if not value.startswith(string):
41-
raise ValidationError(f"'{value}' does not start with '{string}'", schema="startswith")
46+
raise ValidationError(
47+
"{value} does not start with {string}",
48+
value=repr(value),
49+
string=repr(string),
50+
schema="startswith",
51+
)
4252

4353
return True
4454

@@ -53,7 +63,12 @@ def validator_endswith(string: str) -> Callable[[str], bool]:
5363
def ends_with(value):
5464
validate(str, value)
5565
if not value.endswith(string):
56-
raise ValidationError(f"'{value}' does not end with '{string}'", schema="endswith")
66+
raise ValidationError(
67+
"{value} does not end with {string}",
68+
value=repr(value),
69+
string=repr(string),
70+
schema="endswith",
71+
)
5772

5873
return True
5974

@@ -68,7 +83,12 @@ def validator_contains(string: str) -> Callable[[str], bool]:
6883
def contains_str(value):
6984
validate(str, value)
7085
if string not in value:
71-
raise ValidationError(f"'{value}' does not contain '{string}'", schema="contains")
86+
raise ValidationError(
87+
"{value} does not contain {string}",
88+
value=repr(value),
89+
string=repr(string),
90+
schema="contains",
91+
)
7292

7393
return True
7494

@@ -90,16 +110,29 @@ def check_url(value):
90110
validate(str, value)
91111
parsed = urlparse(value)
92112
if not parsed.netloc:
93-
raise ValidationError(f"'{value}' is not a valid URL", schema="url")
113+
raise ValidationError(
114+
"{value} is not a valid URL",
115+
value=repr(value),
116+
schema="url",
117+
)
94118

95119
for name, schema in attributes.items():
96120
if not hasattr(parsed, name):
97-
raise ValidationError(f"Invalid URL attribute '{name}'", schema="url")
121+
raise ValidationError(
122+
"Invalid URL attribute {name}",
123+
name=repr(name),
124+
schema="url",
125+
)
98126

99127
try:
100128
validate(schema, getattr(parsed, name))
101129
except ValidationError as err:
102-
raise ValidationError(f"Unable to validate URL attribute '{name}'", schema="url", context=err)
130+
raise ValidationError(
131+
"Unable to validate URL attribute {name}",
132+
name=repr(name),
133+
schema="url",
134+
context=err,
135+
)
103136

104137
return True
105138

@@ -188,7 +221,11 @@ def xpath_find(value):
188221
validate(iselement, value)
189222
value = value.find(xpath)
190223
if value is None:
191-
raise ValidationError(f"XPath '{xpath}' did not return an element", schema="xml_find")
224+
raise ValidationError(
225+
"XPath {xpath} did not return an element",
226+
xpath=repr(xpath),
227+
schema="xml_find",
228+
)
192229

193230
return validate(iselement, value)
194231

tests/test_api_validate.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class B(A):
8787
validate(B, a)
8888
assert_validationerror(cm.exception, """
8989
ValidationError(type):
90-
Type of a should be 'B', but is 'A'
90+
Type of a should be B, but is A
9191
""")
9292

9393
def test_callable(self):
@@ -112,7 +112,7 @@ def test_all(self):
112112
validate(all(int, float), 123)
113113
assert_validationerror(cm.exception, """
114114
ValidationError(type):
115-
Type of 123 should be 'float', but is 'int'
115+
Type of 123 should be float, but is int
116116
""")
117117

118118
def test_any(self):
@@ -126,9 +126,9 @@ def test_any(self):
126126
assert_validationerror(cm.exception, """
127127
ValidationError(AnySchema):
128128
ValidationError(type):
129-
Type of '123' should be 'int', but is 'str'
129+
Type of '123' should be int, but is str
130130
ValidationError(type):
131-
Type of '123' should be 'float', but is 'str'
131+
Type of '123' should be float, but is str
132132
""")
133133

134134
def test_transform(self):
@@ -211,7 +211,7 @@ def test_get(self):
211211
validate(get("key"), None)
212212
assert_validationerror(cm.exception, """
213213
ValidationError(GetItemSchema):
214-
Could not get key "key" from object "None"
214+
Could not get key 'key' from object None
215215
Context:
216216
'NoneType' object is not subscriptable
217217
""")
@@ -227,14 +227,14 @@ def test_get(self):
227227
validate(get(("one", "invalidkey", "three")), data)
228228
assert_validationerror(cm.exception, """
229229
ValidationError(GetItemSchema):
230-
Item "invalidkey" was not found in object "{'two': {'three': 'value1'}}"
230+
Item 'invalidkey' was not found in object {'two': {'three': 'value1'}}
231231
""")
232232

233233
with self.assertRaises(ValueError) as cm:
234234
validate(all(get("one"), get("invalidkey"), get("three")), data)
235235
assert_validationerror(cm.exception, """
236236
ValidationError(GetItemSchema):
237-
Could not get key "three" from object "None"
237+
Could not get key 'three' from object None
238238
Context:
239239
'NoneType' object is not subscriptable
240240
""")
@@ -377,7 +377,7 @@ def test_attr(self):
377377
validate(attr({"foo": str}), {"bar": "baz"})
378378
assert_validationerror(cm.exception, """
379379
ValidationError(AttrSchema):
380-
Attribute "foo" not found on object "{'bar': 'baz'}"
380+
Attribute 'foo' not found on object {'bar': 'baz'}
381381
""")
382382

383383
def test_url(self):
@@ -592,3 +592,14 @@ def test_schema(self):
592592
ValidationError(something):
593593
bar
594594
""")
595+
596+
def test_truncate(self):
597+
err = ValidationError(
598+
"foo {foo} bar {bar} baz",
599+
foo="Some really long error message that exceeds the maximum error message length",
600+
bar=repr("Some really long error message that exceeds the maximum error message length"),
601+
)
602+
assert_validationerror(err, """
603+
ValidationError:
604+
foo <Some really long error message that exceeds the maximum...> bar <'Some really long error message that exceeds the maximu...> baz
605+
""") # noqa: 501

0 commit comments

Comments
 (0)