Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Generate expr method signatures, docs #3600

Merged
merged 86 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
a60e8cc
feat: Adds `altair.tools.schemapi.vega_expr`
dangotbanned Sep 19, 2024
e7dca1c
chore: Add imports
dangotbanned Sep 19, 2024
49d328d
feat: Add download/ast parse wrapper
dangotbanned Sep 19, 2024
11d759d
feat: Define constants
dangotbanned Sep 19, 2024
0109c8c
feat: Define some `re` patterns
dangotbanned Sep 19, 2024
1f736be
feat: Extend `utils.RSTParse` to support external tokens
dangotbanned Sep 19, 2024
e26083a
feat: Adds `VegaExprParam`
dangotbanned Sep 19, 2024
6bb4c27
feat: Adds `VegaExprNode`
dangotbanned Sep 19, 2024
d1bf708
feat: Adds `parse_expressions`
dangotbanned Sep 19, 2024
3e62bd9
feat(DRAFT): Add `VegaExprNode._doc_post_process`
dangotbanned Sep 19, 2024
67c6a1e
fix: Don't include `"*args"` in `parameter_names`
dangotbanned Sep 19, 2024
815404e
feat(DRAFT): Adds `VegaExprNode.to_signature`
dangotbanned Sep 19, 2024
51e569e
feat: Finish `to_signature` params component
dangotbanned Sep 19, 2024
b3006bc
feat(DRAFT): Add `render_expr_method`
dangotbanned Sep 19, 2024
54f7db0
refactor: `FunctionExpression` -> `Expression` for annotation only
dangotbanned Sep 19, 2024
328cc98
feat: Handle `(expr|SchemaBase).copy` conflict
dangotbanned Sep 20, 2024
6e1897d
fix: Disable `string_` overloads
dangotbanned Sep 20, 2024
fb33243
fix: Classify more cases as variadic
dangotbanned Sep 20, 2024
1ce1567
fix: Use quotes for `node.name`
dangotbanned Sep 20, 2024
b88221f
feat(DRAFT): Full `expr` module generation
dangotbanned Sep 20, 2024
0a09936
feat: Strip include tags from docs
dangotbanned Sep 21, 2024
9377598
feat: Split doc summary, wrap body
dangotbanned Sep 21, 2024
8e88cd2
chore: Remove some notes
dangotbanned Sep 21, 2024
f5b3717
fix: Replace `vega` docs relative links
dangotbanned Sep 21, 2024
7ec0505
feat: Convert inline links to references
dangotbanned Sep 21, 2024
29f08dc
refactor: Rewrite `VegaExprNode` as a regular class
dangotbanned Sep 23, 2024
f6cc776
feat: Replace all `Vega` function refs in code blocks
dangotbanned Sep 23, 2024
321a0cd
docs: Update doc for `is_overloaded`
dangotbanned Sep 23, 2024
90f7b0a
docs: Add note to `_doc_fmt`
dangotbanned Sep 23, 2024
36ac45a
refactor: Simplify `VegaExprNode.is_callable`
dangotbanned Sep 23, 2024
56dfa27
refactor: Simplify `VegaExprNode._split_signature_tokens`
dangotbanned Sep 23, 2024
9cab1a1
docs: Add doc for `deep_split_punctuation`
dangotbanned Sep 23, 2024
283eff6
Merge remote-tracking branch 'upstream/main' into vega-expr-gen
dangotbanned Sep 23, 2024
497f2fd
feat: Refine `ReplaceMany`
dangotbanned Sep 24, 2024
b0b1952
docs: Add doc for `ReplaceMany`
dangotbanned Sep 24, 2024
2d5271e
refactor: Replace `VegaExprNode.parameter_names` property with a filt…
dangotbanned Sep 24, 2024
849f428
refactor: Tidy up `"clamprange"` -> `"clampRange"` special case
dangotbanned Sep 24, 2024
bed8d0f
refactor: Reorder `with_` methods, add docs
dangotbanned Sep 24, 2024
264e4a6
refactor: Rename reorder `_split_markers`
dangotbanned Sep 24, 2024
7c24e46
refactor: Move url expansion to renderer
dangotbanned Sep 24, 2024
051bcb2
feat: Adds initial `vega_expr` api
dangotbanned Sep 24, 2024
5e75051
build: run `generate-schema-wrapper`
dangotbanned Sep 24, 2024
222d03e
chore: Add note to `test_expr.py`
dangotbanned Sep 24, 2024
f7a47a5
fix: Add trailing comma to single arg methods
dangotbanned Sep 24, 2024
9238fb6
test: Adds `test_dummy_expr_funcs`
dangotbanned Sep 24, 2024
7311fdf
test: Refactor `test_expr`
dangotbanned Sep 25, 2024
6077075
fix: Move some imports to `TYPE_CHECKING` block
dangotbanned Sep 25, 2024
0094596
chore: Delete `expr.dummy.py`
dangotbanned Sep 25, 2024
16a92a4
test: Remove old `test_expr` functions
dangotbanned Sep 25, 2024
17da9d0
build: Generate new `alt.expr.__init__.py`
dangotbanned Sep 25, 2024
21d13e7
fix(typing): Resolve some revealed issues
dangotbanned Sep 25, 2024
7364605
Merge remote-tracking branch 'upstream/main' into vega-expr-gen
dangotbanned Sep 25, 2024
7e0db68
refactor: Move `.md`, `.rst` utils to `tools.markup.py`
dangotbanned Sep 25, 2024
9aaf862
chore: Remove debugging code
dangotbanned Sep 25, 2024
9215d7d
refactor: Replace `render_expr_method` with a method
dangotbanned Sep 25, 2024
7a10e3d
refactor: Add signature template, rename others
dangotbanned Sep 25, 2024
e3273f6
docs: Update metaclass description
dangotbanned Sep 25, 2024
1a7d241
refactor: Rename `_ConstExpressionType` -> `_ExprMeta`
dangotbanned Sep 25, 2024
cfb676f
refactor: Align `VegaExpr(Node|Param)` apis
dangotbanned Sep 25, 2024
6fd32e9
refactor: Factor out to `italics_to_backticks`
dangotbanned Sep 26, 2024
8fb9341
Merge branch 'main' into vega-expr-gen
dangotbanned Sep 26, 2024
b2aeecb
feat: Strip template markup earlier
dangotbanned Sep 26, 2024
fde31e2
refactor: Move `unescape` to `render_tokens`
dangotbanned Sep 26, 2024
d4d9145
refactor: Move `_doc_post_process` -> `with_doc`
dangotbanned Sep 26, 2024
433611b
refactor: Remove redundant branches in `_override_predicate`
dangotbanned Sep 26, 2024
c2af5f4
refactor: Assign names to literals in `_doc_fmt`
dangotbanned Sep 26, 2024
e7e79c9
docs(typing): Add missing annotations
dangotbanned Sep 26, 2024
e20490b
refactor: Final tidy up, renaming
dangotbanned Sep 26, 2024
5929077
fix: Resolve false negatives for `is_callable`
dangotbanned Sep 27, 2024
d2c32a3
Merge branch 'main' into vega-expr-gen
dangotbanned Sep 27, 2024
2f1d199
fix: Correct indent for `expr.screen` reference
dangotbanned Sep 27, 2024
01e61e3
feat: Bump source to add `alt.expr.sort`
dangotbanned Sep 27, 2024
68786cc
Merge branch 'main' into vega-expr-gen
dangotbanned Sep 27, 2024
d75792b
fix: Pin to `vega` release instead of commit hash
dangotbanned Sep 28, 2024
968114e
docs: Include header comment
dangotbanned Sep 28, 2024
061b066
build(DRAFT): Add `vegalite_to_vega_version`
dangotbanned Sep 28, 2024
d95a573
feat: Adds `vl_convert_to_vega_version`, remove `vegalite_to_vega_ver…
dangotbanned Sep 30, 2024
c91b71c
Merge remote-tracking branch 'upstream/main' into vega-expr-gen
dangotbanned Oct 2, 2024
8294623
Merge branch 'main' into vega-expr-gen
dangotbanned Oct 3, 2024
6f810c8
Merge branch 'main' into vega-expr-gen
dangotbanned Oct 5, 2024
554410c
Merge branch 'main' into vega-expr-gen
dangotbanned Oct 6, 2024
b0f1164
refactor: Use `vlc.get_vega_version()`
dangotbanned Oct 7, 2024
5a08189
revert: Remove `vl_convert_to_vega_version`
dangotbanned Oct 7, 2024
0969b30
refactor: Factor out `download_expressions_md`
dangotbanned Oct 7, 2024
ad1ac8f
refactor: `tools.schemapi.vega_expr.py` -> `tools.vega_expr.py`
dangotbanned Oct 12, 2024
91180d1
docs: Add module docstring for `vega_expr.py`
dangotbanned Oct 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,788 changes: 1,144 additions & 644 deletions altair/expr/__init__.py

Large diffs are not rendered by default.

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 @@ -557,9 +557,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))
assert param_color_py_expr.expr == param_color_py_when.expr

chart = (
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",
]
7 changes: 6 additions & 1 deletion tools/generate_schema_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
sys.path.insert(0, str(Path.cwd()))


from tools.markup import rst_syntax_for_class
from tools.schemapi import ( # noqa: F401
CodeSnippet,
SchemaInfo,
arg_invalid_kwds,
arg_kwds,
arg_required_kwds,
codegen,
write_expr_module,
)
from tools.schemapi.utils import (
SchemaProperties,
Expand All @@ -37,7 +39,6 @@
import_typing_extensions,
indent_docstring,
resolve_references,
rst_syntax_for_class,
ruff_format_py,
ruff_write_lint_format_str,
spell_literal,
Expand All @@ -61,6 +62,9 @@
SCHEMA_URL_TEMPLATE: Final = "https://vega.github.io/schema/{library}/{version}.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 @@ -1206,6 +1210,7 @@ def main() -> None:
args = parser.parse_args()
copy_schemapi_util()
vegalite_main(args.skip_download)
write_expr_module(source_url="static", output=EXPR_FILE)

# 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
140 changes: 140 additions & 0 deletions tools/markup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Tools for working with formats like ``.md``, ``.rst``."""

from __future__ import annotations

import re
from html import unescape
from typing import TYPE_CHECKING, Any, Iterable, Literal

import mistune.util
from mistune import InlineParser as _InlineParser
from mistune import Markdown as _Markdown
from mistune.renderers.rst import RSTRenderer as _RSTRenderer

if TYPE_CHECKING:
import sys
from pathlib import Path

if sys.version_info >= (3, 11):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
from re import Pattern

from mistune import BaseRenderer, BlockParser, BlockState, InlineState

Token: TypeAlias = "dict[str, Any]"

_RE_LINK: Pattern[str] = re.compile(r"(?<=\[)([^\]]+)(?=\]\([^\)]+\))", re.MULTILINE)
_RE_SPECIAL: Pattern[str] = re.compile(r"[*_]{2,3}|`", re.MULTILINE)
_RE_LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})")


