Skip to content

Commit eb6febd

Browse files
authored
fix(typing): Improve Then annotations, autocompletion, docs (#3567)
1 parent fa420f3 commit eb6febd

File tree

9 files changed

+570
-253
lines changed

9 files changed

+570
-253
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
@@ -28,7 +28,7 @@
2828
from narwhals.dependencies import get_polars, is_pandas_dataframe
2929
from narwhals.typing import IntoDataFrame
3030

31-
from altair.utils.schemapi import SchemaBase, Undefined
31+
from altair.utils.schemapi import SchemaBase, SchemaLike, Undefined
3232

3333
if sys.version_info >= (3, 12):
3434
from typing import Protocol, TypeAliasType, runtime_checkable
@@ -869,6 +869,8 @@ def _wrap_in_channel(self, obj: Any, encoding: str, /):
869869
obj = {"shorthand": obj}
870870
elif isinstance(obj, (list, tuple)):
871871
return [self._wrap_in_channel(el, encoding) for el in obj]
872+
elif isinstance(obj, SchemaLike):
873+
obj = obj.to_dict()
872874
if channel := self.name_to_channel.get(encoding):
873875
tp = channel["value" if "value" in obj else "field"]
874876
try:

altair/utils/schemapi.py

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
Any,
1919
Dict,
2020
Final,
21+
Generic,
2122
Iterable,
2223
Iterator,
2324
List,
2425
Literal,
26+
Mapping,
2527
Sequence,
2628
TypeVar,
2729
Union,
@@ -41,6 +43,11 @@
4143
# not yet be fully instantiated in case your code is being executed during import time
4244
from altair import vegalite
4345

46+
if sys.version_info >= (3, 12):
47+
from typing import Protocol, TypeAliasType, runtime_checkable
48+
else:
49+
from typing_extensions import Protocol, TypeAliasType, runtime_checkable
50+
4451
if TYPE_CHECKING:
4552
from types import ModuleType
4653
from typing import ClassVar
@@ -524,11 +531,7 @@ def _todict(obj: Any, context: dict[str, Any] | None, np_opt: Any, pd_opt: Any)
524531
for k, v in obj.items()
525532
if v is not Undefined
526533
}
527-
elif (
528-
hasattr(obj, "to_dict")
529-
and (module_name := obj.__module__)
530-
and module_name.startswith("altair")
531-
):
534+
elif isinstance(obj, SchemaLike):
532535
return obj.to_dict()
533536
elif pd_opt is not None and isinstance(obj, pd_opt.Timestamp):
534537
return pd_opt.Timestamp(obj).isoformat()
@@ -789,6 +792,95 @@ def _get_default_error_message(
789792
return message
790793

791794

795+
_JSON_VT_co = TypeVar(
796+
"_JSON_VT_co",
797+
Literal["string"],
798+
Literal["object"],
799+
Literal["array"],
800+
covariant=True,
801+
)
802+
"""
803+
One of a subset of JSON Schema `primitive types`_:
804+
805+
["string", "object", "array"]
806+
807+
.. _primitive types:
808+
https://json-schema.org/draft-07/json-schema-validation#rfc.section.6.1.1
809+
"""
810+
811+
_TypeMap = TypeAliasType(
812+
"_TypeMap", Mapping[Literal["type"], _JSON_VT_co], type_params=(_JSON_VT_co,)
813+
)
814+
"""
815+
A single item JSON Schema using the `type`_ keyword.
816+
817+
This may represent **one of**:
818+
819+
{"type": "string"}
820+
{"type": "object"}
821+
{"type": "array"}
822+
823+
.. _type:
824+
https://json-schema.org/understanding-json-schema/reference/type
825+
"""
826+
827+
# NOTE: Type checkers want opposing things:
828+
# - `mypy` : Covariant type variable "_JSON_VT_co" used in protocol where invariant one is expected [misc]
829+
# - `pyright`: Type variable "_JSON_VT_co" used in generic protocol "SchemaLike" should be covariant [reportInvalidTypeVarUse]
830+
# Siding with `pyright` as this is consistent with https://github.com/python/typeshed/blob/9e506eb5e8fc2823db8c60ad561b1145ff114947/stdlib/typing.pyi#L690
831+
832+
833+
@runtime_checkable
834+
class SchemaLike(Generic[_JSON_VT_co], Protocol): # type: ignore[misc]
835+
"""
836+
Represents ``altair`` classes which *may* not derive ``SchemaBase``.
837+
838+
Attributes
839+
----------
840+
_schema
841+
A single item JSON Schema using the `type`_ keyword.
842+
843+
Notes
844+
-----
845+
Should be kept tightly defined to the **minimum** requirements for:
846+
- Converting into a form that can be validated by `jsonschema`_.
847+
- Avoiding calling ``.to_dict()`` on a class external to ``altair``.
848+
- ``_schema`` is more accurately described as a ``ClassVar``
849+
- See `discussion`_ for blocking issue.
850+
851+
.. _jsonschema:
852+
https://github.com/python-jsonschema/jsonschema
853+
.. _type:
854+
https://json-schema.org/understanding-json-schema/reference/type
855+
.. _discussion:
856+
https://github.com/python/typing/discussions/1424
857+
"""
858+
859+
_schema: _TypeMap[_JSON_VT_co]
860+
861+
def to_dict(self, *args, **kwds) -> Any: ...
862+
863+
864+
@runtime_checkable
865+
class ConditionLike(SchemaLike[Literal["object"]], Protocol):
866+
"""
867+
Represents the wrapped state of a conditional encoding or property.
868+
869+
Attributes
870+
----------
871+
condition
872+
One or more (predicate, statement) pairs which each form a condition.
873+
874+
Notes
875+
-----
876+
- Can be extended with additional conditions.
877+
- *Does not* define a default value, but can be finalized with one.
878+
"""
879+
880+
condition: Any
881+
_schema: _TypeMap[Literal["object"]] = {"type": "object"}
882+
883+
792884
class UndefinedType:
793885
"""A singleton object for marking undefined parameters."""
794886

0 commit comments

Comments
 (0)