Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions altair/utils/schemapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,14 @@ def _get_most_relevant_errors(
if len(errors) == 0:
return []

# Start from the first error on the top-level as we want to show
# an error message for one specific error only even if the chart
# specification might have multiple issues
top_level_error = errors[0]

# Go to lowest level in schema where an error happened as these give
# the most relevant error messages
lowest_level = errors[0]
# the most relevant error messages.
lowest_level = top_level_error
while lowest_level.context:
lowest_level = lowest_level.context[0]

Expand All @@ -102,9 +107,25 @@ def _get_most_relevant_errors(
# In this case we are still at the top level and can return all errors
most_relevant_errors = errors
else:
# Return all errors of the lowest level out of which
# Use all errors of the lowest level out of which
# we can construct more informative error messages
most_relevant_errors = lowest_level.parent.context
if lowest_level.validator == "enum":
# There might be other possible enums which are allowed, e.g. for
# the "timeUnit" property of the "Angle" encoding channel. These do not
# necessarily need to be in the same branch of this tree of errors that
# we traversed down to the lowest level. We therefore gather
# all enums in the leaves of the error tree.
enum_errors = _get_all_lowest_errors_with_validator(
top_level_error, validator="enum"
)
# Remove errors which already exist in enum_errors
enum_errors = [
err
for err in enum_errors
if err.message not in [e.message for e in most_relevant_errors]
]
most_relevant_errors = most_relevant_errors + enum_errors

# This should never happen but might still be good to test for it as else
# the original error would just slip through without being raised
Expand All @@ -114,6 +135,19 @@ def _get_most_relevant_errors(
return most_relevant_errors


def _get_all_lowest_errors_with_validator(
error: jsonschema.ValidationError, validator: str
) -> List[jsonschema.ValidationError]:
matches: List[jsonschema.ValidationError] = []
if error.context:
for err in error.context:
if err.context:
matches.extend(_get_all_lowest_errors_with_validator(err, validator))
elif err.validator == validator:
matches.append(err)
return matches


def _subclasses(cls):
"""Breadth-first sequence of all classes which inherit from cls."""
seen = set()
Expand Down
15 changes: 15 additions & 0 deletions tests/utils/tests/test_schemapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,10 @@ def chart_example_invalid_y_option_value_with_condition():
)


def chart_example_invalid_timeunit_value():
return alt.Chart().encode(alt.Angle().timeUnit("invalid_value"))


@pytest.mark.parametrize(
"chart_func, expected_error_message",
[
Expand Down Expand Up @@ -551,6 +555,17 @@ def chart_example_invalid_y_option_value_with_condition():
See the help for `Encoding` to read the full description of these parameters""" # noqa: W291
),
),
(
chart_example_invalid_timeunit_value,
inspect.cleandoc(
r"""'invalid_value' is an invalid value for `timeUnit`:

'invalid_value' is not one of \['year', 'quarter', 'month', 'week', 'day', 'dayofyear', 'date', 'hours', 'minutes', 'seconds', 'milliseconds'\]
'invalid_value' is not one of \['utcyear', 'utcquarter', 'utcmonth', 'utcweek', 'utcday', 'utcdayofyear', 'utcdate', 'utchours', 'utcminutes', 'utcseconds', 'utcmilliseconds'\]
'invalid_value' is not one of \['yearquarter', 'yearquartermonth', 'yearmonth', 'yearmonthdate', 'yearmonthdatehours', 'yearmonthdatehoursminutes', 'yearmonthdatehoursminutesseconds', 'yearweek', 'yearweekday', 'yearweekdayhours', 'yearweekdayhoursminutes', 'yearweekdayhoursminutesseconds', 'yeardayofyear', 'quartermonth', 'monthdate', 'monthdatehours', 'monthdatehoursminutes', 'monthdatehoursminutesseconds', 'weekday', 'weeksdayhours', 'weekdayhoursminutes', 'weekdayhoursminutesseconds', 'dayhours', 'dayhoursminutes', 'dayhoursminutesseconds', 'hoursminutes', 'hoursminutesseconds', 'minutesseconds', 'secondsmilliseconds'\]
'invalid_value' is not one of \['utcyearquarter', 'utcyearquartermonth', 'utcyearmonth', 'utcyearmonthdate', 'utcyearmonthdatehours', 'utcyearmonthdatehoursminutes', 'utcyearmonthdatehoursminutesseconds', 'utcyearweek', 'utcyearweekday', 'utcyearweekdayhours', 'utcyearweekdayhoursminutes', 'utcyearweekdayhoursminutesseconds', 'utcyeardayofyear', 'utcquartermonth', 'utcmonthdate', 'utcmonthdatehours', 'utcmonthdatehoursminutes', 'utcmonthdatehoursminutesseconds', 'utcweekday', 'utcweeksdayhours', 'utcweekdayhoursminutes', 'utcweekdayhoursminutesseconds', 'utcdayhours', 'utcdayhoursminutes', 'utcdayhoursminutesseconds', 'utchoursminutes', 'utchoursminutesseconds', 'utcminutesseconds', 'utcsecondsmilliseconds'\]"""
),
),
],
)
def test_chart_validation_errors(chart_func, expected_error_message):
Expand Down
40 changes: 37 additions & 3 deletions tools/schemapi/schemapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,14 @@ def _get_most_relevant_errors(
if len(errors) == 0:
return []

# Start from the first error on the top-level as we want to show
# an error message for one specific error only even if the chart
# specification might have multiple issues
top_level_error = errors[0]

# Go to lowest level in schema where an error happened as these give
# the most relevant error messages
lowest_level = errors[0]
# the most relevant error messages.
lowest_level = top_level_error
while lowest_level.context:
lowest_level = lowest_level.context[0]

Expand All @@ -100,9 +105,25 @@ def _get_most_relevant_errors(
# In this case we are still at the top level and can return all errors
most_relevant_errors = errors
else:
# Return all errors of the lowest level out of which
# Use all errors of the lowest level out of which
# we can construct more informative error messages
most_relevant_errors = lowest_level.parent.context
if lowest_level.validator == "enum":
# There might be other possible enums which are allowed, e.g. for
# the "timeUnit" property of the "Angle" encoding channel. These do not
# necessarily need to be in the same branch of this tree of errors that
# we traversed down to the lowest level. We therefore gather
# all enums in the leaves of the error tree.
enum_errors = _get_all_lowest_errors_with_validator(
top_level_error, validator="enum"
)
# Remove errors which already exist in enum_errors
enum_errors = [
err
for err in enum_errors
if err.message not in [e.message for e in most_relevant_errors]
]
most_relevant_errors = most_relevant_errors + enum_errors

# This should never happen but might still be good to test for it as else
# the original error would just slip through without being raised
Expand All @@ -112,6 +133,19 @@ def _get_most_relevant_errors(
return most_relevant_errors


def _get_all_lowest_errors_with_validator(
error: jsonschema.ValidationError, validator: str
) -> List[jsonschema.ValidationError]:
matches: List[jsonschema.ValidationError] = []
if error.context:
for err in error.context:
if err.context:
matches.extend(_get_all_lowest_errors_with_validator(err, validator))
elif err.validator == validator:
matches.append(err)
return matches


def _subclasses(cls):
"""Breadth-first sequence of all classes which inherit from cls."""
seen = set()
Expand Down
15 changes: 15 additions & 0 deletions tools/schemapi/tests/test_schemapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ def chart_example_invalid_y_option_value_with_condition():
)


def chart_example_invalid_timeunit_value():
return alt.Chart().encode(alt.Angle().timeUnit("invalid_value"))


@pytest.mark.parametrize(
"chart_func, expected_error_message",
[
Expand Down Expand Up @@ -549,6 +553,17 @@ def chart_example_invalid_y_option_value_with_condition():
See the help for `Encoding` to read the full description of these parameters""" # noqa: W291
),
),
(
chart_example_invalid_timeunit_value,
inspect.cleandoc(
r"""'invalid_value' is an invalid value for `timeUnit`:

'invalid_value' is not one of \['year', 'quarter', 'month', 'week', 'day', 'dayofyear', 'date', 'hours', 'minutes', 'seconds', 'milliseconds'\]
'invalid_value' is not one of \['utcyear', 'utcquarter', 'utcmonth', 'utcweek', 'utcday', 'utcdayofyear', 'utcdate', 'utchours', 'utcminutes', 'utcseconds', 'utcmilliseconds'\]
'invalid_value' is not one of \['yearquarter', 'yearquartermonth', 'yearmonth', 'yearmonthdate', 'yearmonthdatehours', 'yearmonthdatehoursminutes', 'yearmonthdatehoursminutesseconds', 'yearweek', 'yearweekday', 'yearweekdayhours', 'yearweekdayhoursminutes', 'yearweekdayhoursminutesseconds', 'yeardayofyear', 'quartermonth', 'monthdate', 'monthdatehours', 'monthdatehoursminutes', 'monthdatehoursminutesseconds', 'weekday', 'weeksdayhours', 'weekdayhoursminutes', 'weekdayhoursminutesseconds', 'dayhours', 'dayhoursminutes', 'dayhoursminutesseconds', 'hoursminutes', 'hoursminutesseconds', 'minutesseconds', 'secondsmilliseconds'\]
'invalid_value' is not one of \['utcyearquarter', 'utcyearquartermonth', 'utcyearmonth', 'utcyearmonthdate', 'utcyearmonthdatehours', 'utcyearmonthdatehoursminutes', 'utcyearmonthdatehoursminutesseconds', 'utcyearweek', 'utcyearweekday', 'utcyearweekdayhours', 'utcyearweekdayhoursminutes', 'utcyearweekdayhoursminutesseconds', 'utcyeardayofyear', 'utcquartermonth', 'utcmonthdate', 'utcmonthdatehours', 'utcmonthdatehoursminutes', 'utcmonthdatehoursminutesseconds', 'utcweekday', 'utcweeksdayhours', 'utcweekdayhoursminutes', 'utcweekdayhoursminutesseconds', 'utcdayhours', 'utcdayhoursminutes', 'utcdayhoursminutesseconds', 'utchoursminutes', 'utchoursminutesseconds', 'utcminutesseconds', 'utcsecondsmilliseconds'\]"""
),
),
],
)
def test_chart_validation_errors(chart_func, expected_error_message):
Expand Down