class RSTRenderer(_RSTRenderer):
def __init__(self) -> None:
super().__init__()

def inline_html(self, token: Token, state: BlockState) -> str:
html = token["raw"]
return rf"\ :raw-html:`{html}`\ "


class RSTParse(_Markdown):
"""
Minor extension to support partial `ast`_ conversion.

Only need to convert the docstring tokens to `.rst`.

.. _ast:
https://mistune.lepture.com/en/latest/guide.html#abstract-syntax-tree
"""

def __init__(
self,
renderer: BaseRenderer | Literal["ast"] | None,
block: BlockParser | None = None,
inline: _InlineParser | None = None,
plugins=None,
) -> None:
if renderer == "ast":
renderer = None
super().__init__(renderer, block, inline, plugins)

def __call__(self, s: str) -> str:
s = super().__call__(s) # pyright: ignore[reportAssignmentType]
return unescape(s).replace(r"\ ,", ",").replace(r"\ ", " ")

def render_tokens(self, tokens: Iterable[Token], /) -> str:
"""
Render ast tokens originating from another parser.

Parameters
----------
tokens
All tokens will be rendered into a single `.rst` string
"""
if self.renderer is None:
msg = "Unable to render tokens without a renderer."
raise TypeError(msg)
state = self.block.state_cls()
s = self.renderer(self._iter_render(tokens, state), state)
return mistune.util.unescape(s)


