Repository: altair-viz/altair
Pull request #2732 (2022-11-30 04:22:51)

Aliases for ImputeParams and TitleParams
Let's not merge this for a little while to make sure this doesn't break anything and to think about whether we need to do anything more sophisticated.  The point is to make it easier for `impute` and `title` to find the appropriate docstrings, as discussed in https://github.com/altair-viz/altair/pull/2592

joelostblom: And I would add also make it more convenient and consistent to use these classes since the syntax is briefer and more similar to that of the other classes we use for similar things, e.g. `alt.Title` instead of `alt.TitleParams` just like we use `alt.X` etc
mattijn: There are more candidate aliases I think?
```python
import requests as r
import re
vl_scheme = r.get('https://vega.github.io/schema/vega-lite/v5.json').json()
scheme_str = str(vl_scheme['definitions'])
col = []
for word in re.findall(r"\w+", scheme_str):
    if 'Params' in word:
        col.append(word)
list(set(col))
```
Returns 10 candidates:
```python
['TimeUnitParams',
 'TitleParams',
 'SchemeParams',
 'GraticuleParams',
 'SequenceParams',
 'BinParams',
 'ImputeParams',
 'ScaleBinParams',
 'ScaleInterpolateParams',
 'AutoSizeParams']
```

Not sure about this `*Offset`. There are many more:
```python
col_offset = []
for word in re.findall(r"\w+", scheme_str):
    if 'Offset' in word:
        col_offset.append(word)
list(set(col_offset))
```
```python
['theta2Offset',
 'labelOffset',
 'y2Offset',
 'strokeDashOffset',
 'Offset',
 'gridDashOffset',
 'yOffset',
 'tickDashOffset',
 'symbolDashOffset',
 'xOffset',
 'nestedOffsetPaddingInner',
 'StackOffset',
 'bandWithNestedOffsetPaddingInner',
 'thetaOffset',
 'bandWithNestedOffsetPaddingOuter',
 'x2Offset',
 'OffsetDef',
 'strokeOffset',
 'radiusOffset',
 'radius2Offset',
 'domainDashOffset',
 'symbolOffset',
 'tickOffset',
 'gradientLabelOffset',
 'labelFlushOffset',
 'Offsets']
```
joelostblom: Ah, I only looked through some of the encodings methods when I tried so I missed those other Params helper classes, I think it would make sense to add those.

For the Offset, I think `StackOffset` (and maybe `Offset(s)`) are different from the rest in that they are classes that can be passed inside the encodings (such as `X(stack=StackOffset(...)`). Many of the others seem to be names of parameters or encoding channels and would not need an alias I believe.
ChristopherDavisUCI: @joelostblom I don't have a clear view of how the docstrings are currently generated.  Is this PR still useful?  If it is, I will go through the comments above carefully and try to implement the suggestions.
joelostblom: These were required in the way I originally implemented the docstrings, but I haven't reviewed if the additions you made changed that. Regardless, I would be in favor of having these shortcuts (at least `Title`), to make it more consistent and convenient to type them out.
ChristopherDavisUCI: Thanks @mattijn for the extra examples above.  I looked through them but didn't find any others which were being used as these attribute setter methods, so I didn't make any more additions beyond @joelostblom's suggested `StackOffset` (which corresponds to the attribute setter method `stack`).

It seems kind of random to me that the Python 3.8 build is failing but not the builds for Python 3.7, 3.9, 3.10, 3.11.  Do any of you see what the issue is?

