Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7bf976e

Browse files
joelostblombinste
andauthored
ENH: Make the schema validation error for non-existing params more informative (#2568)
* Make the schema validation error for non-existing params more informative * Neatly format the existing params as a table * Use more reliable way of returning the non-existing param name My original approach sometimes returned the name of an existing parameter. This commit uses the same approach as the fallback but extracts just the parameter name from the message string. * Break out table formatting into its own function * Add changes into code generation file * Remove mistakingly added old lines * Only show existing parameters if an unknown parameter was used * Update tests to check for detailed parameter errors * Trim error messages to be more to the point by removing unhelpful info * Add a general heading to error messages with multiple errors so that they work nicely with the cmopressed error message format * Remove "self" from listed params * Blake format and use cleandoc for proper indendation in the source * Update old test message to remove what is no longer in the error message * Move invalid y option tests next to each other * Black format * Update test to account for missing self * Refine the message construction and move it to the else clause where it will be used * Remove flake8 test for multline line strings with trailing whitespace * Add latest updates to source generator file as well * Remove redundant schema_path variable and allow unknown encodings params to trigger the parameter table Co-authored-by: Stefan Binder <[email protected]> * Remove redundant assert * Add latest updates to source generator file as well * Include changes to tests in tools code generation file --------- Co-authored-by: Stefan Binder <[email protected]>
1 parent 50497a9 commit 7bf976e

File tree

4 files changed

+316
-76
lines changed

4 files changed

+316
-76
lines changed

altair/utils/schemapi.py

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import textwrap
88
from typing import Any, Sequence, List
9+
from itertools import zip_longest
910

1011
import jsonschema
1112
import jsonschema.exceptions
@@ -15,6 +16,7 @@
1516

1617
from altair import vegalite
1718

19+
1820
# If DEBUG_MODE is True, then schema objects are converted to dict and
1921
# validated at creation time. This slows things down, particularly for
2022
# larger specs, but leads to much more useful tracebacks for the user.
@@ -158,6 +160,63 @@ def __init__(self, obj, err):
158160
self.obj = obj
159161
self._additional_errors = getattr(err, "_additional_errors", [])
160162

163+
@staticmethod
164+
def _format_params_as_table(param_dict_keys):
165+
"""Format param names into a table so that they are easier to read"""
166+
param_names, name_lengths = zip(
167+
*[
168+
(name, len(name))
169+
for name in param_dict_keys
170+
if name not in ["kwds", "self"]
171+
]
172+
)
173+
# Worst case scenario with the same longest param name in the same
174+
# row for all columns
175+
max_name_length = max(name_lengths)
176+
max_column_width = 80
177+
# Output a square table if not too big (since it is easier to read)
178+
num_param_names = len(param_names)
179+
square_columns = int(np.ceil(num_param_names**0.5))
180+
columns = min(max_column_width // max_name_length, square_columns)
181+
182+
# Compute roughly equal column heights to evenly divide the param names
183+
def split_into_equal_parts(n, p):
184+
return [n // p + 1] * (n % p) + [n // p] * (p - n % p)
185+
186+
column_heights = split_into_equal_parts(num_param_names, columns)
187+
188+
# Section the param names into columns and compute their widths
189+
param_names_columns = []
190+
column_max_widths = []
191+
last_end_idx = 0
192+
for ch in column_heights:
193+
param_names_columns.append(param_names[last_end_idx : last_end_idx + ch])
194+
column_max_widths.append(
195+
max([len(param_name) for param_name in param_names_columns[-1]])
196+
)
197+
last_end_idx = ch + last_end_idx
198+
199+
# Transpose the param name columns into rows to facilitate looping
200+
param_names_rows = []
201+
for li in zip_longest(*param_names_columns, fillvalue=""):
202+
param_names_rows.append(li)
203+
# Build the table as a string by iterating over and formatting the rows
204+
param_names_table = ""
205+
for param_names_row in param_names_rows:
206+
for num, param_name in enumerate(param_names_row):
207+
# Set column width based on the longest param in the column
208+
max_name_length_column = column_max_widths[num]
209+
column_pad = 3
210+
param_names_table += "{:<{}}".format(
211+
param_name, max_name_length_column + column_pad
212+
)
213+
# Insert newlines and spacing after the last element in each row
214+
if num == (len(param_names_row) - 1):
215+
param_names_table += "\n"
216+
# 16 is the indendation of the returned multiline string below
217+
param_names_table += " " * 16
218+
return param_names_table
219+
161220
def __str__(self):
162221
# Try to get the lowest class possible in the chart hierarchy so
163222
# it can be displayed in the error message. This should lead to more informative
@@ -174,26 +233,48 @@ def __str__(self):
174233
# back on the class of the top-level object which created
175234
# the SchemaValidationError
176235
cls = self.obj.__class__
177-
schema_path = ["{}.{}".format(cls.__module__, cls.__name__)]
178-
schema_path.extend(self.schema_path)
179-
schema_path = "->".join(
180-
str(val)
181-
for val in schema_path[:-1]
182-
if val not in (0, "properties", "additionalProperties", "patternProperties")
183-
)
184-
message = self.message
185-
if self._additional_errors:
186-
message += "\n " + "\n ".join(
187-
[e.message for e in self._additional_errors]
188-
)
189-
return """Invalid specification
190236

191-
{}, validating {!r}
237+
# Output all existing parameters when an unknown parameter is specified
238+
if self.validator == "additionalProperties":
239+
param_dict_keys = inspect.signature(cls).parameters.keys()
240+
param_names_table = self._format_params_as_table(param_dict_keys)
241+
242+
# `cleandoc` removes multiline string indentation in the output
243+
return inspect.cleandoc(
244+
"""`{}` has no parameter named {!r}
245+
246+
Existing parameter names are:
247+
{}
248+
See the help for `{}` to read the full description of these parameters
249+
""".format(
250+
cls.__name__,
251+
self.message.split("('")[-1].split("'")[0],
252+
param_names_table,
253+
cls.__name__,
254+
)
255+
)
256+
# Use the default error message for all other cases than unknown parameter errors
257+
else:
258+
message = self.message
259+
# Add a summary line when parameters are passed an invalid value
260+
# For example: "'asdf' is an invalid value for `stack`
261+
if self.absolute_path:
262+
# The indentation here must match that of `cleandoc` below
263+
message = f"""'{self.instance}' is an invalid value for `{self.absolute_path[-1]}`:
264+
265+
{message}"""
266+
267+
if self._additional_errors:
268+
message += "\n " + "\n ".join(
269+
[e.message for e in self._additional_errors]
270+
)
192271

193-
{}
194-
""".format(
195-
schema_path, self.validator, message
196-
)
272+
return inspect.cleandoc(
273+
"""{}
274+
""".format(
275+
message
276+
)
277+
)
197278

198279

199280
class UndefinedType(object):

tests/utils/tests/test_schemapi.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# tools/generate_schema_wrapper.py. Do not modify directly.
33
import copy
44
import io
5+
import inspect
56
import json
67
import jsonschema
78
import re
@@ -377,9 +378,6 @@ def test_schema_validation_error():
377378
assert isinstance(the_err, SchemaValidationError)
378379
message = str(the_err)
379380

380-
assert message.startswith("Invalid specification")
381-
assert "test_schemapi.MySchema->a" in message
382-
assert "validating {!r}".format(the_err.validator) in message
383381
assert the_err.message in message
384382

385383

@@ -467,36 +465,77 @@ def chart_example_invalid_y_option_value_with_condition():
467465
[
468466
(
469467
chart_example_invalid_y_option,
470-
r"schema.channels.X.*"
471-
+ r"Additional properties are not allowed \('unknown' was unexpected\)",
468+
inspect.cleandoc(
469+
r"""`X` has no parameter named 'unknown'
470+
471+
Existing parameter names are:
472+
shorthand bin scale timeUnit
473+
aggregate field sort title
474+
axis impute stack type
475+
bandPosition
476+
477+
See the help for `X` to read the full description of these parameters""" # noqa: W291
478+
),
472479
),
473480
(
474-
chart_example_invalid_y_option_value,
475-
r"schema.channels.Y.*"
476-
+ r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
477-
+ r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
481+
chart_example_layer,
482+
inspect.cleandoc(
483+
r"""`VConcatChart` has no parameter named 'width'
484+
485+
Existing parameter names are:
486+
vconcat center description params title
487+
autosize config name resolve transform
488+
background data padding spacing usermeta
489+
bounds datasets
490+
491+
See the help for `VConcatChart` to read the full description of these parameters""" # noqa: W291
492+
),
478493
),
479494
(
480-
chart_example_layer,
481-
r"api.VConcatChart.*"
482-
+ r"Additional properties are not allowed \('width' was unexpected\)",
495+
chart_example_invalid_y_option_value,
496+
inspect.cleandoc(
497+
r"""'asdf' is an invalid value for `stack`:
498+
499+
'asdf' is not one of \['zero', 'center', 'normalize'\]
500+
'asdf' is not of type 'null'
501+
'asdf' is not of type 'boolean'"""
502+
),
483503
),
484504
(
485505
chart_example_invalid_y_option_value_with_condition,
486-
r"schema.channels.Y.*"
487-
+ r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
488-
+ r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
506+
inspect.cleandoc(
507+
r"""'asdf' is an invalid value for `stack`:
508+
509+
'asdf' is not one of \['zero', 'center', 'normalize'\]
510+
'asdf' is not of type 'null'
511+
'asdf' is not of type 'boolean'"""
512+
),
489513
),
490514
(
491515
chart_example_hconcat,
492-
r"schema.core.TitleParams.*"
493-
+ r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'string'.*"
494-
+ r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'array'",
516+
inspect.cleandoc(
517+
r"""'{'text': 'Horsepower', 'align': 'right'}' is an invalid value for `title`:
518+
519+
{'text': 'Horsepower', 'align': 'right'} is not of type 'string'
520+
{'text': 'Horsepower', 'align': 'right'} is not of type 'array'"""
521+
),
495522
),
496523
(
497524
chart_example_invalid_channel_and_condition,
498-
r"schema.core.Encoding->encoding.*"
499-
+ r"Additional properties are not allowed \('invalidChannel' was unexpected\)",
525+
inspect.cleandoc(
526+
r"""`Encoding` has no parameter named 'invalidChannel'
527+
528+
Existing parameter names are:
529+
angle key order strokeDash tooltip xOffset
530+
color latitude radius strokeOpacity url y
531+
description latitude2 radius2 strokeWidth x y2
532+
detail longitude shape text x2 yError
533+
fill longitude2 size theta xError yError2
534+
fillOpacity opacity stroke theta2 xError2 yOffset
535+
href
536+
537+
See the help for `Encoding` to read the full description of these parameters""" # noqa: W291
538+
),
500539
),
501540
],
502541
)

0 commit comments

Comments
 (0)