Skip to content

Commit f0de7f2

Browse files
authored
fix: Include *(Datum|Value) in SchemaValidationError (#3750)
1 parent 2b7e9b3 commit f0de7f2

File tree

3 files changed

+105
-26
lines changed

3 files changed

+105
-26
lines changed

altair/utils/schemapi.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,42 @@ def _validator_values(errors: Iterable[ValidationError], /) -> Iterator[str]:
592592
yield cast("str", err.validator_value)
593593

594594

595+
def _iter_channels(tp: type[Any], spec: Mapping[str, Any], /) -> Iterator[type[Any]]:
596+
from altair import vegalite
597+
598+
for channel_type in ("datum", "value"):
599+
if channel_type in spec:
600+
name = f"{tp.__name__}{channel_type.capitalize()}"
601+
if narrower := getattr(vegalite, name, None):
602+
yield narrower
603+
604+
605+
def _is_channel(obj: Any) -> TypeIs[dict[str, Any]]:
606+
props = {"datum", "value"}
607+
return (
608+
_is_dict(obj)
609+
and all(isinstance(k, str) for k in obj)
610+
and not (props.isdisjoint(obj))
611+
)
612+
613+
614+
def _maybe_channel(tp: type[Any], spec: Any, /) -> type[Any]:
615+
"""
616+
Replace a channel type with a `more specific`_ one or passthrough unchanged.
617+
618+
Parameters
619+
----------
620+
tp
621+
An imported ``SchemaBase`` class.
622+
spec
623+
The instance that failed validation.
624+
625+
.. _more specific:
626+
https://github.com/vega/altair/issues/2913#issuecomment-2571762700
627+
"""
628+
return next(_iter_channels(tp, spec), tp) if _is_channel(spec) else tp
629+
630+
595631
class SchemaValidationError(jsonschema.ValidationError):
596632
_JS_TO_PY: ClassVar[Mapping[str, str]] = {
597633
"boolean": "bool",
@@ -703,22 +739,19 @@ def _get_altair_class_for_error(
703739
Try to get the lowest class possible in the chart hierarchy so it can be displayed in the error message.
704740
705741
This should lead to more informative error messages pointing the user closer to the source of the issue.
742+
743+
If we did not find a suitable class based on traversing the path so we fall
744+
back on the class of the top-level object which created the SchemaValidationError
706745
"""
707746
from altair import vegalite
708747

709748
for prop_name in reversed(error.absolute_path):
710749
# Check if str as e.g. first item can be a 0
711750
if isinstance(prop_name, str):
712-
potential_class_name = prop_name[0].upper() + prop_name[1:]
713-
cls = getattr(vegalite, potential_class_name, None)
714-
if cls is not None:
715-
break
716-
else:
717-
# Did not find a suitable class based on traversing the path so we fall
718-
# back on the class of the top-level object which created
719-
# the SchemaValidationError
720-
cls = self.obj.__class__
721-
return cls
751+
candidate = prop_name[0].upper() + prop_name[1:]
752+
if tp := getattr(vegalite, candidate, None):
753+
return _maybe_channel(tp, self.instance)
754+
return type(self.obj)
722755

723756
@staticmethod
724757
def _format_params_as_table(param_dict_keys: Iterable[str]) -> str:

tests/utils/test_schemapi.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,11 @@ def chart_error_example__additional_datum_argument():
522522
return alt.Chart().mark_point().encode(x=alt.datum(1, wrong_argument=1))
523523

524524

525+
def chart_error_example__additional_value_argument():
526+
# Error: `ColorValue` has no parameter named 'predicate'
527+
return alt.Chart().mark_point().encode(color=alt.value("red", predicate=True))
528+
529+
525530
def chart_error_example__invalid_value_type():
526531
# Error: Value cannot be an integer in this case
527532
return (
@@ -812,15 +817,23 @@ def id_func_chart_error_example(val) -> str:
812817
),
813818
(
814819
chart_error_example__additional_datum_argument,
815-
r"""`X` has no parameter named 'wrong_argument'
820+
r"""`XDatum` has no parameter named 'wrong_argument'
821+
822+
Existing parameter names are:
823+
datum impute title
824+
axis scale type
825+
bandPosition stack
826+
827+
See the help for `XDatum` to read the full description of these parameters$""",
828+
),
829+
(
830+
chart_error_example__additional_value_argument,
831+
r"""`ColorValue` has no parameter named 'predicate'
816832
817833
Existing parameter names are:
818-
shorthand bin scale timeUnit
819-
aggregate field sort title
820-
axis impute stack type
821-
bandPosition
834+
value condition
822835
823-
See the help for `X` to read the full description of these parameters$""",
836+
See the help for `ColorValue` to read the full description of these parameters$""",
824837
),
825838
(
826839
chart_error_example__invalid_value_type,

tools/schemapi/schemapi.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,42 @@ def _validator_values(errors: Iterable[ValidationError], /) -> Iterator[str]:
590590
yield cast("str", err.validator_value)
591591

592592

593+
def _iter_channels(tp: type[Any], spec: Mapping[str, Any], /) -> Iterator[type[Any]]:
594+
from altair import vegalite
595+
596+
for channel_type in ("datum", "value"):
597+
if channel_type in spec:
598+
name = f"{tp.__name__}{channel_type.capitalize()}"
599+
if narrower := getattr(vegalite, name, None):
600+
yield narrower
601+
602+
603+
def _is_channel(obj: Any) -> TypeIs[dict[str, Any]]:
604+
props = {"datum", "value"}
605+
return (
606+
_is_dict(obj)
607+
and all(isinstance(k, str) for k in obj)
608+
and not (props.isdisjoint(obj))
609+
)
610+
611+
612+
def _maybe_channel(tp: type[Any], spec: Any, /) -> type[Any]:
613+
"""
614+
Replace a channel type with a `more specific`_ one or passthrough unchanged.
615+
616+
Parameters
617+
----------
618+
tp
619+
An imported ``SchemaBase`` class.
620+
spec
621+
The instance that failed validation.
622+
623+
.. _more specific:
624+
https://github.com/vega/altair/issues/2913#issuecomment-2571762700
625+
"""
626+
return next(_iter_channels(tp, spec), tp) if _is_channel(spec) else tp
627+
628+
593629
class SchemaValidationError(jsonschema.ValidationError):
594630
_JS_TO_PY: ClassVar[Mapping[str, str]] = {
595631
"boolean": "bool",
@@ -701,22 +737,19 @@ def _get_altair_class_for_error(
701737
Try to get the lowest class possible in the chart hierarchy so it can be displayed in the error message.
702738
703739
This should lead to more informative error messages pointing the user closer to the source of the issue.
740+
741+
If we did not find a suitable class based on traversing the path so we fall
742+
back on the class of the top-level object which created the SchemaValidationError
704743
"""
705744
from altair import vegalite
706745

707746
for prop_name in reversed(error.absolute_path):
708747
# Check if str as e.g. first item can be a 0
709748
if isinstance(prop_name, str):
710-
potential_class_name = prop_name[0].upper() + prop_name[1:]
711-
cls = getattr(vegalite, potential_class_name, None)
712-
if cls is not None:
713-
break
714-
else:
715-
# Did not find a suitable class based on traversing the path so we fall
716-
# back on the class of the top-level object which created
717-
# the SchemaValidationError
718-
cls = self.obj.__class__
719-
return cls
749+
candidate = prop_name[0].upper() + prop_name[1:]
750+
if tp := getattr(vegalite, candidate, None):
751+
return _maybe_channel(tp, self.instance)
752+
return type(self.obj)
720753

721754
@staticmethod
722755
def _format_params_as_table(param_dict_keys: Iterable[str]) -> str:

0 commit comments

Comments
 (0)