If the test issue can be resolved I'd say this is ready to be merged from my perspective.
mattijn: I'll rerun the failed tests. 
Meanwhile can you have a look to this [selected code](https://github.com/altair-viz/altair/blob/master/altair/vegalite/v5/api.py#L3059-L3076) @ChristopherDavisUCI? It was introduced in this PR: https://github.com/altair-viz/altair/pull/1667. It seems that `core.SequenceParams` and `core.GraticuleParams` have its own utility function defined (`alt.sequence()` and `alt.graticule()`). Is that something we have to consider here as well?
ChristopherDavisUCI: Hi @mattijn, thanks!  I don't see any conflict with those two definitions you shared.  Is there anything in particular you were worried about with those?

I do think each customization we add that doesn't stem from Vega-Lite is a possible headache down the road, but since these aliases `Bin`, `Impute`, and `Stack` are only being used to get docstrings, the risk seems pretty low to me.

Please just let me know if I missed your concern!
mattijn: Would it be possible to add some examples what is different after this PR? I thought that we are getting an capital case `alt.Title()` instead of `alt.TitleParams()` function (not sure exactly how and when to use these) and was surprised that `alt.GraticuleParams` is a small case `alt.graticule()`, but the latter seems to do [more](https://github.com/altair-viz/altair/issues/1660). Anyhow, some examples would be beneficial!
joelostblom: I suggest that we remove the `Stack` alias and keep the other two. It would also be good to go through the docs and gallery and replace all the occurrences of `TitleParams` with `Title` and all the `ImputeParams` with `Impute`, since there is now no reason to use the longer more inconvenient name.

---

Some more detailed thoughts below:

It looks like `alt.sequence` is a mix of `alt.SequenceParams` and `alt.SequenceGenerator` so I think it makes sense that these are lower case functions (and the same for `alt.graticule`). The changes here are more similar to `alt.Bin`/`alt.BinParams` which I don't think has a utility function anywhere.

As for when to use `alt.Title`, it is helpful when creating titles with formatting or subtitles as in this SO answer https://stackoverflow.com/a/66147967/2166823. `alt.Impute` has similar advantages since it also takes multiple parameters, but for `alt.Stack` I don't see an advantage other than for the docstrings since it only takes a single parameter.

I did confirm that these aliases are all required for the propagation of the docstrings from the corresponding helper classes, but the docstring from the `StackOffset` class is sparse and all the info there is already contained within the section of the docstring that is extracted from `alt.X()` etc. There is also a discrepancy between the two docstrings where `alt.StackOffset` does not include `true`, `false`, and `null` although these are all allowed values.

![image](https://user-images.githubusercontent.com/4560057/210531657-3d2ce285-efb7-4a14-bf1c-e5758b05bf03.png)

![image](https://user-images.githubusercontent.com/4560057/210531879-9c188093-78cb-4fa4-9679-627c37a46c38.png)


Patch:
diff --git a/altair/utils/display.py b/altair/utils/display.py
index 92d7c783..bcf8232b 100644
--- a/altair/utils/display.py
+++ b/altair/utils/display.py
@@ -4,10 +4,9 @@ import textwrap
 from typing import Callable, Dict
 import uuid
 
-from jsonschema import validate
-
 from .plugin_registry import PluginRegistry
 from .mimebundle import spec_to_mimebundle
+from .schemapi import validate_jsonschema
 
 
 # ==============================================================================
@@ -126,7 +125,7 @@ class Displayable(object):
         # type: () -> None
         """Validate the spec against the schema."""
         schema_dict = json.loads(pkgutil.get_data(*self.schema_path).decode("utf-8"))
-        validate(self.spec, schema_dict)
+        validate_jsonschema(self.spec, schema_dict)
 
     def _repr_mimebundle_(self, include=None, exclude=None):
         """Return a MIME bundle for display in Jupyter frontends."""
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index 733f799f..2c86b1d2 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -8,12 +8,13 @@ import textwrap
 from typing import Any
 
 import jsonschema
+import jsonschema.exceptions
 import numpy as np
 import pandas as pd
 
 from altair import vegalite
 
-
+JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -43,6 +44,24 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
+def validate_jsonschema(spec, schema, resolver=None):
+    # We don't use jsonschema.validate as this would validate the schema itself.
+    # Instead, we pass the schema directly to the validator class. This is done for
+    # two reasons: The schema comes from Vega-Lite and is not based on the user
+    # input, therefore there is no need to validate it in the first place. Furthermore,
+    # the "uri-reference" format checker fails for some of the references as URIs in
+    # "$ref" are not encoded,
+    # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
+    # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
+    # it is not a valid URI reference due to the characters such as '<'.
+    validator = JSONSCHEMA_VALIDATOR(
+        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
+    )
+    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
+    if error is not None:
+        raise error
+
+
 def _subclasses(cls):
     """Breadth-first sequence of all classes which inherit from cls."""
     seen = set()
@@ -158,7 +177,7 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = jsonschema.Draft7Validator
+    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -448,9 +467,7 @@ class SchemaBase(object):
         if schema is None:
             schema = cls._schema
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(
-            instance, schema, cls=cls._validator, resolver=resolver
-        )
+        return validate_jsonschema(instance, schema, resolver=resolver)
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -469,7 +486,7 @@ class SchemaBase(object):
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -563,7 +580,7 @@ class _FromDict(object):
             for possible_schema in schemas:
                 resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    jsonschema.validate(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, resolver=resolver)
                 except jsonschema.ValidationError:
                     continue
                 else:
@@ -621,13 +638,12 @@ class _PropertySetter(object):
                         f"\n\n    {altair_prop.__doc__[attribute_index:]}"
                     )
                 )
-            # For short docsstrings such as Aggregate, Stack, et
+            # For short docstrings such as Aggregate, Stack, et
             else:
                 self.__doc__ = (
                     altair_prop.__doc__.replace("    ", "") + "\n" + self.__doc__
                 )
             # Add signatures and tab completion for the method and parameter names
-            # Currently works for `alt.X.bin` but not alt.X().bin`
             self.__signature__ = inspect.signature(altair_prop)
             self.__wrapped__ = inspect.getfullargspec(altair_prop)
             self.__name__ = altair_prop.__name__
@@ -645,7 +661,9 @@ class _PropertySetter(object):
 
 
 def with_property_setters(cls):
-    """Decorator to add property setters to a Schema class."""
+    """
+    Decorator to add property setters to a Schema class.
+    """
     schema = cls.resolve_references()
     for prop, propschema in schema.get("properties", {}).items():
         setattr(cls, prop, _PropertySetter(prop, propschema))
diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index f32ccd63..84a8018d 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -107,6 +107,8 @@ def _prepare_data(data, context=None):
 # ------------------------------------------------------------------------
 # Aliases & specializations
 Bin = core.BinParams
+Impute = core.ImputeParams
+Title = core.TitleParams
 
 
 @utils.use_signature(core.LookupData)
diff --git a/altair/vegalite/v5/schema/channels.py b/altair/vegalite/v5/schema/channels.py
index a5ea91cd..714dd9a4 100644
--- a/altair/vegalite/v5/schema/channels.py
+++ b/altair/vegalite/v5/schema/channels.py
@@ -91,6 +91,7 @@ class DatumChannelMixin(object):
                                                       ignore=ignore,
                                                       context=context)
 
+
 @with_property_setters
 class Angle(FieldChannelMixin, core.FieldOrDatumDefWithConditionMarkPropFieldDefnumber):
     """Angle schema wrapper
diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py
index 7f17694e..a073457f 100644
--- a/tools/generate_schema_wrapper.py
+++ b/tools/generate_schema_wrapper.py
@@ -248,7 +248,7 @@ def download_schemafile(library, version, schemapath, skip_download=False):
 
 def copy_schemapi_util():
     """
-    Copy the schemapi utility and its test file into altair/utils/
+    Copy the schemapi utility into altair/utils/ and its test file to tests/utils/
     """
     # copy the schemapi utility file
     source_path = abspath(join(dirname(__file__), "schemapi", "schemapi.py"))
@@ -267,7 +267,7 @@ def copy_schemapi_util():
         join(dirname(__file__), "schemapi", "tests", "test_schemapi.py")
     )
     destination_path = abspath(
-        join(dirname(__file__), "..", "altair", "utils", "tests", "test_schemapi.py")
+        join(dirname(__file__), "..", "tests", "utils", "tests", "test_schemapi.py")
     )
 
     print("Copying\n {}\n  -> {}".format(source_path, destination_path))
diff --git a/tools/schemapi/jsonschema-draft04.json b/tools/schemapi/jsonschema-draft04.json
deleted file mode 100644
index bcbb8474..00000000
--- a/tools/schemapi/jsonschema-draft04.json
+++ /dev/null
@@ -1,149 +0,0 @@
-{
-    "id": "http://json-schema.org/draft-04/schema#",
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "description": "Core schema meta-schema",
-    "definitions": {
-        "schemaArray": {
-            "type": "array",
-            "minItems": 1,
-            "items": { "$ref": "#" }
-        },
-        "positiveInteger": {
-            "type": "integer",
-            "minimum": 0
-        },
-        "positiveIntegerDefault0": {
-            "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
-        },
-        "simpleTypes": {
-            "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
-        },
-        "stringArray": {
-            "type": "array",
-            "items": { "type": "string" },
-            "minItems": 1,
-            "uniqueItems": true
-        }
-    },
-    "type": "object",
-    "properties": {
-        "id": {
-            "type": "string"
-        },
-        "$schema": {
-            "type": "string"
-        },
-        "title": {
-            "type": "string"
-        },
-        "description": {
-            "type": "string"
-        },
-        "default": {},
-        "multipleOf": {
-            "type": "number",
-            "minimum": 0,
-            "exclusiveMinimum": true
-        },
-        "maximum": {
-            "type": "number"
-        },
-        "exclusiveMaximum": {
-            "type": "boolean",
-            "default": false
-        },
-        "minimum": {
-            "type": "number"
-        },
-        "exclusiveMinimum": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxLength": { "$ref": "#/definitions/positiveInteger" },
-        "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "pattern": {
-            "type": "string",
-            "format": "regex"
-        },
-        "additionalItems": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "items": {
-            "anyOf": [
-                { "$ref": "#" },
-                { "$ref": "#/definitions/schemaArray" }
-            ],
-            "default": {}
-        },
-        "maxItems": { "$ref": "#/definitions/positiveInteger" },
-        "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "uniqueItems": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxProperties": { "$ref": "#/definitions/positiveInteger" },
-        "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "required": { "$ref": "#/definitions/stringArray" },
-        "additionalProperties": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "definitions": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "properties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "patternProperties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "dependencies": {
-            "type": "object",
-            "additionalProperties": {
-                "anyOf": [
-                    { "$ref": "#" },
-                    { "$ref": "#/definitions/stringArray" }
-                ]
-            }
-        },
-        "enum": {
-            "type": "array",
-            "minItems": 1,
-            "uniqueItems": true
-        },
-        "type": {
-            "anyOf": [
-                { "$ref": "#/definitions/simpleTypes" },
-                {
-                    "type": "array",
-                    "items": { "$ref": "#/definitions/simpleTypes" },
-                    "minItems": 1,
-                    "uniqueItems": true
-                }
-            ]
-        },
-        "format": { "type": "string" },
-        "allOf": { "$ref": "#/definitions/schemaArray" },
-        "anyOf": { "$ref": "#/definitions/schemaArray" },
-        "oneOf": { "$ref": "#/definitions/schemaArray" },
-        "not": { "$ref": "#" }
-    },
-    "dependencies": {
-        "exclusiveMaximum": [ "maximum" ],
-        "exclusiveMinimum": [ "minimum" ]
-    },
-    "default": {}
-}
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index a3f9cd3b..e652d2c3 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -6,12 +6,13 @@ import textwrap
 from typing import Any
 
 import jsonschema
+import jsonschema.exceptions
 import numpy as np
 import pandas as pd
 
 from altair import vegalite
 
-
+JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -41,6 +42,24 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
+def validate_jsonschema(spec, schema, resolver=None):
+    # We don't use jsonschema.validate as this would validate the schema itself.
+    # Instead, we pass the schema directly to the validator class. This is done for
+    # two reasons: The schema comes from Vega-Lite and is not based on the user
+    # input, therefore there is no need to validate it in the first place. Furthermore,
+    # the "uri-reference" format checker fails for some of the references as URIs in
+    # "$ref" are not encoded,
+    # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
+    # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
+    # it is not a valid URI reference due to the characters such as '<'.
+    validator = JSONSCHEMA_VALIDATOR(
+        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
+    )
+    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
+    if error is not None:
+        raise error
+
+
 def _subclasses(cls):
     """Breadth-first sequence of all classes which inherit from cls."""
     seen = set()
@@ -156,7 +175,7 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = jsonschema.Draft7Validator
+    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -446,9 +465,7 @@ class SchemaBase(object):
         if schema is None:
             schema = cls._schema
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(
-            instance, schema, cls=cls._validator, resolver=resolver
-        )
+        return validate_jsonschema(instance, schema, resolver=resolver)
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -467,7 +484,7 @@ class SchemaBase(object):
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -561,7 +578,7 @@ class _FromDict(object):
             for possible_schema in schemas:
                 resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    jsonschema.validate(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, resolver=resolver)
                 except jsonschema.ValidationError:
                     continue
                 else:
diff --git a/tools/schemapi/utils.py b/tools/schemapi/utils.py
index 38875ab0..61261773 100644
--- a/tools/schemapi/utils.py
+++ b/tools/schemapi/utils.py
@@ -1,8 +1,6 @@
 """Utilities for working with schemas"""
 
-import json
 import keyword
-import pkgutil
 import re
 import textwrap
 import urllib
@@ -13,12 +11,6 @@ import jsonschema
 EXCLUDE_KEYS = ("definitions", "title", "description", "$schema", "id")
 
 
-def load_metaschema():
-    schema = pkgutil.get_data("schemapi", "jsonschema-draft04.json")
-    schema = schema.decode()
-    return json.loads(schema)
-
-
 def resolve_references(schema, root=None):
     """Resolve References within a JSON schema"""
     resolver = jsonschema.RefResolver.from_schema(root or schema)
@@ -144,7 +136,7 @@ class SchemaProperties(object):
 class SchemaInfo(object):
     """A wrapper for inspecting a JSON schema"""
 
-    def __init__(self, schema, rootschema=None, validate=False):
+    def __init__(self, schema, rootschema=None):
         if hasattr(schema, "_schema"):
             if hasattr(schema, "_rootschema"):
                 schema, rootschema = schema._schema, schema._rootschema
@@ -152,10 +144,6 @@ class SchemaInfo(object):
                 schema, rootschema = schema._schema, schema._schema
         elif not rootschema:
             rootschema = schema
-        if validate:
-            metaschema = load_metaschema()
-            jsonschema.validate(schema, metaschema)
-            jsonschema.validate(rootschema, metaschema)
         self.raw_schema = schema
         self.rootschema = rootschema
         self.schema = resolve_references(schema, rootschema)


Test patch:
diff --git a/tests/vegalite/v5/tests/test_alias.py b/tests/vegalite/v5/tests/test_alias.py
new file mode 100644
index 00000000..b9051f13
--- /dev/null
+++ b/tests/vegalite/v5/tests/test_alias.py
@@ -0,0 +1,21 @@
+import pytest
+
+import altair.vegalite.v5 as alt
+
+
+def test_aliases():
+    """Ensure that any aliases defined in `api.py` aren't colliding with names already defined in `core.py` or `channels.py`."""
+    for alias in ["Bin", "Impute", "Title"]:
+        # this test pass if the alias can resolve to its real name
+        try:
+            getattr(alt, alias)
+        except AttributeError as e:
+            assert False, f"cannot resolve '{alias}':, {e}"
+
+        # this test fails if the alias match a colliding name in core
+        with pytest.raises(AttributeError):
+            getattr(alt.core, alias)
+
+        # this test fails if the alias match a colliding name in channels
+        with pytest.raises(AttributeError):
+            getattr(alt.channels, alias)
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
index 74ea431c..de2ef6fe 100644
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -7,7 +7,7 @@ import pytest
 
 import numpy as np
 
-from ..schemapi import (
+from altair.utils.schemapi import (
     UndefinedType,
     SchemaBase,
     Undefined,