Skip to content

Commit

Permalink
Merge branch 'main' into vega-datasets
Browse files Browse the repository at this point in the history
  • Loading branch information
dangotbanned authored Oct 12, 2024
2 parents 511a845 + 8135911 commit c76cfd4
Show file tree
Hide file tree
Showing 10 changed files with 2,393 additions and 745 deletions.
1,789 changes: 1,141 additions & 648 deletions altair/expr/__init__.py

Large diffs are not rendered by default.

46 changes: 42 additions & 4 deletions altair/vegalite/v5/schema/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -25256,10 +25256,48 @@ def encode(
yOffset : str, :class:`YOffset`, Dict, :class:`YOffsetDatum`, :class:`YOffsetValue`
Offset of y-position of the marks
"""
# Compat prep for `infer_encoding_types` signature
kwargs = locals()
kwargs.pop("self")
args = kwargs.pop("args")
kwargs = {
"angle": angle,
"color": color,
"column": column,
"description": description,
"detail": detail,
"facet": facet,
"fill": fill,
"fillOpacity": fillOpacity,
"href": href,
"key": key,
"latitude": latitude,
"latitude2": latitude2,
"longitude": longitude,
"longitude2": longitude2,
"opacity": opacity,
"order": order,
"radius": radius,
"radius2": radius2,
"row": row,
"shape": shape,
"size": size,
"stroke": stroke,
"strokeDash": strokeDash,
"strokeOpacity": strokeOpacity,
"strokeWidth": strokeWidth,
"text": text,
"theta": theta,
"theta2": theta2,
"tooltip": tooltip,
"url": url,
"x": x,
"x2": x2,
"xError": xError,
"xError2": xError2,
"xOffset": xOffset,
"y": y,
"y2": y2,
"yError": yError,
"yError2": yError2,
"yOffset": yOffset,
}
if args:
kwargs = {k: v for k, v in kwargs.items() if v is not Undefined}

Expand Down
60 changes: 39 additions & 21 deletions tests/expr/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

import operator
import sys
from inspect import classify_class_attrs, getmembers
from typing import Any, Iterator
from inspect import classify_class_attrs, getmembers, signature
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, TypeVar, cast

import pytest
from jsonschema.exceptions import ValidationError

from altair import datum, expr, ExprRef
from altair.expr import _ConstExpressionType
from altair.expr import _ExprMeta
from altair.expr.core import Expression, GetAttrExpression

if TYPE_CHECKING:
from inspect import _IntrospectableCallable

T = TypeVar("T")

# This maps vega expression function names to the Python name
VEGA_REMAP = {"if_": "if"}
Expand All @@ -19,20 +25,29 @@ def _is_property(obj: Any, /) -> bool:
return isinstance(obj, property)


def _get_classmethod_names(tp: type[Any], /) -> Iterator[str]:
for m in classify_class_attrs(tp):
if m.kind == "class method" and m.defining_class is tp:
yield m.name
def _get_property_names(tp: type[Any], /) -> Iterator[str]:
for nm, _ in getmembers(tp, _is_property):
yield nm


def _remap_classmethod_names(tp: type[Any], /) -> Iterator[tuple[str, str]]:
for name in _get_classmethod_names(tp):
yield VEGA_REMAP.get(name, name), name
def signature_n_params(
obj: _IntrospectableCallable,
/,
*,
exclude: Iterable[str] = frozenset(("cls", "self")),
) -> int:
sig = signature(obj)
return len(set(sig.parameters).difference(exclude))


def _get_property_names(tp: type[Any], /) -> Iterator[str]:
for nm, _ in getmembers(tp, _is_property):
yield nm
def _iter_classmethod_specs(
tp: type[T], /
) -> Iterator[tuple[str, Callable[..., Expression], int]]:
for m in classify_class_attrs(tp):
if m.kind == "class method" and m.defining_class is tp:
name = m.name
fn = cast("classmethod[T, ..., Expression]", m.object).__func__
yield (VEGA_REMAP.get(name, name), fn.__get__(tp), signature_n_params(fn))


def test_unary_operations():
Expand Down Expand Up @@ -86,23 +101,26 @@ def test_abs():
assert repr(z) == "abs(datum.xxx)"


@pytest.mark.parametrize(("veganame", "methodname"), _remap_classmethod_names(expr))
def test_expr_funcs(veganame: str, methodname: str):
"""Test all functions defined in expr.funcs."""
func = getattr(expr, methodname)
z = func(datum.xxx)
assert repr(z) == f"{veganame}(datum.xxx)"
@pytest.mark.parametrize(("veganame", "fn", "n_params"), _iter_classmethod_specs(expr))
def test_expr_methods(
veganame: str, fn: Callable[..., Expression], n_params: int
) -> None:
datum_names = [f"col_{n}" for n in range(n_params)]
datum_args = ",".join(f"datum.{nm}" for nm in datum_names)

fn_call = fn(*(GetAttrExpression("datum", nm) for nm in datum_names))
assert repr(fn_call) == f"{veganame}({datum_args})"


@pytest.mark.parametrize("constname", _get_property_names(_ConstExpressionType))
@pytest.mark.parametrize("constname", _get_property_names(_ExprMeta))
def test_expr_consts(constname: str):
"""Test all constants defined in expr.consts."""
const = getattr(expr, constname)
z = const * datum.xxx
assert repr(z) == f"({constname} * datum.xxx)"


@pytest.mark.parametrize("constname", _get_property_names(_ConstExpressionType))
@pytest.mark.parametrize("constname", _get_property_names(_ExprMeta))
def test_expr_consts_immutable(constname: str):
"""Ensure e.g `alt.expr.PI = 2` is prevented."""
if sys.version_info >= (3, 11):
Expand Down
14 changes: 10 additions & 4 deletions tests/vegalite/v5/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,13 @@ def test_when_labels_position_based_on_condition() -> None:
# `mypy` will flag structural errors here
cond = when["condition"][0]
otherwise = when["value"]
param_color_py_when = alt.param(
expr=alt.expr.if_(cond["test"], cond["value"], otherwise)
)

# TODO: Open an issue on making `OperatorMixin` generic
# Something like this would be used as the return type for all `__dunder__` methods:
# R = TypeVar("R", Expression, SelectionPredicateComposition)
test = cond["test"]
assert not isinstance(test, alt.PredicateComposition)
param_color_py_when = alt.param(expr=alt.expr.if_(test, cond["value"], otherwise))
lhs_param = param_color_py_expr.param
rhs_param = param_color_py_when.param
assert isinstance(lhs_param, alt.VariableParameter)
Expand Down Expand Up @@ -600,7 +604,9 @@ def test_when_expressions_inside_parameters() -> None:
cond = when_then_otherwise["condition"][0]
otherwise = when_then_otherwise["value"]
expected = alt.expr.if_(alt.datum.b >= 0, 10, -20)
actual = alt.expr.if_(cond["test"], cond["value"], otherwise)
test = cond["test"]
assert not isinstance(test, alt.PredicateComposition)
actual = alt.expr.if_(test, cond["value"], otherwise)
assert expected == actual

text_conditioned = bar.mark_text(
Expand Down
9 changes: 8 additions & 1 deletion tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from tools import generate_api_docs, generate_schema_wrapper, schemapi, update_init_file
from tools import (
generate_api_docs,
generate_schema_wrapper,
markup,
schemapi,
update_init_file,
)

__all__ = [
"generate_api_docs",
"generate_schema_wrapper",
"markup",
"schemapi",
"update_init_file",
]
30 changes: 17 additions & 13 deletions tools/generate_schema_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@
sys.path.insert(0, str(Path.cwd()))


from tools.schemapi import ( # noqa: F401
CodeSnippet,
SchemaInfo,
arg_invalid_kwds,
arg_kwds,
arg_required_kwds,
codegen,
)
from tools.markup import rst_syntax_for_class
from tools.schemapi import CodeSnippet, SchemaInfo, arg_kwds, arg_required_kwds, codegen
from tools.schemapi.utils import (
SchemaProperties,
TypeAliasTracer,
Expand All @@ -37,16 +31,17 @@
import_typing_extensions,
indent_docstring,
resolve_references,
rst_syntax_for_class,
ruff_format_py,
ruff_write_lint_format_str,
spell_literal,
)
from tools.vega_expr import write_expr_module

if TYPE_CHECKING:
from tools.schemapi.codegen import ArgInfo, AttrGetter
from vl_convert import VegaThemes


SCHEMA_VERSION: Final = "v5.20.1"


Expand All @@ -60,8 +55,14 @@
"""

SCHEMA_URL_TEMPLATE: Final = "https://vega.github.io/schema/{library}/{version}.json"
VL_PACKAGE_TEMPLATE = (
"https://raw.githubusercontent.com/vega/vega-lite/refs/tags/{version}/package.json"
)
SCHEMA_FILE = "vega-lite-schema.json"
THEMES_FILE = "vega-themes.json"
EXPR_FILE: Path = (
Path(__file__).parent / ".." / "altair" / "expr" / "__init__.py"
).resolve()

CHANNEL_MYPY_IGNORE_STATEMENTS: Final = """\
# These errors need to be ignored as they come from the overload methods
Expand Down Expand Up @@ -277,10 +278,7 @@ class _EncodingMixin:
def encode(self, *args: Any, {method_args}) -> Self:
"""Map properties of the data to visual properties of the chart (see :class:`FacetedEncoding`)
{docstring}"""
# Compat prep for `infer_encoding_types` signature
kwargs = locals()
kwargs.pop("self")
args = kwargs.pop("args")
kwargs = {dict_literal}
if args:
kwargs = {{k: v for k, v in kwargs.items() if v is not Undefined}}
Expand Down Expand Up @@ -1180,6 +1178,7 @@ def generate_encoding_artifacts(
method: str = fmt_method.format(
method_args=", ".join(signature_args),
docstring=indent_docstring(signature_doc_params, indent_level=8, lstrip=False),
dict_literal="{" + ", ".join(f"{kwd!r}:{kwd}" for kwd in channel_infos) + "}",
)
typed_dict = generate_typed_dict(
facet_encoding,
Expand Down Expand Up @@ -1207,6 +1206,11 @@ def main() -> None:
args = parser.parse_args()
copy_schemapi_util()
vegalite_main(args.skip_download)
write_expr_module(
vlc.get_vega_version(),
output=EXPR_FILE,
header=HEADER_COMMENT,
)

# The modules below are imported after the generation of the new schema files
# as these modules import Altair. This allows them to use the new changes
Expand Down
Loading

0 comments on commit c76cfd4

Please sign in to comment.