Skip to content

Commit edd34fd

Browse files
committed
Merge remote-tracking branch 'origin/main' into perf-validation
2 parents 0c829fb + eb6febd commit edd34fd

File tree

10 files changed

+578
-249
lines changed

10 files changed

+578
-249
lines changed

altair/utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
from .deprecation import AltairDeprecationWarning, deprecated, deprecated_warn
1313
from .html import spec_to_html
1414
from .plugin_registry import PluginRegistry
15-
from .schemapi import Optional, SchemaBase, Undefined, is_undefined
15+
from .schemapi import Optional, SchemaBase, SchemaLike, Undefined, is_undefined
1616

1717
__all__ = (
1818
"SHORTHAND_KEYS",
1919
"AltairDeprecationWarning",
2020
"Optional",
2121
"PluginRegistry",
2222
"SchemaBase",
23+
"SchemaLike",
2324
"Undefined",
2425
"deprecated",
2526
"deprecated_warn",

altair/utils/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from narwhals.dependencies import get_polars, is_pandas_dataframe
2828
from narwhals.typing import IntoDataFrame
2929

30-
from altair.utils.schemapi import SchemaBase, Undefined
30+
from altair.utils.schemapi import SchemaBase, SchemaLike, Undefined
3131

3232
if sys.version_info >= (3, 12):
3333
from typing import Protocol, TypeAliasType, runtime_checkable
@@ -868,6 +868,8 @@ def _wrap_in_channel(self, obj: Any, encoding: str, /):
868868
obj = {"shorthand": obj}
869869
elif isinstance(obj, (list, tuple)):
870870
return [self._wrap_in_channel(el, encoding) for el in obj]
871+
elif isinstance(obj, SchemaLike):
872+
obj = obj.to_dict()
871873
if channel := self.name_to_channel.get(encoding):
872874
tp = channel["value" if "value" in obj else "field"]
873875
# Don't force validation here; some objects won't be valid until

altair/utils/schemapi.py

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
TYPE_CHECKING,
1818
Any,
1919
Dict,
20+
Final,
21+
Generic,
2022
Iterable,
2123
List,
24+
Literal,
2225
Mapping,
2326
Sequence,
2427
TypeVar,
@@ -33,9 +36,14 @@
3336
from jsonschema import ValidationError
3437
from packaging.version import Version
3538

39+
if sys.version_info >= (3, 12):
40+
from typing import Protocol, TypeAliasType, runtime_checkable
41+
else:
42+
from typing_extensions import Protocol, TypeAliasType, runtime_checkable
43+
3644
if TYPE_CHECKING:
3745
from types import ModuleType
38-
from typing import Callable, ClassVar, Final, Iterator, KeysView, Literal
46+
from typing import Callable, ClassVar, Final, Iterator, KeysView
3947

4048
from jsonschema.protocols import Validator, _JsonParameter
4149

@@ -677,11 +685,7 @@ def _todict(obj: Any, context: dict[str, Any] | None, np_opt: Any, pd_opt: Any)
677685
for k, v in obj.items()
678686
if v is not Undefined
679687
}
680-
elif (
681-
hasattr(obj, "to_dict")
682-
and (module_name := obj.__module__)
683-
and module_name.startswith("altair")
684-
):
688+
elif isinstance(obj, SchemaLike):
685689
return obj.to_dict()
686690
elif pd_opt is not None and isinstance(obj, pd_opt.Timestamp):
687691
return pd_opt.Timestamp(obj).isoformat()
@@ -912,6 +916,95 @@ def _get_default_error_message(
912916
return message.strip()
913917

914918

919+
_JSON_VT_co = TypeVar(
920+
"_JSON_VT_co",
921+
Literal["string"],
922+
Literal["object"],
923+
Literal["array"],
924+
covariant=True,
925+
)
926+
"""
927+
One of a subset of JSON Schema `primitive types`_:
928+
929+
["string", "object", "array"]
930+
931+
.. _primitive types:
932+
https://json-schema.org/draft-07/json-schema-validation#rfc.section.6.1.1
933+
"""
934+
935+
_TypeMap = TypeAliasType(
936+
"_TypeMap", Mapping[Literal["type"], _JSON_VT_co], type_params=(_JSON_VT_co,)
937+
)
938+
"""
939+
A single item JSON Schema using the `type`_ keyword.
940+
941+
This may represent **one of**:
942+
943+
{"type": "string"}
944+
{"type": "object"}
945+
{"type": "array"}
946+
947+
.. _type:
948+
https://json-schema.org/understanding-json-schema/reference/type
949+
"""
950+
951+
# NOTE: Type checkers want opposing things:
952+
# - `mypy` : Covariant type variable "_JSON_VT_co" used in protocol where invariant one is expected [misc]
953+
# - `pyright`: Type variable "_JSON_VT_co" used in generic protocol "SchemaLike" should be covariant [reportInvalidTypeVarUse]
954+
# Siding with `pyright` as this is consistent with https://github.com/python/typeshed/blob/9e506eb5e8fc2823db8c60ad561b1145ff114947/stdlib/typing.pyi#L690
955+
956+
957+
@runtime_checkable
958+
class SchemaLike(Generic[_JSON_VT_co], Protocol): # type: ignore[misc]
959+
"""
960+
Represents ``altair`` classes which *may* not derive ``SchemaBase``.
961+
962+
Attributes
963+
----------
964+
_schema
965+
A single item JSON Schema using the `type`_ keyword.
966+
967+
Notes
968+
-----
969+
Should be kept tightly defined to the **minimum** requirements for:
970+
- Converting into a form that can be validated by `jsonschema`_.
971+
- Avoiding calling ``.to_dict()`` on a class external to ``altair``.
972+
- ``_schema`` is more accurately described as a ``ClassVar``
973+
- See `discussion`_ for blocking issue.
974+
975+
.. _jsonschema:
976+
https://github.com/python-jsonschema/jsonschema
977+
.. _type:
978+
https://json-schema.org/understanding-json-schema/reference/type
979+
.. _discussion:
980+
https://github.com/python/typing/discussions/1424
981+
"""
982+
983+
_schema: _TypeMap[_JSON_VT_co]
984+
985+
def to_dict(self, *args, **kwds) -> Any: ...
986+
987+
988+
@runtime_checkable
989+
class ConditionLike(SchemaLike[Literal["object"]], Protocol):
990+
"""
991+
Represents the wrapped state of a conditional encoding or property.
992+
993+
Attributes
994+
----------
995+
condition
996+
One or more (predicate, statement) pairs which each form a condition.
997+
998+
Notes
999+
-----
1000+
- Can be extended with additional conditions.
1001+
- *Does not* define a default value, but can be finalized with one.
1002+
"""
1003+
1004+
condition: Any
1005+
_schema: _TypeMap[Literal["object"]] = {"type": "object"}
1006+
1007+
9151008
class UndefinedType:
9161009
"""A singleton object for marking undefined parameters."""
9171010

0 commit comments

Comments
 (0)