class RSTParseVegaLite(RSTParse):
def __init__(
self,
renderer: RSTRenderer | None = None,
block: BlockParser | None = None,
inline: _InlineParser | None = None,
plugins=None,
) -> None:
super().__init__(renderer or RSTRenderer(), block, inline, plugins)

def __call__(self, s: str) -> str:
# remove formatting from links
description = "".join(
_RE_SPECIAL.sub("", d) if i % 2 else d
for i, d in enumerate(_RE_LINK.split(s))
)

description = super().__call__(description)
# Some entries in the Vega-Lite schema miss the second occurence of '__'
description = description.replace("__Default value: ", "__Default value:__ ")
# Links to the vega-lite documentation cannot be relative but instead need to
# contain the full URL.
description = description.replace(
"types#datetime", "https://vega.github.io/vega-lite/docs/datetime.html"
)
# Fixing ambiguous unicode, RUF001 produces RUF002 in docs
description = description.replace("’", "'") # noqa: RUF001 [RIGHT SINGLE QUOTATION MARK]
description = description.replace("–", "-") # noqa: RUF001 [EN DASH]
description = description.replace(" ", " ") # noqa: RUF001 [NO-BREAK SPACE]
return description.strip()


class InlineParser(_InlineParser):
def __init__(self, hard_wrap: bool = False) -> None:
super().__init__(hard_wrap)

def process_text(self, text: str, state: InlineState) -> None:
"""
Removes `liquid`_ templating markup.

.. _liquid:
https://shopify.github.io/liquid/
"""
state.append_token({"type": "text", "raw": _RE_LIQUID_INCLUDE.sub(r"", text)})


def read_ast_tokens(source: Path, /) -> list[Token]:
"""
Read from ``source``, drop ``BlockState``.

Factored out to provide accurate typing.
"""
return _Markdown(renderer=None, inline=InlineParser()).read(source)[0]


def rst_syntax_for_class(class_name: str) -> str:
return f":class:`{class_name}`"
2 changes: 2 additions & 0 deletions tools/schemapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from tools.schemapi.schemapi import SchemaBase, Undefined
from tools.schemapi.utils import OneOrSeq, SchemaInfo
from tools.schemapi.vega_expr import write_expr_module

__all__ = [
"CodeSnippet",
Expand All @@ -21,4 +22,5 @@
"arg_required_kwds",
"codegen",
"utils",
"write_expr_module",
]
Loading