From a60e8cc28a481f14c5d1b168bc1aad8dcf64b3c3 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:14:19 +0100 Subject: [PATCH 01/77] feat: Adds `altair.tools.schemapi.vega_expr` --- tools/schemapi/vega_expr.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tools/schemapi/vega_expr.py diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py new file mode 100644 index 000000000..9d48db4f9 --- /dev/null +++ b/tools/schemapi/vega_expr.py @@ -0,0 +1 @@ +from __future__ import annotations From e7dca1c3130a6289ab68e62cad53902b846b55b1 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:20:00 +0100 Subject: [PATCH 02/77] chore: Add imports --- tools/schemapi/vega_expr.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 9d48db4f9..78e5532d7 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -1 +1,27 @@ from __future__ import annotations + +import dataclasses +import functools +import keyword +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload +from urllib import request + +import mistune +import mistune.util + +from tools.schemapi.utils import RSTParse, RSTRenderer + +if TYPE_CHECKING: + import sys + from re import Pattern + + from mistune import BaseRenderer, BlockParser, BlockState, InlineParser + + if sys.version_info >= (3, 11): + from typing import LiteralString, Self, TypeAlias + else: + from typing_extensions import LiteralString, Self, TypeAlias + Token: TypeAlias = "dict[str, Any]" + From 49d328d05f3340ee058c92f33f9e27c599518123 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:25:33 +0100 Subject: [PATCH 03/77] feat: Add download/ast parse wrapper --- tools/schemapi/vega_expr.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 78e5532d7..81e6d0b47 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -17,7 +17,7 @@ import sys from re import Pattern - from mistune import BaseRenderer, BlockParser, BlockState, InlineParser + from mistune import BaseRenderer, BlockParser, InlineParser if sys.version_info >= (3, 11): from typing import LiteralString, Self, TypeAlias @@ -25,3 +25,30 @@ from typing_extensions import LiteralString, Self, TypeAlias Token: TypeAlias = "dict[str, Any]" + +EXPRESSIONS_URL = ( + "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" +) + +def download_expressions_md(url: str, /) -> Path: + """Download to a temporary file, return that as a ``pathlib.Path``.""" + tmp, _ = request.urlretrieve(url) + fp = Path(tmp) + if not fp.exists(): + msg = ( + f"Expressions download failed: {fp!s}.\n\n" + f"Try manually accessing resource: {url!r}" + ) + raise FileNotFoundError(msg) + else: + return fp + + +def read_tokens(source: Path, /) -> list[Any]: + """ + Read from ``source``, drop ``BlockState``. + + Factored out to provide accurate typing. + """ + return mistune.create_markdown(renderer="ast").read(source)[0] + From 11d759d6c9fdd8110a7082f8c2b0aa3f989cd3bf Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:26:57 +0100 Subject: [PATCH 04/77] feat: Define constants --- tools/schemapi/vega_expr.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 81e6d0b47..47ef97308 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -29,6 +29,12 @@ EXPRESSIONS_URL = ( "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" ) +TYPE: Literal[r"type"] = "type" +RAW: Literal["raw"] = "raw" +SOFTBREAK: Literal["softbreak"] = "softbreak" +TEXT: Literal["text"] = "text" +CHILDREN: Literal["children"] = "children" + def download_expressions_md(url: str, /) -> Path: """Download to a temporary file, return that as a ``pathlib.Path``.""" From 0109c8c73f3b2e4f82130644d540f43544b861ac Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:29:10 +0100 Subject: [PATCH 05/77] feat: Define some `re` patterns --- tools/schemapi/vega_expr.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 47ef97308..c4ddcd91a 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -29,6 +29,10 @@ EXPRESSIONS_URL = ( "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" ) + +FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") +LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") + TYPE: Literal[r"type"] = "type" RAW: Literal["raw"] = "raw" SOFTBREAK: Literal["softbreak"] = "softbreak" @@ -58,3 +62,12 @@ def read_tokens(source: Path, /) -> list[Any]: """ return mistune.create_markdown(renderer="ast").read(source)[0] + +def strip_include_tag(s: str, /) -> str: + """ + Removes `liquid`_ templating markup. + + .. _liquid: + https://shopify.github.io/liquid/ + """ + return LIQUID_INCLUDE.sub(r"", s) From 1f736be91c1d74d20a7fa9d38f19c6616c2c1989 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:32:38 +0100 Subject: [PATCH 06/77] feat: Extend `utils.RSTParse` to support external tokens --- tools/schemapi/vega_expr.py | 48 ++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c4ddcd91a..5a359ae17 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -11,7 +11,8 @@ import mistune import mistune.util -from tools.schemapi.utils import RSTParse, RSTRenderer +from tools.schemapi.utils import RSTParse as _RSTParse +from tools.schemapi.utils import RSTRenderer if TYPE_CHECKING: import sys @@ -71,3 +72,48 @@ def strip_include_tag(s: str, /) -> str: https://shopify.github.io/liquid/ """ return LIQUID_INCLUDE.sub(r"", s) + + +class RSTParse(_RSTParse): + """ + Minor extension to support partial `ast`_ conversion. + + Only need to convert the docstring tokens to `.rst`. + + NOTE + ---- + Once `PR`_ is merged, move this to the parent class and rename + + .. _ast: + https://mistune.lepture.com/en/latest/guide.html#abstract-syntax-tree + .. _PR: + https://github.com/vega/altair/pull/3536 + """ + + def __init__( + self, + renderer: BaseRenderer, + block: BlockParser | None = None, + inline: InlineParser | None = None, + plugins=None, + ) -> None: + super().__init__(renderer, block, inline, plugins) + if self.renderer is None: + msg = "Must provide a renderer, got `None`" + raise TypeError(msg) + self.renderer: BaseRenderer + + def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: + """ + Render ast tokens originating from another parser. + + Parameters + ---------- + tokens + All tokens will be rendered into a single `.rst` string + """ + state = self.block.state_cls() + return self.renderer(self._iter_render(tokens, state), state) + + +parser: RSTParse = RSTParse(RSTRenderer()) From e26083ac721416bac56a521033b46d5b083fff7b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:34:18 +0100 Subject: [PATCH 07/77] feat: Adds `VegaExprParam` Similar idea to `inspect.Parameter` --- tools/schemapi/vega_expr.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 5a359ae17..cb62eb6bc 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -117,3 +117,27 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: parser: RSTParse = RSTParse(RSTRenderer()) +@dataclasses.dataclass +class VegaExprParam: + name: str + required: bool + variadic: bool = False + + @classmethod + def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: + """Yields an ordered parameter list.""" + is_required: bool = True + for s in raw_texts: + if s not in {"(", ")"}: + if s == "[": + is_required = False + continue + elif s == "]": + is_required = True + continue + elif s.isalnum(): + yield cls(s, required=is_required) + elif s == "...": + yield cls("*args", required=False, variadic=True) + else: + continue From 6bb4c27b9160b9de73936722584b8b70cd7fe34a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:36:45 +0100 Subject: [PATCH 08/77] feat: Adds `VegaExprNode` Similar to `inspect.Signature` --- tools/schemapi/vega_expr.py | 171 ++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index cb62eb6bc..69e6f6fe0 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -117,6 +117,177 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: parser: RSTParse = RSTParse(RSTRenderer()) + +@dataclasses.dataclass +class VegaExprNode: + """ + ``SchemaInfo``-like, but operates on `expressions.md`_. + + .. _expressions.md: + https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md + """ + + name: str + _children: Sequence[Token] = dataclasses.field(repr=False) + doc: str = "" + parameters: list[VegaExprParam] = dataclasses.field(default_factory=list) + + def with_parameters(self) -> Self: + raw_texts = self._split_signature_tokens() + name = next(raw_texts) + # NOTE: Overwriting the with the rendered text + if self.name != name: + self.name = name + self.parameters = list(VegaExprParam.iter_params(raw_texts)) + return self + + def with_doc(self) -> Self: + self.doc = parser.render_tokens(self._doc_tokens()) + return self + + @functools.cached_property + def parameter_names(self) -> frozenset[str]: + if self.parameters: + return frozenset(param.name for param in self.parameters) + else: + msg = ( + f"Cannot provide `parameter_names` until they have been initialized via:\n" + f"{type(self).__name__}.with_parameters()" + ) + raise TypeError(msg) + + @property + def name_safe(self) -> str: + """Use for the method definition, but not when calling internally.""" + return f"{self.name}_" if self.is_keyword() else self.name + + def _split_signature_tokens(self) -> Iterator[str]: + """Very rough splitting/cleaning of relevant token raw text.""" + it = iter(self) + current = next(it) + # NOTE: softbreak(s) denote the line the sig appears on + while current[TYPE] != SOFTBREAK: + current = next(it) + current = next(it) + while current[TYPE] != SOFTBREAK: + # NOTE: This drops html markup tags + if current[TYPE] == TEXT: + clean = strip_include_tag(current[RAW]).strip(", -") + if clean not in {", ", ""}: + yield from VegaExprNode.deep_split_punctuation(clean) + current = next(it, None) + if current is None: + break + + def _doc_tokens(self) -> Sequence[Token]: + """ + Return the slice of `self.children` that contains docstring content. + + Works for 100% of cases. + """ + for idx, item in enumerate(self): + if item[TYPE] == SOFTBREAK and self[idx + 1][TYPE] == TEXT: + return self[idx + 1 :] + else: + continue + msg = f"Expected to find a text node marking the start of docstring content.\nFailed for:\n\n{self!r}" + raise NotImplementedError(msg) + + @staticmethod + def deep_split_punctuation(s: str, /) -> Iterator[str]: + """Deep splitting of ending punctuation.""" + if s.isalnum(): + yield s + else: + end: list[str] = [] + if s.endswith((")", "]")): + end.append(s[-1]) + s = s[:-1] + elif s.endswith("..."): + end.append(s[-3:]) + s = s[:-3] + elif s.endswith(" |"): + end.append(s[-2:]) + s = s[:-2] + if len(s) == 1: + yield s + elif len(s) > 1: + yield from VegaExprNode.deep_split_punctuation(s) + yield from end + + def is_callable(self) -> bool: + """ + Rough filter for excluding `constants`_. + + - Most of the parsing is to handle varying signatures. + - Constants can just be referenced by name, so can skip those + + .. _constants: + https://vega.github.io/vega/docs/expressions/#constants + """ + name = self.name + if name.startswith("string_"): + # HACK: There are string/array functions that overlap + # - the `.md` handles this by prefixing the ` bool: + """ + ``Vega`` `bound variables`_. + + .. _bound variables: + https://vega.github.io/vega/docs/expressions/#bound-variables + """ + RESERVED_NAMES: set[str] = {"datum", "event", "signal"} + return self.name in RESERVED_NAMES + + def is_overloaded(self) -> bool: + """ + Covers the `color functions`_. + + These look like: + + lab(l, a, b[, opacity]) | lab(specifier) + + .. _color functions: + https://vega.github.io/vega/docs/expressions/#color-functions + """ + for idx, item in enumerate(self): + if item[TYPE] == TEXT and item.get(RAW, "") == "]) |": + return self[idx + 1][TYPE] == SOFTBREAK + else: + continue + return False + + def is_keyword(self) -> bool: + return keyword.iskeyword(self.name) + + def __iter__(self) -> Iterator[Token]: + yield from self._children + + @overload + def __getitem__(self, index: int) -> Token: ... + @overload + def __getitem__(self, index: slice) -> Sequence[Token]: ... + def __getitem__(self, index: int | slice) -> Token | Sequence[Token]: + return self._children.__getitem__(index) + + @dataclasses.dataclass class VegaExprParam: name: str From d1bf70885fd9c0c3ba1e444c5496bedbab83395c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:39:50 +0100 Subject: [PATCH 09/77] feat: Adds `parse_expressions` --- tools/schemapi/vega_expr.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 69e6f6fe0..4d8026cfe 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -312,3 +312,21 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: yield cls("*args", required=False, variadic=True) else: continue + + +def parse_expressions(url: str, /) -> Iterator[tuple[str, VegaExprNode]]: + """Download, read markdown and iteratively parse into signature representations.""" + for tok in read_tokens(download_expressions_md(url)): + if ( + (children := tok.get(CHILDREN)) is not None + and (child := next(iter(children)).get(RAW)) is not None + and (match := FUNCTION_DEF_LINE.match(child)) + ): + node = VegaExprNode(match[1], children) + if node.is_callable(): + yield node.name, node.with_parameters().with_doc() + request.urlcleanup() + + +def test_parse() -> dict[str, VegaExprNode]: + return dict(parse_expressions(EXPRESSIONS_URL)) From 3e62bd94505d8c77be9eb028e290a07f6b69cae8 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:56:04 +0100 Subject: [PATCH 10/77] feat(DRAFT): Add `VegaExprNode._doc_post_process` --- tools/schemapi/vega_expr.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 4d8026cfe..5aa6ce9d0 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -118,6 +118,7 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: parser: RSTParse = RSTParse(RSTRenderer()) + @dataclasses.dataclass class VegaExprNode: """ @@ -142,7 +143,7 @@ def with_parameters(self) -> Self: return self def with_doc(self) -> Self: - self.doc = parser.render_tokens(self._doc_tokens()) + self.doc = self._doc_post_process(parser.render_tokens(self._doc_tokens())) return self @functools.cached_property @@ -193,6 +194,32 @@ def _doc_tokens(self) -> Sequence[Token]: msg = f"Expected to find a text node marking the start of docstring content.\nFailed for:\n\n{self!r}" raise NotImplementedError(msg) + def _doc_post_process(self, rendered: str, /) -> str: + r""" + Utilizing properties found during parsing to improve docs. + + Temporarily handling this here. + + TODO + ---- + - [x] Replace \*param_name\* -> \`\`param_name\`\`. + - [x] References to ``func`` -> ``alt.expr.func`` + - **Doesn't include other vega expressions yet** + - [x] Artifacts like: ``monthAbbrevFormat(0) -> "Jan"`` + - [ ] Split after first sentence ends + - [ ] Replace "For example:" -> an example block + - [ ] Fix relative links to vega docs + - There's code in ``utils`` for this + """ + highlight_params = re.sub( + rf"\*({'|'.join(self.parameter_names)})\*", r"``\g<1>``", rendered + ) + with_alt_references = re.sub( + rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params + ) + unescaped = mistune.util.unescape(with_alt_references) + return unescaped + @staticmethod def deep_split_punctuation(s: str, /) -> Iterator[str]: """Deep splitting of ending punctuation.""" From 67c6a1e4ba18d5b98f3e6f9f0ac8a901c73fd116 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:01:10 +0100 Subject: [PATCH 11/77] fix: Don't include `"*args"` in `parameter_names` --- tools/schemapi/vega_expr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 5aa6ce9d0..2f0582a49 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -149,7 +149,9 @@ def with_doc(self) -> Self: @functools.cached_property def parameter_names(self) -> frozenset[str]: if self.parameters: - return frozenset(param.name for param in self.parameters) + return frozenset( + param.name for param in self.parameters if not param.variadic + ) else: msg = ( f"Cannot provide `parameter_names` until they have been initialized via:\n" From 815404ecb894dd7a238e8cc576f22e4bd4192c8b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:15:44 +0100 Subject: [PATCH 12/77] feat(DRAFT): Adds `VegaExprNode.to_signature` - Only the most common case - no docs - No method body --- tools/schemapi/vega_expr.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 2f0582a49..cf88dc906 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -40,6 +40,8 @@ TEXT: Literal["text"] = "text" CHILDREN: Literal["children"] = "children" +RETURN_ANNOTATION = "FunctionExpression" + def download_expressions_md(url: str, /) -> Path: """Download to a temporary file, return that as a ``pathlib.Path``.""" @@ -133,6 +135,17 @@ class VegaExprNode: doc: str = "" parameters: list[VegaExprParam] = dataclasses.field(default_factory=list) + def to_signature(self) -> str: + pre_params = f"def {self.name_safe}(cls, " + post_params = f", /) -> {RETURN_ANNOTATION}:" + param_list = "" + if all(p.required for p in self.parameters): + # NOTE: covers 101/147 cases + param_list = ", ".join(p.name for p in self.parameters) + else: + param_list = "" + return f"{pre_params}{param_list}{post_params}" + def with_parameters(self) -> Self: raw_texts = self._split_signature_tokens() name = next(raw_texts) From 51e569e44dd6ac3d3bf010fd5a1b0f913a006830 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:49:13 +0100 Subject: [PATCH 13/77] feat: Finish `to_signature` params component --- tools/schemapi/vega_expr.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index cf88dc906..8effd0aec 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -34,13 +34,16 @@ FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") -TYPE: Literal[r"type"] = "type" +TYPE: Literal[r"type"] = r"type" RAW: Literal["raw"] = "raw" SOFTBREAK: Literal["softbreak"] = "softbreak" TEXT: Literal["text"] = "text" CHILDREN: Literal["children"] = "children" RETURN_ANNOTATION = "FunctionExpression" +EXPR_ANNOTATION = "IntoExpression" +NONE: Literal[r"None"] = r"None" +STAR_ARGS: Literal["*args"] = "*args" def download_expressions_md(url: str, /) -> Path: @@ -136,14 +139,14 @@ class VegaExprNode: parameters: list[VegaExprParam] = dataclasses.field(default_factory=list) def to_signature(self) -> str: + """NOTE: 101/147 cases are all required args.""" pre_params = f"def {self.name_safe}(cls, " post_params = f", /) -> {RETURN_ANNOTATION}:" param_list = "" - if all(p.required for p in self.parameters): - # NOTE: covers 101/147 cases - param_list = ", ".join(p.name for p in self.parameters) + if self.is_overloaded(): + param_list = VegaExprParam.star_args() else: - param_list = "" + param_list = ", ".join(p.to_str() for p in self.parameters) return f"{pre_params}{param_list}{post_params}" def with_parameters(self) -> Self: @@ -336,6 +339,19 @@ class VegaExprParam: required: bool variadic: bool = False + @staticmethod + def star_args() -> LiteralString: + return f"{STAR_ARGS}: Any" + + def to_str(self) -> str: + """Return as an annotated parameter, with a default if needed.""" + if self.required: + return f"{self.name}: {EXPR_ANNOTATION}" + elif not self.variadic: + return f"{self.name}: {EXPR_ANNOTATION} = {NONE}" + else: + return self.star_args() + @classmethod def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: """Yields an ordered parameter list.""" @@ -351,7 +367,7 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: elif s.isalnum(): yield cls(s, required=is_required) elif s == "...": - yield cls("*args", required=False, variadic=True) + yield cls(STAR_ARGS, required=False, variadic=True) else: continue From b3006bcdf12f8569762693605cf103761cde8257 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:23:42 +0100 Subject: [PATCH 14/77] feat(DRAFT): Add `render_expr_method` Currently just collects the pieces, but doesn't render the final str --- tools/schemapi/vega_expr.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 8effd0aec..6f164a82a 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -24,7 +24,10 @@ from typing import LiteralString, Self, TypeAlias else: from typing_extensions import LiteralString, Self, TypeAlias - Token: TypeAlias = "dict[str, Any]" + +Token: TypeAlias = "dict[str, Any]" +WorkInProgress: TypeAlias = Any +"""Marker for a type that will not be final.""" EXPRESSIONS_URL = ( @@ -44,6 +47,7 @@ EXPR_ANNOTATION = "IntoExpression" NONE: Literal[r"None"] = r"None" STAR_ARGS: Literal["*args"] = "*args" +DECORATOR = r"@classmethod" def download_expressions_md(url: str, /) -> Path: @@ -163,11 +167,9 @@ def with_doc(self) -> Self: return self @functools.cached_property - def parameter_names(self) -> frozenset[str]: + def parameter_names(self) -> tuple[str, ...]: if self.parameters: - return frozenset( - param.name for param in self.parameters if not param.variadic - ) + return tuple(param.name for param in self.parameters if not param.variadic) else: msg = ( f"Cannot provide `parameter_names` until they have been initialized via:\n" @@ -386,5 +388,14 @@ def parse_expressions(url: str, /) -> Iterator[tuple[str, VegaExprNode]]: request.urlcleanup() +def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: + if node.is_overloaded(): + body_params = STAR_ARGS[1:] + else: + body_params = f"({', '.join(param.name for param in node.parameters)})" + body = f"return {RETURN_ANNOTATION}({node.name}, {body_params})" + return DECORATOR, node.to_signature(), node.doc, body + + def test_parse() -> dict[str, VegaExprNode]: return dict(parse_expressions(EXPRESSIONS_URL)) From 54f7db0f17fc8ad3ed5ace0ee12adb5315b2f431 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:35:41 +0100 Subject: [PATCH 15/77] refactor: `FunctionExpression` -> `Expression` for annotation only Also renamed constant `EXPR_ANNOTATION` -> `INPUT_ANNOTATION` --- tools/schemapi/vega_expr.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 6f164a82a..c7d5d63d6 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -43,8 +43,13 @@ TEXT: Literal["text"] = "text" CHILDREN: Literal["children"] = "children" -RETURN_ANNOTATION = "FunctionExpression" -EXPR_ANNOTATION = "IntoExpression" +RETURN_WRAPPER = "FunctionExpression" +RETURN_ANNOTATION = "Expression" +# NOTE: No benefit to annotating with the actual wrapper +# - `Expression` is shorter, and has all the functionality/attributes + +INPUT_ANNOTATION = "IntoExpression" + NONE: Literal[r"None"] = r"None" STAR_ARGS: Literal["*args"] = "*args" DECORATOR = r"@classmethod" @@ -348,9 +353,9 @@ def star_args() -> LiteralString: def to_str(self) -> str: """Return as an annotated parameter, with a default if needed.""" if self.required: - return f"{self.name}: {EXPR_ANNOTATION}" + return f"{self.name}: {INPUT_ANNOTATION}" elif not self.variadic: - return f"{self.name}: {EXPR_ANNOTATION} = {NONE}" + return f"{self.name}: {INPUT_ANNOTATION} = {NONE}" else: return self.star_args() @@ -393,7 +398,7 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: body_params = STAR_ARGS[1:] else: body_params = f"({', '.join(param.name for param in node.parameters)})" - body = f"return {RETURN_ANNOTATION}({node.name}, {body_params})" + body = f"return {RETURN_WRAPPER}({node.name}, {body_params})" return DECORATOR, node.to_signature(), node.doc, body From 328cc9898d943c9cf4633dce64917ef95b6ecab1 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:45:37 +0100 Subject: [PATCH 16/77] feat: Handle `(expr|SchemaBase).copy` conflict --- tools/schemapi/vega_expr.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c7d5d63d6..1758e96d9 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -4,6 +4,7 @@ import functools import keyword import re +from inspect import getmembers from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload from urllib import request @@ -11,6 +12,7 @@ import mistune import mistune.util +from tools.schemapi.schemapi import SchemaBase as _SchemaBase from tools.schemapi.utils import RSTParse as _RSTParse from tools.schemapi.utils import RSTRenderer @@ -53,6 +55,7 @@ NONE: Literal[r"None"] = r"None" STAR_ARGS: Literal["*args"] = "*args" DECORATOR = r"@classmethod" +IGNORE_OVERRIDE = r"# type: ignore[override]" def download_expressions_md(url: str, /) -> Path: @@ -88,6 +91,20 @@ def strip_include_tag(s: str, /) -> str: return LIQUID_INCLUDE.sub(r"", s) +def _override_predicate(obj: Any, /) -> bool: + return ( + callable(obj) + and (name := obj.__name__) + and isinstance(name, str) + and not (name.startswith("_")) + ) + + +_SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( + nm for nm, _ in getmembers(_SchemaBase, _override_predicate) +) + + class RSTParse(_RSTParse): """ Minor extension to support partial `ast`_ conversion. @@ -151,6 +168,8 @@ def to_signature(self) -> str: """NOTE: 101/147 cases are all required args.""" pre_params = f"def {self.name_safe}(cls, " post_params = f", /) -> {RETURN_ANNOTATION}:" + if self.is_incompatible_override(): + post_params = f"{post_params} {IGNORE_OVERRIDE}" param_list = "" if self.is_overloaded(): param_list = VegaExprParam.star_args() @@ -329,6 +348,14 @@ def is_overloaded(self) -> bool: def is_keyword(self) -> bool: return keyword.iskeyword(self.name) + def is_incompatible_override(self) -> bool: + """ + ``self.name_safe`` shadows an unrelated ``SchemaBase`` method. + + Requires an ignore comment for a type checker. + """ + return self.name_safe in _SCHEMA_BASE_MEMBERS + def __iter__(self) -> Iterator[Token]: yield from self._children From 6e1897d3d76a418b803e4cbe9e00fc62cd90befd Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 20 Sep 2024 20:01:31 +0100 Subject: [PATCH 17/77] fix: Disable `string_` overloads --- tools/schemapi/vega_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 1758e96d9..e4e42a3ef 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -302,7 +302,7 @@ def is_callable(self) -> bool: # - the `.md` handles this by prefixing the ` Date: Fri, 20 Sep 2024 20:02:53 +0100 Subject: [PATCH 18/77] fix: Classify more cases as variadic --- tools/schemapi/vega_expr.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index e4e42a3ef..dde4255d0 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -167,7 +167,8 @@ class VegaExprNode: def to_signature(self) -> str: """NOTE: 101/147 cases are all required args.""" pre_params = f"def {self.name_safe}(cls, " - post_params = f", /) -> {RETURN_ANNOTATION}:" + post_params = ")" if self.is_variadic() else ", /)" + post_params = f"{post_params} -> {RETURN_ANNOTATION}:" if self.is_incompatible_override(): post_params = f"{post_params} {IGNORE_OVERRIDE}" param_list = "" @@ -235,7 +236,10 @@ def _doc_tokens(self) -> Sequence[Token]: return self[idx + 1 :] else: continue - msg = f"Expected to find a text node marking the start of docstring content.\nFailed for:\n\n{self!r}" + msg = ( + f"Expected to find a text node marking the start of docstring content.\n" + f"Failed for:\n\n{self!r}" + ) raise NotImplementedError(msg) def _doc_post_process(self, rendered: str, /) -> str: @@ -343,6 +347,14 @@ def is_overloaded(self) -> bool: return self[idx + 1][TYPE] == SOFTBREAK else: continue + for idx, p in enumerate(self.parameters): + if not p.required: + others = self.parameters[idx + 1 :] + if not others: + return False + else: + return any(sp.required for sp in others) + return False def is_keyword(self) -> bool: @@ -356,6 +368,10 @@ def is_incompatible_override(self) -> bool: """ return self.name_safe in _SCHEMA_BASE_MEMBERS + def is_variadic(self) -> bool: + """Position-only parameter separator `"/"` not allowed after `"*"` parameter.""" + return self.is_overloaded() or any(p.variadic for p in self.parameters) + def __iter__(self) -> Iterator[Token]: yield from self._children From 1ce15671a60f212a36d96cc2cc26731041d6459c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 20 Sep 2024 20:04:26 +0100 Subject: [PATCH 19/77] fix: Use quotes for `node.name` --- tools/schemapi/vega_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index dde4255d0..653b38bec 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -441,7 +441,7 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: body_params = STAR_ARGS[1:] else: body_params = f"({', '.join(param.name for param in node.parameters)})" - body = f"return {RETURN_WRAPPER}({node.name}, {body_params})" + body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" return DECORATOR, node.to_signature(), node.doc, body From b88221f41cd0134976410e31e539b4ee054c693a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 20 Sep 2024 20:07:00 +0100 Subject: [PATCH 20/77] feat(DRAFT): Full `expr` module generation - Contains all the functionality - Needs a lot of tidying up --- tools/schemapi/vega_expr.py | 175 +++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 653b38bec..6c7d0171d 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -5,6 +5,7 @@ import keyword import re from inspect import getmembers +from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload from urllib import request @@ -50,12 +51,16 @@ # NOTE: No benefit to annotating with the actual wrapper # - `Expression` is shorter, and has all the functionality/attributes +CONST_WRAPPER = "ConstExpression" +CONST_META = "_ConstExpressionType" + INPUT_ANNOTATION = "IntoExpression" NONE: Literal[r"None"] = r"None" STAR_ARGS: Literal["*args"] = "*args" DECORATOR = r"@classmethod" IGNORE_OVERRIDE = r"# type: ignore[override]" +IGNORE_MISC = r"# type: ignore[misc]" def download_expressions_md(url: str, /) -> Path: @@ -436,14 +441,182 @@ def parse_expressions(url: str, /) -> Iterator[tuple[str, VegaExprNode]]: request.urlcleanup() +EXPR_MODULE_PRE = '''\ +"""Tools for creating transform & filter expressions with a python syntax.""" + +from __future__ import annotations + +import sys +from typing import Any, TYPE_CHECKING + +from altair.expr.core import {const}, {func}, {return_ann}, {input_ann} +from altair.vegalite.v5.schema.core import ExprRef as _ExprRef + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +class {metaclass}(type): + """Metaclass providing read-only class properties for :class:`expr`.""" + + @property + def NaN(cls) -> {return_ann}: + """Not a number (same as JavaScript literal NaN).""" + return {const}("NaN") + + @property + def LN10(cls) -> {return_ann}: + """The natural log of 10 (alias to Math.LN10).""" + return {const}("LN10") + + @property + def E(cls) -> {return_ann}: + """The transcendental number e (alias to Math.E).""" + return {const}("E") + + @property + def LOG10E(cls) -> {return_ann}: + """The base 10 logarithm e (alias to Math.LOG10E).""" + return {const}("LOG10E") + + @property + def LOG2E(cls) -> {return_ann}: + """The base 2 logarithm of e (alias to Math.LOG2E).""" + return {const}("LOG2E") + + @property + def SQRT1_2(cls) -> {return_ann}: + """The square root of 0.5 (alias to Math.SQRT1_2).""" + return {const}("SQRT1_2") + + @property + def LN2(cls) -> {return_ann}: + """The natural log of 2 (alias to Math.LN2).""" + return {const}("LN2") + + @property + def SQRT2(cls) -> {return_ann}: + """The square root of 2 (alias to Math.SQRT1_2).""" + return {const}("SQRT2") + + @property + def PI(cls) -> {return_ann}: + """The transcendental number pi (alias to Math.PI).""" + return {const}("PI") +''' + + +EXPR_MODULE_POST = """\ +_ExprType = expr +# NOTE: Compatibility alias for previous type of `alt.expr`. +# `_ExprType` was not referenced in any internal imports/tests. +""" + +EXPR_CLS_DOC = """ + Utility providing *constants* and *classmethods* to construct expressions. + + `Expressions`_ can be used to write basic formulas that enable custom interactions. + + Alternatively, an `inline expression`_ may be defined via :class:`expr()`. + + Parameters + ---------- + expr: str + A `vega expression`_ string. + + Returns + ------- + ``ExprRef`` + + .. _Expressions: + https://altair-viz.github.io/user_guide/interactions.html#expressions + .. _inline expression: + https://altair-viz.github.io/user_guide/interactions.html#inline-expressions + .. _vega expression: + https://vega.github.io/vega/docs/expressions/ + + Examples + -------- + >>> import altair as alt + + >>> bind_range = alt.binding_range(min=100, max=300, name="Slider value: ") + >>> param_width = alt.param(bind=bind_range, name="param_width") + >>> param_color = alt.param( + ... expr=alt.expr.if_(param_width < 200, "red", "black"), + ... name="param_color", + ... ) + >>> y = alt.Y("yval").axis(titleColor=param_color) + + >>> y + Y({ + axis: {'titleColor': Parameter('param_color', VariableParameter({ + expr: if((param_width < 200),'red','black'), + name: 'param_color' + }))}, + shorthand: 'yval' + }) + """ + +EXPR_CLS_TEMPLATE = '''\ +class expr({base}, metaclass={metaclass}): + """{doc}""" + + @override + def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} + return {base}(expr=expr) +''' + +EXPR_METHOD_TEMPLATE = '''\ + {decorator} + {signature} + """ + {doc}\ + """ + {body} +''' + + +def render_expr_cls(): + return EXPR_CLS_TEMPLATE.format( + base="_ExprRef", + metaclass=CONST_META, + doc=EXPR_CLS_DOC, + type_ignore=IGNORE_MISC, + ) + + def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: if node.is_overloaded(): body_params = STAR_ARGS[1:] else: body_params = f"({', '.join(param.name for param in node.parameters)})" body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" - return DECORATOR, node.to_signature(), node.doc, body + return EXPR_METHOD_TEMPLATE.format( + decorator=DECORATOR, signature=node.to_signature(), doc=node.doc, body=body + ) def test_parse() -> dict[str, VegaExprNode]: return dict(parse_expressions(EXPRESSIONS_URL)) + + +def render_expr_full() -> str: + it = (render_expr_method(node) for _, node in parse_expressions(EXPRESSIONS_URL)) + return "\n".join( + chain( + ( + EXPR_MODULE_PRE.format( + metaclass=CONST_META, + const=CONST_WRAPPER, + return_ann=RETURN_ANNOTATION, + input_ann=INPUT_ANNOTATION, + func=RETURN_WRAPPER, + ), + render_expr_cls(), + ), + it, + [EXPR_MODULE_POST], + ) + ) From 0a099362a2c58d28e9c84cd3d3b9d5b6fc966bd7 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 21 Sep 2024 14:14:11 +0100 Subject: [PATCH 21/77] feat: Strip include tags from docs --- tools/schemapi/vega_expr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 6c7d0171d..3abd47e33 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -264,8 +264,9 @@ def _doc_post_process(self, rendered: str, /) -> str: - [ ] Fix relative links to vega docs - There's code in ``utils`` for this """ + strip_include = strip_include_tag(rendered) highlight_params = re.sub( - rf"\*({'|'.join(self.parameter_names)})\*", r"``\g<1>``", rendered + rf"\*({'|'.join(self.parameter_names)})\*", r"``\g<1>``", strip_include ) with_alt_references = re.sub( rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params From 937759866305f5cc45f87bbfda682177db381f12 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 21 Sep 2024 14:20:00 +0100 Subject: [PATCH 22/77] feat: Split doc summary, wrap body - Uses the same config as `indent_docstring` - Can't truly be `numpydoc` though without a parameters section --- tools/schemapi/vega_expr.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 3abd47e33..9466e0cab 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -4,9 +4,11 @@ import functools import keyword import re +from collections import deque from inspect import getmembers from itertools import chain from pathlib import Path +from textwrap import TextWrapper as _TextWrapper from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload from urllib import request @@ -153,6 +155,27 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: parser: RSTParse = RSTParse(RSTRenderer()) +text_wrap = _TextWrapper( + width=100, + break_long_words=False, + break_on_hyphens=False, + initial_indent=8 * " ", + subsequent_indent=8 * " ", +) + + +def _doc_fmt(doc: str, /) -> str: + sentences: deque[str] = deque(re.split(r"\. ", doc)) + if len(sentences) > 1: + summary = f"{sentences.popleft()}.\n" + last_line = sentences.pop().strip() + sentences = deque(f"{s}. " for s in sentences) + sentences.append(last_line) + sentences = deque(text_wrap.wrap("".join(sentences))) + sentences.appendleft(summary) + return "\n".join(sentences) + else: + return sentences.pop().strip() @dataclasses.dataclass @@ -259,7 +282,8 @@ def _doc_post_process(self, rendered: str, /) -> str: - [x] References to ``func`` -> ``alt.expr.func`` - **Doesn't include other vega expressions yet** - [x] Artifacts like: ``monthAbbrevFormat(0) -> "Jan"`` - - [ ] Split after first sentence ends + - [x] Split after first sentence ends + - [x] Wrap wide docs - [ ] Replace "For example:" -> an example block - [ ] Fix relative links to vega docs - There's code in ``utils`` for this @@ -272,7 +296,8 @@ def _doc_post_process(self, rendered: str, /) -> str: rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params ) unescaped = mistune.util.unescape(with_alt_references) - return unescaped + numpydoc_style = _doc_fmt(unescaped) + return numpydoc_style @staticmethod def deep_split_punctuation(s: str, /) -> Iterator[str]: @@ -573,7 +598,7 @@ def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} {decorator} {signature} """ - {doc}\ + {doc} """ {body} ''' From 8e88cd2f2d969a3c689475d9a1c3e631d1d182a3 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:12:34 +0100 Subject: [PATCH 23/77] chore: Remove some notes --- tools/schemapi/vega_expr.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 9466e0cab..5451f24fe 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -254,11 +254,7 @@ def _split_signature_tokens(self) -> Iterator[str]: break def _doc_tokens(self) -> Sequence[Token]: - """ - Return the slice of `self.children` that contains docstring content. - - Works for 100% of cases. - """ + """Return the slice of `self.children` that contains docstring content.""" for idx, item in enumerate(self): if item[TYPE] == SOFTBREAK and self[idx + 1][TYPE] == TEXT: return self[idx + 1 :] @@ -271,22 +267,10 @@ def _doc_tokens(self) -> Sequence[Token]: raise NotImplementedError(msg) def _doc_post_process(self, rendered: str, /) -> str: - r""" + """ Utilizing properties found during parsing to improve docs. Temporarily handling this here. - - TODO - ---- - - [x] Replace \*param_name\* -> \`\`param_name\`\`. - - [x] References to ``func`` -> ``alt.expr.func`` - - **Doesn't include other vega expressions yet** - - [x] Artifacts like: ``monthAbbrevFormat(0) -> "Jan"`` - - [x] Split after first sentence ends - - [x] Wrap wide docs - - [ ] Replace "For example:" -> an example block - - [ ] Fix relative links to vega docs - - There's code in ``utils`` for this """ strip_include = strip_include_tag(rendered) highlight_params = re.sub( From f5b371743f188a57d8dacb27b3adf9392b1384b1 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:59:23 +0100 Subject: [PATCH 24/77] fix: Replace `vega` docs relative links --- tools/schemapi/vega_expr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 5451f24fe..fc27c0fc3 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -38,6 +38,7 @@ EXPRESSIONS_URL = ( "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" ) +VEGA_DOCS_URL = "https://vega.github.io/vega/docs/" FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") @@ -280,7 +281,8 @@ def _doc_post_process(self, rendered: str, /) -> str: rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params ) unescaped = mistune.util.unescape(with_alt_references) - numpydoc_style = _doc_fmt(unescaped) + non_relative_links = re.sub(r"\.\.\/", VEGA_DOCS_URL, unescaped) + numpydoc_style = _doc_fmt(non_relative_links) return numpydoc_style @staticmethod From 7ec050502c236992d4748e94332498b10cdef0f8 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:53:28 +0100 Subject: [PATCH 25/77] feat: Convert inline links to references See https://docutils.sourceforge.io/docs/user/rst/quickref.html#external-hyperlink-targets --- tools/schemapi/vega_expr.py | 62 ++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index fc27c0fc3..42e29da84 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -9,6 +9,7 @@ from itertools import chain from pathlib import Path from textwrap import TextWrapper as _TextWrapper +from textwrap import indent from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload from urllib import request @@ -17,13 +18,13 @@ from tools.schemapi.schemapi import SchemaBase as _SchemaBase from tools.schemapi.utils import RSTParse as _RSTParse -from tools.schemapi.utils import RSTRenderer +from tools.schemapi.utils import RSTRenderer as _RSTRenderer if TYPE_CHECKING: import sys from re import Pattern - from mistune import BaseRenderer, BlockParser, InlineParser + from mistune import BlockParser, BlockState, InlineParser if sys.version_info >= (3, 11): from typing import LiteralString, Self, TypeAlias @@ -39,9 +40,11 @@ "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" ) VEGA_DOCS_URL = "https://vega.github.io/vega/docs/" +EXPRESSIONS_DOCS_URL = f"{VEGA_DOCS_URL}expressions/" FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") +SENTENCE_BREAK: Pattern[str] = re.compile(r"(? bool: ) +class RSTRenderer(_RSTRenderer): + def __init__(self) -> None: + super().__init__() + + def link(self, token: Token, state: BlockState) -> str: + """Store link url, for appending at the end of doc.""" + attrs = token["attrs"] + url: str = attrs["url"] + if url.startswith("#"): + url = f"{EXPRESSIONS_DOCS_URL}{url}" + text = self.render_children(token, state) + text = text.replace("`", "") + inline = f"`{text}`_" + state.env["ref_links"][text] = {"url": url} + return inline + + def text(self, token: Token, state: BlockState) -> str: + text = super().text(token, state) + return strip_include_tag(text) + + +def _iter_link_lines(ref_links: Any, /) -> Iterator[str]: + links: dict[str, Any] = ref_links + for ref_name, attrs in links.items(): + yield from (f".. _{ref_name}:", f" {attrs['url']}") + + class RSTParse(_RSTParse): """ Minor extension to support partial `ast`_ conversion. @@ -131,7 +161,7 @@ class RSTParse(_RSTParse): def __init__( self, - renderer: BaseRenderer, + renderer: RSTRenderer, block: BlockParser | None = None, inline: InlineParser | None = None, plugins=None, @@ -140,9 +170,9 @@ def __init__( if self.renderer is None: msg = "Must provide a renderer, got `None`" raise TypeError(msg) - self.renderer: BaseRenderer + self.renderer: RSTRenderer - def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: + def render_tokens(self, tokens: Iterable[Token], /) -> str: """ Render ast tokens originating from another parser. @@ -152,7 +182,11 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: All tokens will be rendered into a single `.rst` string """ state = self.block.state_cls() - return self.renderer(self._iter_render(tokens, state), state) + result = self.renderer(self._iter_render(tokens, state), state) + if links := state.env.get("ref_links", {}): + return "\n".join(chain([result], _iter_link_lines(links))) + else: + return result parser: RSTParse = RSTParse(RSTRenderer()) @@ -166,14 +200,19 @@ def render_tokens(self, tokens: Iterable[Token], /) -> LiteralString: def _doc_fmt(doc: str, /) -> str: - sentences: deque[str] = deque(re.split(r"\. ", doc)) + sentences: deque[str] = deque(SENTENCE_BREAK.split(doc)) if len(sentences) > 1: + references: str = "" summary = f"{sentences.popleft()}.\n" last_line = sentences.pop().strip() sentences = deque(f"{s}. " for s in sentences) + if "\n\n.. _" in last_line: + last_line, references = last_line.split("\n\n", maxsplit=1) sentences.append(last_line) sentences = deque(text_wrap.wrap("".join(sentences))) sentences.appendleft(summary) + if references: + sentences.extend(("", indent(references, 8 * " "))) return "\n".join(sentences) else: return sentences.pop().strip() @@ -273,10 +312,11 @@ def _doc_post_process(self, rendered: str, /) -> str: Temporarily handling this here. """ - strip_include = strip_include_tag(rendered) - highlight_params = re.sub( - rf"\*({'|'.join(self.parameter_names)})\*", r"``\g<1>``", strip_include - ) + # NOTE: Avoids adding backticks to parameter names that are also used in a link + # - All cases of these are for `unit|units` + pre, post = "[^`_]", "[^`]" + pattern = rf"({pre})\*({'|'.join(self.parameter_names)})\*({post})" + highlight_params = re.sub(pattern, r"\g<1>``\g<2>``\g<3>", rendered) with_alt_references = re.sub( rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params ) From 29f08dc48352a5ebca0fa5f086ada96c3236d828 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:37:55 +0100 Subject: [PATCH 26/77] refactor: Rewrite `VegaExprNode` as a regular class --- tools/schemapi/vega_expr.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 42e29da84..1531a5370 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -218,7 +218,6 @@ def _doc_fmt(doc: str, /) -> str: return sentences.pop().strip() -@dataclasses.dataclass class VegaExprNode: """ ``SchemaInfo``-like, but operates on `expressions.md`_. @@ -227,10 +226,11 @@ class VegaExprNode: https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md """ - name: str - _children: Sequence[Token] = dataclasses.field(repr=False) - doc: str = "" - parameters: list[VegaExprParam] = dataclasses.field(default_factory=list) + def __init__(self, name: str, children: Sequence[Token], /) -> None: + self.name: str = name + self._children: Sequence[Token] = children + self.parameters: list[VegaExprParam] = [] + self.doc: str = "" def to_signature(self) -> str: """NOTE: 101/147 cases are all required args.""" @@ -439,6 +439,15 @@ def __getitem__(self, index: slice) -> Sequence[Token]: ... def __getitem__(self, index: int | slice) -> Token | Sequence[Token]: return self._children.__getitem__(index) + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(\n " + f"name={self.name!r},\n " + f"parameters={self.parameters!r},\n " + f"doc={self.doc!r}\n" + ")" + ) + @dataclasses.dataclass class VegaExprParam: From f6cc7762a9d68ad592868eaf963328eada07631c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:19:12 +0100 Subject: [PATCH 27/77] feat: Replace all `Vega` function refs in code blocks - Renamed `name_safe` -> `title` - Perform an eager pass to extract replacement pairs https://github.com/vega/altair/pull/3600#discussion_r1771091211 --- tools/schemapi/vega_expr.py | 144 +++++++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 17 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 1531a5370..952de759b 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -10,7 +10,18 @@ from pathlib import Path from textwrap import TextWrapper as _TextWrapper from textwrap import indent -from typing import TYPE_CHECKING, Any, Iterable, Iterator, Literal, Sequence, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Iterable, + Iterator, + Literal, + Mapping, + Sequence, + overload, +) from urllib import request import mistune @@ -22,7 +33,7 @@ if TYPE_CHECKING: import sys - from re import Pattern + from re import Match, Pattern from mistune import BlockParser, BlockState, InlineParser @@ -30,6 +41,7 @@ from typing import LiteralString, Self, TypeAlias else: from typing_extensions import LiteralString, Self, TypeAlias + from _typeshed import SupportsKeysAndGetItem Token: TypeAlias = "dict[str, Any]" WorkInProgress: TypeAlias = Any @@ -218,6 +230,81 @@ def _doc_fmt(doc: str, /) -> str: return sentences.pop().strip() +class ReplaceMany: + def __init__( + self, + m: Mapping[str, str] | None = None, + /, + fmt_match: str = "(?P{0})", + fmt_replace: str = "{0}", + ) -> None: + self._mapping: dict[str, str] = dict(m) if m else {} + self._fmt_match: str = fmt_match + self._fmt_replace: str = fmt_replace + self.pattern: Pattern[str] + self.repl: Callable[[Match[str]], str] + + def update( + self, + m: SupportsKeysAndGetItem[str, str] | Iterable[tuple[str, str]], + /, + **kwds: str, + ) -> None: + """Update replacements mapping.""" + self._mapping.update(m, **kwds) + + def clear(self) -> None: + """Reset replacements mapping.""" + self._mapping.clear() + + def prepare(self) -> None: + """ + Compile replacement pattern and generate substitution function. + + Notes + ----- + Should be called **after** all (old, new) pairs have been collected. + """ + self.pattern = self._compile() + self.repl = self._replacer() + + def __call__(self, s: str, count: int = 0, /) -> str: + """ + Replace the leftmost non-overlapping occurrences of ``self.pattern`` in ``s`` using ``self.repl``. + + Wraps `re.sub`_ + + .. _re.sub: + _https://docs.python.org/3/library/re.html#re.sub + """ + return self.pattern.sub(self.repl, s, count) + + def _compile(self) -> Pattern[str]: + if not self._mapping: + name = self._mapping.__qualname__ + msg = ( + f"Requires {name!r} to be populated, but got:\n" + f"{name}={self._mapping!r}" + ) + raise TypeError(msg) + return re.compile(rf"{self._fmt_match.format('|'.join(self._mapping))}") + + def _replacer(self) -> Callable[[Match[str]], str]: + def repl(m: Match[str], /) -> str: + return self._fmt_replace.format(self._mapping[m["key"]]) + + return repl + + def __getitem__(self, key: str) -> str: + return self._mapping[key] + + def __setitem__(self, key: str, value: str) -> None: + self._mapping[key] = value + + def __repr__(self) -> str: + return f"{type(self).__name__}(\n {self._mapping!r}\n)" + + class VegaExprNode: """ ``SchemaInfo``-like, but operates on `expressions.md`_. @@ -226,15 +313,20 @@ class VegaExprNode: https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md """ + remap_title: ClassVar[ReplaceMany] = ReplaceMany( + fmt_match=r"(?P{0})\(", fmt_replace="{0}(" + ) + def __init__(self, name: str, children: Sequence[Token], /) -> None: self.name: str = name self._children: Sequence[Token] = children self.parameters: list[VegaExprParam] = [] self.doc: str = "" + self.signature: str = "" - def to_signature(self) -> str: + def with_signature(self) -> Self: """NOTE: 101/147 cases are all required args.""" - pre_params = f"def {self.name_safe}(cls, " + pre_params = f"def {self.title}(cls, " post_params = ")" if self.is_variadic() else ", /)" post_params = f"{post_params} -> {RETURN_ANNOTATION}:" if self.is_incompatible_override(): @@ -244,7 +336,8 @@ def to_signature(self) -> str: param_list = VegaExprParam.star_args() else: param_list = ", ".join(p.to_str() for p in self.parameters) - return f"{pre_params}{param_list}{post_params}" + self.signature = f"{pre_params}{param_list}{post_params}" + return self def with_parameters(self) -> Self: raw_texts = self._split_signature_tokens() @@ -271,9 +364,15 @@ def parameter_names(self) -> tuple[str, ...]: raise TypeError(msg) @property - def name_safe(self) -> str: - """Use for the method definition, but not when calling internally.""" - return f"{self.name}_" if self.is_keyword() else self.name + def title(self) -> str: + """ + Use for the method definition, but not when calling internally. + + Updates ``VegaExprNode.remap_title`` for documentation example substitutions. + """ + title = f"{self.name}_" if self.is_keyword() else self.name + type(self).remap_title.update({self.name: f"alt.expr.{title}"}) + return title def _split_signature_tokens(self) -> Iterator[str]: """Very rough splitting/cleaning of relevant token raw text.""" @@ -317,9 +416,7 @@ def _doc_post_process(self, rendered: str, /) -> str: pre, post = "[^`_]", "[^`]" pattern = rf"({pre})\*({'|'.join(self.parameter_names)})\*({post})" highlight_params = re.sub(pattern, r"\g<1>``\g<2>``\g<3>", rendered) - with_alt_references = re.sub( - rf"({self.name}\()", f"alt.expr.{self.name_safe}(", highlight_params - ) + with_alt_references = type(self).remap_title(highlight_params) unescaped = mistune.util.unescape(with_alt_references) non_relative_links = re.sub(r"\.\.\/", VEGA_DOCS_URL, unescaped) numpydoc_style = _doc_fmt(non_relative_links) @@ -423,7 +520,7 @@ def is_incompatible_override(self) -> bool: Requires an ignore comment for a type checker. """ - return self.name_safe in _SCHEMA_BASE_MEMBERS + return self.title in _SCHEMA_BASE_MEMBERS def is_variadic(self) -> bool: """Position-only parameter separator `"/"` not allowed after `"*"` parameter.""" @@ -488,7 +585,7 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: continue -def parse_expressions(url: str, /) -> Iterator[tuple[str, VegaExprNode]]: +def _parse_expressions(url: str, /) -> Iterator[VegaExprNode]: """Download, read markdown and iteratively parse into signature representations.""" for tok in read_tokens(download_expressions_md(url)): if ( @@ -498,10 +595,22 @@ def parse_expressions(url: str, /) -> Iterator[tuple[str, VegaExprNode]]: ): node = VegaExprNode(match[1], children) if node.is_callable(): - yield node.name, node.with_parameters().with_doc() + yield node.with_parameters().with_signature() request.urlcleanup() +def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: + """ + Eagerly parse signatures of relevant definitions, then yield with docs. + + Ensures each doc can use all remapped names, regardless of the order they appear. + """ + eager = tuple(_parse_expressions(url)) + VegaExprNode.remap_title.prepare() + for node in eager: + yield node.with_doc() + + EXPR_MODULE_PRE = '''\ """Tools for creating transform & filter expressions with a python syntax.""" @@ -655,16 +764,17 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: body_params = f"({', '.join(param.name for param in node.parameters)})" body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" return EXPR_METHOD_TEMPLATE.format( - decorator=DECORATOR, signature=node.to_signature(), doc=node.doc, body=body + decorator=DECORATOR, signature=node.signature, doc=node.doc, body=body ) def test_parse() -> dict[str, VegaExprNode]: - return dict(parse_expressions(EXPRESSIONS_URL)) + """Temporary introspection tool.""" + return {node.name: node for node in parse_expressions(EXPRESSIONS_URL)} def render_expr_full() -> str: - it = (render_expr_method(node) for _, node in parse_expressions(EXPRESSIONS_URL)) + it = (render_expr_method(node) for node in parse_expressions(EXPRESSIONS_URL)) return "\n".join( chain( ( From 321a0cde1bb3d210a381b47c2b07a99940ab077b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:21:46 +0100 Subject: [PATCH 28/77] docs: Update doc for `is_overloaded` I've identified multiple kinds of overloads. If we want to further improve accuracy, then these would need to be considered separately --- tools/schemapi/vega_expr.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 952de759b..aad002851 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -493,8 +493,17 @@ def is_overloaded(self) -> bool: lab(l, a, b[, opacity]) | lab(specifier) + Looping of parameters is for signatures like `sequence`_: + + sequence([start, ]stop[, step]) + + The optional first parameter, followed by a required one would need an + ``@overload`` in ``python``. + .. _color functions: https://vega.github.io/vega/docs/expressions/#color-functions + .. _sequence: + https://vega.github.io/vega/docs/expressions/#sequence """ for idx, item in enumerate(self): if item[TYPE] == TEXT and item.get(RAW, "") == "]) |": From 90f7b0a22aef1914aa10b14b9725d606ad266cef Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:22:52 +0100 Subject: [PATCH 29/77] docs: Add note to `_doc_fmt` --- tools/schemapi/vega_expr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index aad002851..7153327c3 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -212,6 +212,11 @@ def render_tokens(self, tokens: Iterable[Token], /) -> str: def _doc_fmt(doc: str, /) -> str: + """ + FIXME: Currently doing too many things. + + Primarily using to exclude summary line + references from ``textwrap``. + """ sentences: deque[str] = deque(SENTENCE_BREAK.split(doc)) if len(sentences) > 1: references: str = "" From 36ac45aded1221ae06757a0516e33f42fbf76258 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:04:48 +0100 Subject: [PATCH 30/77] refactor: Simplify `VegaExprNode.is_callable` --- tools/schemapi/vega_expr.py | 40 ++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 7153327c3..873cac4c2 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -459,24 +459,16 @@ def is_callable(self) -> bool: .. _constants: https://vega.github.io/vega/docs/expressions/#constants """ - name = self.name - if name.startswith("string_"): - # HACK: There are string/array functions that overlap - # - the `.md` handles this by prefixing the ` bool: """ ``Vega`` `bound variables`_. + These do not provide signatures: + + {"datum", "event", "signal"} + .. _bound variables: https://vega.github.io/vega/docs/expressions/#bound-variables """ @@ -525,6 +521,18 @@ def is_overloaded(self) -> bool: return False + def is_overloaded_string_array(self) -> bool: + """ + HACK: There are string/array functions that overlap. + + - the `.md` handles this by prefixing the ` bool: return keyword.iskeyword(self.name) From 56dfa276fd932ef4ef419a19fab6cf3e3448b644 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:12:31 +0100 Subject: [PATCH 31/77] refactor: Simplify `VegaExprNode._split_signature_tokens` - Separates the token vs string iteration - Improve doc, w/ example --- tools/schemapi/vega_expr.py | 57 +++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 873cac4c2..05b315ae3 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -95,7 +95,7 @@ def download_expressions_md(url: str, /) -> Path: return fp -def read_tokens(source: Path, /) -> list[Any]: +def read_tokens(source: Path, /) -> list[Token]: """ Read from ``source``, drop ``BlockState``. @@ -348,6 +348,7 @@ def with_parameters(self) -> Self: raw_texts = self._split_signature_tokens() name = next(raw_texts) # NOTE: Overwriting the with the rendered text + # - required for `clamprange` -> `clampRange` if self.name != name: self.name = name self.parameters = list(VegaExprParam.iter_params(raw_texts)) @@ -379,23 +380,49 @@ def title(self) -> str: type(self).remap_title.update({self.name: f"alt.expr.{title}"}) return title - def _split_signature_tokens(self) -> Iterator[str]: - """Very rough splitting/cleaning of relevant token raw text.""" - it = iter(self) + def _signature_tokens(self) -> Iterator[Token]: + """ + Target for signature appears between 2 softbreak tokens. + + - Proceeds to the first token **after** a softbreak + - Yield **only** text tokens + - Skips all inline html tags + - Stops at 2nd softbreak + """ + it: Iterator[Token] = iter(self) current = next(it) - # NOTE: softbreak(s) denote the line the sig appears on while current[TYPE] != SOFTBREAK: current = next(it) - current = next(it) - while current[TYPE] != SOFTBREAK: - # NOTE: This drops html markup tags - if current[TYPE] == TEXT: - clean = strip_include_tag(current[RAW]).strip(", -") - if clean not in {", ", ""}: - yield from VegaExprNode.deep_split_punctuation(clean) - current = next(it, None) - if current is None: + next(it) + for target in it: + if target[TYPE] == TEXT: + yield target + elif target[TYPE] == SOFTBREAK: break + else: + continue + + def _split_signature_tokens(self) -> Iterator[str]: + """ + Normalize the text content of the signature. + + Examples + -------- + The following definition: + + # + sequence([start, ]stop[, step])
+ Returns an array containing an arithmetic sequence of numbers. + ... + + Will yield: + + ['sequence', '(', '[', 'start', ']', 'stop', '[', 'step', ']', ')'] + """ + for tok in self._signature_tokens(): + clean = strip_include_tag(tok[RAW]).strip(", -") + if clean not in {", ", ""}: + yield from VegaExprNode.deep_split_punctuation(clean) def _doc_tokens(self) -> Sequence[Token]: """Return the slice of `self.children` that contains docstring content.""" @@ -538,7 +565,7 @@ def is_keyword(self) -> bool: def is_incompatible_override(self) -> bool: """ - ``self.name_safe`` shadows an unrelated ``SchemaBase`` method. + ``self.title`` shadows an unrelated ``SchemaBase`` method. Requires an ignore comment for a type checker. """ From 9cab1a148793e5e0beab85923ca27df83cad2185 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:32:27 +0100 Subject: [PATCH 32/77] docs: Add doc for `deep_split_punctuation` --- tools/schemapi/vega_expr.py | 45 ++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 05b315ae3..93d8f0970 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -48,32 +48,43 @@ """Marker for a type that will not be final.""" +# NOTE: Urls/fragments EXPRESSIONS_URL = ( "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" ) VEGA_DOCS_URL = "https://vega.github.io/vega/docs/" EXPRESSIONS_DOCS_URL = f"{VEGA_DOCS_URL}expressions/" +# NOTE: Regex patterns FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") SENTENCE_BREAK: Pattern[str] = re.compile(r"(? Self: post_params = f"{post_params} -> {RETURN_ANNOTATION}:" if self.is_incompatible_override(): post_params = f"{post_params} {IGNORE_OVERRIDE}" - param_list = "" if self.is_overloaded(): param_list = VegaExprParam.star_args() else: @@ -456,18 +466,27 @@ def _doc_post_process(self, rendered: str, /) -> str: @staticmethod def deep_split_punctuation(s: str, /) -> Iterator[str]: - """Deep splitting of ending punctuation.""" + """ + When ``s`` ends with one of these markers: + + ")", "]", "...", " |" + + - Split ``s`` into rest, match + - using the length of the match to index + - Append match to ``end`` + - Recurse + """ # noqa: D400 if s.isalnum(): yield s else: end: list[str] = [] - if s.endswith((")", "]")): + if s.endswith((CLOSE_PAREN, CLOSE_BRACKET)): end.append(s[-1]) s = s[:-1] - elif s.endswith("..."): + elif s.endswith(ELLIPSIS): end.append(s[-3:]) s = s[:-3] - elif s.endswith(" |"): + elif s.endswith(INLINE_OVERLOAD): end.append(s[-2:]) s = s[:-2] if len(s) == 1: @@ -497,7 +516,7 @@ def is_callable(self) -> bool: else: return False next(it) - return next(it).get(RAW, "") == "(" + return next(it).get(RAW, "") == OPEN_PAREN def is_bound_variable_name(self) -> bool: """ @@ -534,7 +553,7 @@ def is_overloaded(self) -> bool: https://vega.github.io/vega/docs/expressions/#sequence """ for idx, item in enumerate(self): - if item[TYPE] == TEXT and item.get(RAW, "") == "]) |": + if item[TYPE] == TEXT and item.get(RAW, "").endswith(INLINE_OVERLOAD): return self[idx + 1][TYPE] == SOFTBREAK else: continue @@ -619,16 +638,16 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: """Yields an ordered parameter list.""" is_required: bool = True for s in raw_texts: - if s not in {"(", ")"}: - if s == "[": + if s not in {OPEN_PAREN, CLOSE_PAREN}: + if s == OPEN_BRACKET: is_required = False continue - elif s == "]": + elif s == CLOSE_BRACKET: is_required = True continue elif s.isalnum(): yield cls(s, required=is_required) - elif s == "...": + elif s == ELLIPSIS: yield cls(STAR_ARGS, required=False, variadic=True) else: continue From 497f2fd6f6d8c66ddfbf4f64f40be01e38a0e111 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:09:09 +0100 Subject: [PATCH 33/77] feat: Refine `ReplaceMany` - Renaming - Allow refreshing on any call - Auto refresh if first call did not --- tools/schemapi/vega_expr.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 93d8f0970..0fb15a1a3 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -249,16 +249,17 @@ def _doc_fmt(doc: str, /) -> str: class ReplaceMany: def __init__( self, - m: Mapping[str, str] | None = None, + mapping: Mapping[str, str] | None = None, /, fmt_match: str = "(?P{0})", fmt_replace: str = "{0}", ) -> None: - self._mapping: dict[str, str] = dict(m) if m else {} + self._mapping: dict[str, str] = dict(mapping) if mapping else {} self._fmt_match: str = fmt_match self._fmt_replace: str = fmt_replace self.pattern: Pattern[str] self.repl: Callable[[Match[str]], str] + self._is_prepared: bool = False def update( self, @@ -273,7 +274,7 @@ def clear(self) -> None: """Reset replacements mapping.""" self._mapping.clear() - def prepare(self) -> None: + def refresh(self) -> None: """ Compile replacement pattern and generate substitution function. @@ -283,16 +284,19 @@ def prepare(self) -> None: """ self.pattern = self._compile() self.repl = self._replacer() + self._is_prepared = True - def __call__(self, s: str, count: int = 0, /) -> str: + def __call__(self, s: str, count: int = 0, /, refresh: bool = False) -> str: """ Replace the leftmost non-overlapping occurrences of ``self.pattern`` in ``s`` using ``self.repl``. Wraps `re.sub`_ .. _re.sub: - _https://docs.python.org/3/library/re.html#re.sub + https://docs.python.org/3/library/re.html#re.sub """ + if not self._is_prepared or refresh: + self.refresh() return self.pattern.sub(self.repl, s, count) def _compile(self) -> Pattern[str]: @@ -674,7 +678,7 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: Ensures each doc can use all remapped names, regardless of the order they appear. """ eager = tuple(_parse_expressions(url)) - VegaExprNode.remap_title.prepare() + VegaExprNode.remap_title.refresh() for node in eager: yield node.with_doc() From b0b19521d54bda8fc23951de272c6a3fd6013649 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:12:49 +0100 Subject: [PATCH 34/77] docs: Add doc for `ReplaceMany` - Already general enough to be used in other areas of `tools`. - E.g. for `vega-lite` docs using `true`, `false`, `null` -> `True`, `False`, `None` --- tools/schemapi/vega_expr.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 0fb15a1a3..1b4557dad 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -247,6 +247,52 @@ def _doc_fmt(doc: str, /) -> str: class ReplaceMany: + """ + Perform many ``1:1`` replacements on a given text. + + Structured wrapper around a `dict`_ and `re.sub`_. + + Parameters + ---------- + mapping + Optional initial mapping. + fmt_match + **Combined** format string/regex pattern. + Receives the keys of the final ``self._mapping`` as a positional argument. + + .. note:: + Special characters must be escaped **first**, if present. + + fmt_replace + Format string applied to a succesful match, after substition. + Receives ``self._mapping[key]`` as a positional argument. + + .. _dict: + https://docs.python.org/3/library/stdtypes.html#mapping-types-dict + .. _re.sub: + https://docs.python.org/3/library/re.html#re.sub + + Examples + -------- + Providing a mapping during construction: + + string = "The dog chased the cat, chasing the mouse. Poor mouse" + animal_replacer = ReplaceMany({"dog": "cat"}) + >>> animal_replacer(string) + 'The cat chased the cat, chasing the mouse. Poor mouse' + + Updating with new replacements: + + animal_replacer.update({"cat": "mouse", "mouse": "dog"}, duck="rabbit") + >>> animal_replacer(string, refresh=True) + 'The cat chased the mouse, chasing the dog. Poor dog' + + Further calls will continue using the most recent update: + + >>> animal_replacer("duck") + 'rabbit' + """ + def __init__( self, mapping: Mapping[str, str] | None = None, From 2d5271e3362160912f41379913ad93a7c3f3ae60 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:53:11 +0100 Subject: [PATCH 35/77] refactor: Replace `VegaExprNode.parameter_names` property with a filterable iterator --- tools/schemapi/vega_expr.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 1b4557dad..7f8d024b4 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -1,7 +1,6 @@ from __future__ import annotations import dataclasses -import functools import keyword import re from collections import deque @@ -418,10 +417,15 @@ def with_doc(self) -> Self: self.doc = self._doc_post_process(parser.render_tokens(self._doc_tokens())) return self - @functools.cached_property - def parameter_names(self) -> tuple[str, ...]: + def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: + """Pass ``variadic=False`` to omit names like``*args``.""" if self.parameters: - return tuple(param.name for param in self.parameters if not param.variadic) + it: Iterator[str] = ( + (p.name for p in self.parameters) + if variadic + else (p.name for p in self.parameters if not p.variadic) + ) + yield from it else: msg = ( f"Cannot provide `parameter_names` until they have been initialized via:\n" @@ -506,7 +510,9 @@ def _doc_post_process(self, rendered: str, /) -> str: # NOTE: Avoids adding backticks to parameter names that are also used in a link # - All cases of these are for `unit|units` pre, post = "[^`_]", "[^`]" - pattern = rf"({pre})\*({'|'.join(self.parameter_names)})\*({post})" + pattern = ( + rf"({pre})\*({'|'.join(self.parameter_names(variadic=False))})\*({post})" + ) highlight_params = re.sub(pattern, r"\g<1>``\g<2>``\g<3>", rendered) with_alt_references = type(self).remap_title(highlight_params) unescaped = mistune.util.unescape(with_alt_references) @@ -866,7 +872,7 @@ def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} ''' -def render_expr_cls(): +def render_expr_cls() -> WorkInProgress: return EXPR_CLS_TEMPLATE.format( base="_ExprRef", metaclass=CONST_META, @@ -879,7 +885,7 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: if node.is_overloaded(): body_params = STAR_ARGS[1:] else: - body_params = f"({', '.join(param.name for param in node.parameters)})" + body_params = f"({', '.join(node.parameter_names())})" body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" return EXPR_METHOD_TEMPLATE.format( decorator=DECORATOR, signature=node.signature, doc=node.doc, body=body From 849f428bc08e679f0410b58d1572c80d3d25d9b6 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:59:48 +0100 Subject: [PATCH 36/77] refactor: Tidy up `"clamprange"` -> `"clampRange"` special case - Was previously being handled in multiple places - Now the correct name is set during the first access to `self.name` --- tools/schemapi/vega_expr.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 7f8d024b4..915fb3def 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -404,13 +404,8 @@ def with_signature(self) -> Self: return self def with_parameters(self) -> Self: - raw_texts = self._split_signature_tokens() - name = next(raw_texts) - # NOTE: Overwriting the with the rendered text - # - required for `clamprange` -> `clampRange` - if self.name != name: - self.name = name - self.parameters = list(VegaExprParam.iter_params(raw_texts)) + split: Iterator[str] = self._split_signature_tokens(exclude_name=True) + self.parameters = list(VegaExprParam.iter_params(split)) return self def with_doc(self) -> Self: @@ -466,7 +461,7 @@ def _signature_tokens(self) -> Iterator[Token]: else: continue - def _split_signature_tokens(self) -> Iterator[str]: + def _split_signature_tokens(self, *, exclude_name: bool = False) -> Iterator[str]: """ Normalize the text content of the signature. @@ -482,10 +477,15 @@ def _split_signature_tokens(self) -> Iterator[str]: Will yield: ['sequence', '(', '[', 'start', ']', 'stop', '[', 'step', ']', ')'] + + When called with ``exclude_name=True``: + + ['(', '[', 'start', ']', 'stop', '[', 'step', ']', ')'] """ + EXCLUDE: set[str] = {", ", "", self.name} if exclude_name else {", ", ""} for tok in self._signature_tokens(): clean = strip_include_tag(tok[RAW]).strip(", -") - if clean not in {", ", ""}: + if clean not in EXCLUDE: yield from VegaExprNode.deep_split_punctuation(clean) def _doc_tokens(self) -> Sequence[Token]: @@ -558,19 +558,26 @@ def is_callable(self) -> bool: - Most of the parsing is to handle varying signatures. - Constants can just be referenced by name, so can skip those + Notes + ----- + - Overwriting the with the rendered text + - required for `clamprange` -> `clampRange` + .. _constants: https://vega.github.io/vega/docs/expressions/#constants """ if self.is_overloaded_string_array() or self.is_bound_variable_name(): return False it: Iterator[Token] = iter(self) - current: str = next(it, {}).get(RAW, "").lower() - name: str = self.name.lower() - while current != name: + current: str = next(it, {}).get(RAW, "") + name: str = self.name.casefold() + while current.casefold() != name: if (el := next(it, None)) is not None: - current = el.get(RAW, "").lower() + current = el.get(RAW, "") else: return False + if current != self.name: + self.name = current next(it) return next(it).get(RAW, "") == OPEN_PAREN From bed8d0faae908aa4cab27a7eba9db747018f9afd Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:11:36 +0100 Subject: [PATCH 37/77] refactor: Reorder `with_` methods, add docs Little bit easier to see how they relate --- tools/schemapi/vega_expr.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 915fb3def..beff25271 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -389,8 +389,31 @@ def __init__(self, name: str, children: Sequence[Token], /) -> None: self.doc: str = "" self.signature: str = "" + def with_doc(self) -> Self: + """ + Parses docstring content in full. + + Accessible via ``self.doc`` + """ + self.doc = self._doc_post_process(parser.render_tokens(self._doc_tokens())) + return self + + def with_parameters(self) -> Self: + """ + Parses signature content into an intermediate representation. + + Accessible via ``self.parameters``. + """ + split: Iterator[str] = self._split_signature_tokens(exclude_name=True) + self.parameters = list(VegaExprParam.iter_params(split)) + return self + def with_signature(self) -> Self: - """NOTE: 101/147 cases are all required args.""" + """ + Parses ``self.parameters`` into a full signature definition line. + + Accessible via ``self.signature`` + """ pre_params = f"def {self.title}(cls, " post_params = ")" if self.is_variadic() else ", /)" post_params = f"{post_params} -> {RETURN_ANNOTATION}:" @@ -403,15 +426,6 @@ def with_signature(self) -> Self: self.signature = f"{pre_params}{param_list}{post_params}" return self - def with_parameters(self) -> Self: - split: Iterator[str] = self._split_signature_tokens(exclude_name=True) - self.parameters = list(VegaExprParam.iter_params(split)) - return self - - def with_doc(self) -> Self: - self.doc = self._doc_post_process(parser.render_tokens(self._doc_tokens())) - return self - def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: """Pass ``variadic=False`` to omit names like``*args``.""" if self.parameters: From 264e4a6578bbc8730ab1eeae9c18909b1892e24c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:14:56 +0100 Subject: [PATCH 38/77] refactor: Rename reorder `_split_markers` --- tools/schemapi/vega_expr.py | 64 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index beff25271..a7fd7c2f3 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -500,7 +500,38 @@ def _split_signature_tokens(self, *, exclude_name: bool = False) -> Iterator[str for tok in self._signature_tokens(): clean = strip_include_tag(tok[RAW]).strip(", -") if clean not in EXCLUDE: - yield from VegaExprNode.deep_split_punctuation(clean) + yield from VegaExprNode._split_markers(clean) + + @staticmethod + def _split_markers(s: str, /) -> Iterator[str]: + """ + When ``s`` ends with one of these markers: + + ")", "]", "...", " |" + + - Split ``s`` into rest, match + - using the length of the match to index + - Append match to ``end`` + - Recurse + """ # noqa: D400 + if s.isalnum(): + yield s + else: + end: list[str] = [] + if s.endswith((CLOSE_PAREN, CLOSE_BRACKET)): + end.append(s[-1]) + s = s[:-1] + elif s.endswith(ELLIPSIS): + end.append(s[-3:]) + s = s[:-3] + elif s.endswith(INLINE_OVERLOAD): + end.append(s[-2:]) + s = s[:-2] + if len(s) == 1: + yield s + elif len(s) > 1: + yield from VegaExprNode._split_markers(s) + yield from end def _doc_tokens(self) -> Sequence[Token]: """Return the slice of `self.children` that contains docstring content.""" @@ -534,37 +565,6 @@ def _doc_post_process(self, rendered: str, /) -> str: numpydoc_style = _doc_fmt(non_relative_links) return numpydoc_style - @staticmethod - def deep_split_punctuation(s: str, /) -> Iterator[str]: - """ - When ``s`` ends with one of these markers: - - ")", "]", "...", " |" - - - Split ``s`` into rest, match - - using the length of the match to index - - Append match to ``end`` - - Recurse - """ # noqa: D400 - if s.isalnum(): - yield s - else: - end: list[str] = [] - if s.endswith((CLOSE_PAREN, CLOSE_BRACKET)): - end.append(s[-1]) - s = s[:-1] - elif s.endswith(ELLIPSIS): - end.append(s[-3:]) - s = s[:-3] - elif s.endswith(INLINE_OVERLOAD): - end.append(s[-2:]) - s = s[:-2] - if len(s) == 1: - yield s - elif len(s) > 1: - yield from VegaExprNode.deep_split_punctuation(s) - yield from end - def is_callable(self) -> bool: """ Rough filter for excluding `constants`_. From 7c24e4615b5ea1baa85c9850d88e800b7cc6e65d Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:28:02 +0100 Subject: [PATCH 39/77] refactor: Move url expansion to renderer --- tools/schemapi/vega_expr.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index a7fd7c2f3..e960dcc69 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -143,11 +143,19 @@ def __init__(self) -> None: super().__init__() def link(self, token: Token, state: BlockState) -> str: - """Store link url, for appending at the end of doc.""" + """ + Store link url, for appending at the end of doc. + + TODO + ---- + - Parameterize `"#"`, `"../"` expansion during init + """ attrs = token["attrs"] url: str = attrs["url"] if url.startswith("#"): url = f"{EXPRESSIONS_DOCS_URL}{url}" + else: + url = url.replace(r"../", VEGA_DOCS_URL) text = self.render_children(token, state) text = text.replace("`", "") inline = f"`{text}`_" @@ -561,8 +569,7 @@ def _doc_post_process(self, rendered: str, /) -> str: highlight_params = re.sub(pattern, r"\g<1>``\g<2>``\g<3>", rendered) with_alt_references = type(self).remap_title(highlight_params) unescaped = mistune.util.unescape(with_alt_references) - non_relative_links = re.sub(r"\.\.\/", VEGA_DOCS_URL, unescaped) - numpydoc_style = _doc_fmt(non_relative_links) + numpydoc_style = _doc_fmt(unescaped) return numpydoc_style def is_callable(self) -> bool: From 051bcb228e0931fdd030fb5d36f0987cde5c8ead Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:36:01 +0100 Subject: [PATCH 40/77] feat: Adds initial `vega_expr` api There are more things I want to change, but ideally keep `write_expr_module` consistent --- tools/generate_schema_wrapper.py | 5 +++ tools/schemapi/__init__.py | 2 + tools/schemapi/vega_expr.py | 67 +++++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index a0fa08e72..6b2e24c51 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -27,6 +27,7 @@ arg_kwds, arg_required_kwds, codegen, + write_expr_module, ) from tools.schemapi.utils import ( SchemaProperties, @@ -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" / "dummy.py" +).resolve() CHANNEL_MYPY_IGNORE_STATEMENTS: Final = """\ # These errors need to be ignored as they come from the overload methods @@ -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 diff --git a/tools/schemapi/__init__.py b/tools/schemapi/__init__.py index 55c5f4148..a08b8f44e 100644 --- a/tools/schemapi/__init__.py +++ b/tools/schemapi/__init__.py @@ -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", @@ -21,4 +22,5 @@ "arg_required_kwds", "codegen", "utils", + "write_expr_module", ] diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index e960dcc69..14ce9cfa7 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses +import enum import keyword import re from collections import deque @@ -29,6 +30,9 @@ from tools.schemapi.schemapi import SchemaBase as _SchemaBase from tools.schemapi.utils import RSTParse as _RSTParse from tools.schemapi.utils import RSTRenderer as _RSTRenderer +from tools.schemapi.utils import ( + ruff_write_lint_format_str as _ruff_write_lint_format_str, +) if TYPE_CHECKING: import sys @@ -42,18 +46,23 @@ from typing_extensions import LiteralString, Self, TypeAlias from _typeshed import SupportsKeysAndGetItem +__all__ = ["render_expr_full", "test_parse", "write_expr_module"] + Token: TypeAlias = "dict[str, Any]" WorkInProgress: TypeAlias = Any """Marker for a type that will not be final.""" # NOTE: Urls/fragments -EXPRESSIONS_URL = ( - "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" -) VEGA_DOCS_URL = "https://vega.github.io/vega/docs/" EXPRESSIONS_DOCS_URL = f"{VEGA_DOCS_URL}expressions/" + +class Source(str, enum.Enum): + LIVE = "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" + STATIC = "https://raw.githubusercontent.com/vega/vega/ff98519cce32b776a98d01dd982467d76fc9ee34/docs/docs/expressions.md" + + # NOTE: Regex patterns FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") @@ -354,7 +363,7 @@ def __call__(self, s: str, count: int = 0, /, refresh: bool = False) -> str: def _compile(self) -> Pattern[str]: if not self._mapping: - name = self._mapping.__qualname__ + name = self._mapping.__qualname__ # type: ignore[attr-defined] msg = ( f"Requires {name!r} to be populated, but got:\n" f"{name}={self._mapping!r}" @@ -922,11 +931,12 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: def test_parse() -> dict[str, VegaExprNode]: """Temporary introspection tool.""" - return {node.name: node for node in parse_expressions(EXPRESSIONS_URL)} + return {node.name: node for node in parse_expressions(Source.LIVE.value)} def render_expr_full() -> str: - it = (render_expr_method(node) for node in parse_expressions(EXPRESSIONS_URL)) + """Temporary sample of **pre-ruff** module.""" + it = (render_expr_method(node) for node in parse_expressions(Source.LIVE.value)) return "\n".join( chain( ( @@ -943,3 +953,48 @@ def render_expr_full() -> str: [EXPR_MODULE_POST], ) ) + + +def write_expr_module( + source_url: Literal["live", "static"] | str, output: Path +) -> None: + """ + Parse an ``expressions.md`` into a ``.py`` module. + + Parameters + ---------- + source_url + - ``"live"``: current version + - ``"static"``: most recent version available during testing + - Or provide an alternative as a ``str`` + output + Target path to write to. + """ + if source_url == "live": + url = Source.LIVE.value + elif source_url == "static": + url = Source.STATIC.value + else: + url = source_url + content = ( + EXPR_MODULE_PRE.format( + metaclass=CONST_META, + const=CONST_WRAPPER, + return_ann=RETURN_ANNOTATION, + input_ann=INPUT_ANNOTATION, + func=RETURN_WRAPPER, + ), + EXPR_CLS_TEMPLATE.format( + base="_ExprRef", + metaclass=CONST_META, + doc=EXPR_CLS_DOC, + type_ignore=IGNORE_MISC, + ), + ) + contents = chain( + content, + (render_expr_method(node) for node in parse_expressions(url)), + [EXPR_MODULE_POST], + ) + print(f"Generating\n {url!s}\n ->{output!s}") + _ruff_write_lint_format_str(output, contents) From 5e75051a1977016ca3b34f45f68e64f3031d25f2 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:39:11 +0100 Subject: [PATCH 41/77] build: run `generate-schema-wrapper` - Need to do some experimenting with a new test suite for `expr` - Temporarily isolating this to `expr.dummy.py` - Will later replace `expr.__init__.py` --- altair/expr/dummy.py | 1678 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1678 insertions(+) create mode 100644 altair/expr/dummy.py diff --git a/altair/expr/dummy.py b/altair/expr/dummy.py new file mode 100644 index 000000000..78b72f96e --- /dev/null +++ b/altair/expr/dummy.py @@ -0,0 +1,1678 @@ +"""Tools for creating transform & filter expressions with a python syntax.""" + +from __future__ import annotations + +import sys +from typing import Any + +from altair.expr.core import ( + ConstExpression, + Expression, + FunctionExpression, + IntoExpression, +) +from altair.vegalite.v5.schema.core import ExprRef as _ExprRef + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +class _ConstExpressionType(type): + """Metaclass providing read-only class properties for :class:`expr`.""" + + @property + def NaN(cls) -> Expression: + """Not a number (same as JavaScript literal NaN).""" + return ConstExpression("NaN") + + @property + def LN10(cls) -> Expression: + """The natural log of 10 (alias to Math.LN10).""" + return ConstExpression("LN10") + + @property + def E(cls) -> Expression: + """The transcendental number e (alias to Math.E).""" + return ConstExpression("E") + + @property + def LOG10E(cls) -> Expression: + """The base 10 logarithm e (alias to Math.LOG10E).""" + return ConstExpression("LOG10E") + + @property + def LOG2E(cls) -> Expression: + """The base 2 logarithm of e (alias to Math.LOG2E).""" + return ConstExpression("LOG2E") + + @property + def SQRT1_2(cls) -> Expression: + """The square root of 0.5 (alias to Math.SQRT1_2).""" + return ConstExpression("SQRT1_2") + + @property + def LN2(cls) -> Expression: + """The natural log of 2 (alias to Math.LN2).""" + return ConstExpression("LN2") + + @property + def SQRT2(cls) -> Expression: + """The square root of 2 (alias to Math.SQRT1_2).""" + return ConstExpression("SQRT2") + + @property + def PI(cls) -> Expression: + """The transcendental number pi (alias to Math.PI).""" + return ConstExpression("PI") + + +class expr(_ExprRef, metaclass=_ConstExpressionType): + """ + Utility providing *constants* and *classmethods* to construct expressions. + + `Expressions`_ can be used to write basic formulas that enable custom interactions. + + Alternatively, an `inline expression`_ may be defined via :class:`expr()`. + + Parameters + ---------- + expr: str + A `vega expression`_ string. + + Returns + ------- + ``ExprRef`` + + .. _Expressions: + https://altair-viz.github.io/user_guide/interactions.html#expressions + .. _inline expression: + https://altair-viz.github.io/user_guide/interactions.html#inline-expressions + .. _vega expression: + https://vega.github.io/vega/docs/expressions/ + + Examples + -------- + >>> import altair as alt + + >>> bind_range = alt.binding_range(min=100, max=300, name="Slider value: ") + >>> param_width = alt.param(bind=bind_range, name="param_width") + >>> param_color = alt.param( + ... expr=alt.expr.if_(param_width < 200, "red", "black"), + ... name="param_color", + ... ) + >>> y = alt.Y("yval").axis(titleColor=param_color) + + >>> y + Y({ + axis: {'titleColor': Parameter('param_color', VariableParameter({ + expr: if((param_width < 200),'red','black'), + name: 'param_color' + }))}, + shorthand: 'yval' + }) + """ + + @override + def __new__(cls: type[_ExprRef], expr: str) -> _ExprRef: # type: ignore[misc] + return _ExprRef(expr=expr) + + @classmethod + def isArray(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is an array, false otherwise.""" + return FunctionExpression("isArray", (value)) + + @classmethod + def isBoolean(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a boolean (``true`` or ``false``), false otherwise.""" + return FunctionExpression("isBoolean", (value)) + + @classmethod + def isDate(cls, value: IntoExpression, /) -> Expression: + """ + Returns true if ``value`` is a Date object, false otherwise. + + This method will return false for timestamp numbers or date-formatted strings; it recognizes + Date objects only. + """ + return FunctionExpression("isDate", (value)) + + @classmethod + def isDefined(cls, value: IntoExpression, /) -> Expression: + """ + Returns true if ``value`` is a defined value, false if ``value`` equals ``undefined``. + + This method will return true for ``null`` and ``NaN`` values. + """ + return FunctionExpression("isDefined", (value)) + + @classmethod + def isNumber(cls, value: IntoExpression, /) -> Expression: + """ + Returns true if ``value`` is a number, false otherwise. + + ``NaN`` and ``Infinity`` are considered numbers. + """ + return FunctionExpression("isNumber", (value)) + + @classmethod + def isObject(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is an object (including arrays and Dates), false otherwise.""" + return FunctionExpression("isObject", (value)) + + @classmethod + def isRegExp(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a RegExp (regular expression) object, false otherwise.""" + return FunctionExpression("isRegExp", (value)) + + @classmethod + def isString(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a string, false otherwise.""" + return FunctionExpression("isString", (value)) + + @classmethod + def isValid(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is not ``null``, ``undefined``, or ``NaN``, false otherwise.""" + return FunctionExpression("isValid", (value)) + + @classmethod + def toBoolean(cls, value: IntoExpression, /) -> Expression: + """ + Coerces the input ``value`` to a string. + + Null values and empty strings are mapped to ``null``. + """ + return FunctionExpression("toBoolean", (value)) + + @classmethod + def toDate(cls, value: IntoExpression, /) -> Expression: + """ + Coerces the input ``value`` to a Date instance. + + Null values and empty strings are mapped to ``null``. If an optional *parser* function is + provided, it is used to perform date parsing, otherwise ``Date.parse`` is used. Be aware + that ``Date.parse`` has different implementations across browsers! + """ + return FunctionExpression("toDate", (value)) + + @classmethod + def toNumber(cls, value: IntoExpression, /) -> Expression: + """ + Coerces the input ``value`` to a number. + + Null values and empty strings are mapped to ``null``. + """ + return FunctionExpression("toNumber", (value)) + + @classmethod + def toString(cls, value: IntoExpression, /) -> Expression: + """ + Coerces the input ``value`` to a string. + + Null values and empty strings are mapped to ``null``. + """ + return FunctionExpression("toString", (value)) + + @classmethod + def if_( + cls, + test: IntoExpression, + thenValue: IntoExpression, + elseValue: IntoExpression, + /, + ) -> Expression: + """ + If ``test`` is truthy, returns ``thenValue``. + + Otherwise, returns ``elseValue``. The *if* function is equivalent to the ternary operator + ``a ? b : c``. + """ + return FunctionExpression("if", (test, thenValue, elseValue)) + + @classmethod + def isNaN(cls, value: IntoExpression, /) -> Expression: + """ + Returns true if ``value`` is not a number. + + Same as JavaScript's `Number.isNaN`_. + + .. _Number.isNaN: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNan + """ + return FunctionExpression("isNaN", (value)) + + @classmethod + def isFinite(cls, value: IntoExpression, /) -> Expression: + """ + Returns true if ``value`` is a finite number. + + Same as JavaScript's `Number.isFinite`_. + + .. _Number.isFinite: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite + """ + return FunctionExpression("isFinite", (value)) + + @classmethod + def abs(cls, value: IntoExpression, /) -> Expression: + """ + Returns the absolute value of ``value``. + + Same as JavaScript's `Math.abs`_. + + .. _Math.abs: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs + """ + return FunctionExpression("abs", (value)) + + @classmethod + def acos(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric arccosine. + + Same as JavaScript's `Math.acos`_. + + .. _Math.acos: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos + """ + return FunctionExpression("acos", (value)) + + @classmethod + def asin(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric arcsine. + + Same as JavaScript's `Math.asin`_. + + .. _Math.asin: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin + """ + return FunctionExpression("asin", (value)) + + @classmethod + def atan(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric arctangent. + + Same as JavaScript's `Math.atan`_. + + .. _Math.atan: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan + """ + return FunctionExpression("atan", (value)) + + @classmethod + def atan2(cls, dy: IntoExpression, dx: IntoExpression, /) -> Expression: + """ + Returns the arctangent of *dy / dx*. + + Same as JavaScript's `Math.atan2`_. + + .. _Math.atan2: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 + """ + return FunctionExpression("atan2", (dy, dx)) + + @classmethod + def ceil(cls, value: IntoExpression, /) -> Expression: + """ + Rounds ``value`` to the nearest integer of equal or greater value. + + Same as JavaScript's `Math.ceil`_. + + .. _Math.ceil: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil + """ + return FunctionExpression("ceil", (value)) + + @classmethod + def clamp( + cls, value: IntoExpression, min: IntoExpression, max: IntoExpression, / + ) -> Expression: + """Restricts ``value`` to be between the specified ``min`` and ``max``.""" + return FunctionExpression("clamp", (value, min, max)) + + @classmethod + def cos(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric cosine. + + Same as JavaScript's `Math.cos`_. + + .. _Math.cos: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos + """ + return FunctionExpression("cos", (value)) + + @classmethod + def exp(cls, exponent: IntoExpression, /) -> Expression: + """ + Returns the value of *e* raised to the provided ``exponent``. + + Same as JavaScript's `Math.exp`_. + + .. _Math.exp: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp + """ + return FunctionExpression("exp", (exponent)) + + @classmethod + def floor(cls, value: IntoExpression, /) -> Expression: + """ + Rounds ``value`` to the nearest integer of equal or lower value. + + Same as JavaScript's `Math.floor`_. + + .. _Math.floor: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor + """ + return FunctionExpression("floor", (value)) + + @classmethod + def hypot(cls, value: IntoExpression, /) -> Expression: + """ + Returns the square root of the sum of squares of its arguments. + + Same as JavaScript's `Math.hypot`_. + + .. _Math.hypot: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot + """ + return FunctionExpression("hypot", (value)) + + @classmethod + def log(cls, value: IntoExpression, /) -> Expression: + """ + Returns the natural logarithm of ``value``. + + Same as JavaScript's `Math.log`_. + + .. _Math.log: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log + """ + return FunctionExpression("log", (value)) + + @classmethod + def max( + cls, value1: IntoExpression, value2: IntoExpression, *args: Any + ) -> Expression: + """ + Returns the maximum argument value. + + Same as JavaScript's `Math.max`_. + + .. _Math.max: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max + """ + return FunctionExpression("max", (value1, value2, *args)) + + @classmethod + def min( + cls, value1: IntoExpression, value2: IntoExpression, *args: Any + ) -> Expression: + """ + Returns the minimum argument value. + + Same as JavaScript's `Math.min`_. + + .. _Math.min: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min + """ + return FunctionExpression("min", (value1, value2, *args)) + + @classmethod + def pow(cls, value: IntoExpression, exponent: IntoExpression, /) -> Expression: + """ + Returns ``value`` raised to the given ``exponent``. + + Same as JavaScript's `Math.pow`_. + + .. _Math.pow: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow + """ + return FunctionExpression("pow", (value, exponent)) + + @classmethod + def round(cls, value: IntoExpression, /) -> Expression: + """ + Rounds ``value`` to the nearest integer. + + Same as JavaScript's `Math.round`_. + + .. _Math.round: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + """ + return FunctionExpression("round", (value)) + + @classmethod + def sin(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric sine. + + Same as JavaScript's `Math.sin`_. + + .. _Math.sin: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin + """ + return FunctionExpression("sin", (value)) + + @classmethod + def sqrt(cls, value: IntoExpression, /) -> Expression: + """ + Square root function. + + Same as JavaScript's `Math.sqrt`_. + + .. _Math.sqrt: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt + """ + return FunctionExpression("sqrt", (value)) + + @classmethod + def tan(cls, value: IntoExpression, /) -> Expression: + """ + Trigonometric tangent. + + Same as JavaScript's `Math.tan`_. + + .. _Math.tan: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan + """ + return FunctionExpression("tan", (value)) + + @classmethod + def sampleNormal( + cls, mean: IntoExpression = None, stdev: IntoExpression = None, / + ) -> Expression: + """ + Returns a sample from a univariate `normal (Gaussian) probability distribution`_ with specified ``mean`` and standard deviation ``stdev``. + + If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. + + .. _normal (Gaussian) probability distribution: + https://en.wikipedia.org/wiki/Normal_distribution + """ + return FunctionExpression("sampleNormal", (mean, stdev)) + + @classmethod + def sampleLogNormal( + cls, mean: IntoExpression = None, stdev: IntoExpression = None, / + ) -> Expression: + """ + Returns a sample from a univariate `log-normal probability distribution`_ with specified log ``mean`` and log standard deviation ``stdev``. + + If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to + ``1``. + + .. _log-normal probability distribution: + https://en.wikipedia.org/wiki/Log-normal_distribution + """ + return FunctionExpression("sampleLogNormal", (mean, stdev)) + + @classmethod + def sampleUniform( + cls, min: IntoExpression = None, max: IntoExpression = None, / + ) -> Expression: + """ + Returns a sample from a univariate `continuous uniform probability distribution`_) over the interval [``min``, ``max``). + + If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one + argument is provided, it is interpreted as the ``max`` value. + + .. _continuous uniform probability distribution: + https://en.wikipedia.org/wiki/Uniform_distribution_(continuous + """ + return FunctionExpression("sampleUniform", (min, max)) + + @classmethod + def datetime( + cls, + year: IntoExpression, + month: IntoExpression, + day: IntoExpression = None, + hour: IntoExpression = None, + min: IntoExpression = None, + sec: IntoExpression = None, + millisec: IntoExpression = None, + /, + ) -> Expression: + """ + Returns a new ``Date`` instance. + + The ``month`` is 0-based, such that ``1`` represents February. + """ + return FunctionExpression( + "datetime", (year, month, day, hour, min, sec, millisec) + ) + + @classmethod + def date(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the month for the given ``datetime`` value, in local time.""" + return FunctionExpression("date", (datetime)) + + @classmethod + def day(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the week for the given ``datetime`` value, in local time.""" + return FunctionExpression("day", (datetime)) + + @classmethod + def dayofyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the one-based day of the year for the given ``datetime`` value, in local time.""" + return FunctionExpression("dayofyear", (datetime)) + + @classmethod + def year(cls, datetime: IntoExpression, /) -> Expression: + """Returns the year for the given ``datetime`` value, in local time.""" + return FunctionExpression("year", (datetime)) + + @classmethod + def quarter(cls, datetime: IntoExpression, /) -> Expression: + """Returns the quarter of the year (0-3) for the given ``datetime`` value, in local time.""" + return FunctionExpression("quarter", (datetime)) + + @classmethod + def month(cls, datetime: IntoExpression, /) -> Expression: + """Returns the (zero-based) month for the given ``datetime`` value, in local time.""" + return FunctionExpression("month", (datetime)) + + @classmethod + def week(cls, date: IntoExpression, /) -> Expression: + """ + Returns the week number of the year for the given *datetime*, in local time. + + This function assumes Sunday-based weeks. Days before the first Sunday of the year are + considered to be in week 0, the first Sunday of the year is the start of week 1, the second + Sunday week 2, *etc.*. + """ + return FunctionExpression("week", (date)) + + @classmethod + def hours(cls, datetime: IntoExpression, /) -> Expression: + """Returns the hours component for the given ``datetime`` value, in local time.""" + return FunctionExpression("hours", (datetime)) + + @classmethod + def minutes(cls, datetime: IntoExpression, /) -> Expression: + """Returns the minutes component for the given ``datetime`` value, in local time.""" + return FunctionExpression("minutes", (datetime)) + + @classmethod + def seconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the seconds component for the given ``datetime`` value, in local time.""" + return FunctionExpression("seconds", (datetime)) + + @classmethod + def milliseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the milliseconds component for the given ``datetime`` value, in local time.""" + return FunctionExpression("milliseconds", (datetime)) + + @classmethod + def time(cls, datetime: IntoExpression, /) -> Expression: + """Returns the epoch-based timestamp for the given ``datetime`` value.""" + return FunctionExpression("time", (datetime)) + + @classmethod + def timezoneoffset(cls, datetime: IntoExpression, /) -> Expression: + """Returns the timezone offset from the local timezone to UTC for the given ``datetime`` value.""" + return FunctionExpression("timezoneoffset", (datetime)) + + @classmethod + def timeOffset( + cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / + ) -> Expression: + """ + Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in the local timezone. + + The optional ``step`` argument indicates the number of time unit steps to offset by (default + 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units + """ + return FunctionExpression("timeOffset", (unit, date, step)) + + @classmethod + def timeSequence( + cls, + unit: IntoExpression, + start: IntoExpression, + stop: IntoExpression, + step: IntoExpression = None, + /, + ) -> Expression: + """ + Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in the local timezone. + + The optional ``step`` argument indicates the number of time unit steps to take between each + sequence entry (default 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units + """ + return FunctionExpression("timeSequence", (unit, start, stop, step)) + + @classmethod + def utc( + cls, + year: IntoExpression, + month: IntoExpression, + day: IntoExpression = None, + hour: IntoExpression = None, + min: IntoExpression = None, + sec: IntoExpression = None, + millisec: IntoExpression = None, + /, + ) -> Expression: + """ + Returns a timestamp for the given UTC date. + + The ``month`` is 0-based, such that ``1`` represents February. + """ + return FunctionExpression("utc", (year, month, day, hour, min, sec, millisec)) + + @classmethod + def utcdate(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the month for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcdate", (datetime)) + + @classmethod + def utcday(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the week for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcday", (datetime)) + + @classmethod + def utcdayofyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the one-based day of the year for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcdayofyear", (datetime)) + + @classmethod + def utcyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the year for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcyear", (datetime)) + + @classmethod + def utcquarter(cls, datetime: IntoExpression, /) -> Expression: + """Returns the quarter of the year (0-3) for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcquarter", (datetime)) + + @classmethod + def utcmonth(cls, datetime: IntoExpression, /) -> Expression: + """Returns the (zero-based) month for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcmonth", (datetime)) + + @classmethod + def utcweek(cls, date: IntoExpression, /) -> Expression: + """ + Returns the week number of the year for the given *datetime*, in UTC time. + + This function assumes Sunday-based weeks. Days before the first Sunday of the year are + considered to be in week 0, the first Sunday of the year is the start of week 1, the second + Sunday week 2, *etc.*. + """ + return FunctionExpression("utcweek", (date)) + + @classmethod + def utchours(cls, datetime: IntoExpression, /) -> Expression: + """Returns the hours component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utchours", (datetime)) + + @classmethod + def utcminutes(cls, datetime: IntoExpression, /) -> Expression: + """Returns the minutes component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcminutes", (datetime)) + + @classmethod + def utcseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the seconds component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcseconds", (datetime)) + + @classmethod + def utcmilliseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the milliseconds component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcmilliseconds", (datetime)) + + @classmethod + def utcOffset( + cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / + ) -> Expression: + """ + Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in UTC time. + + The optional ``step`` argument indicates the number of time unit steps to offset by (default + 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units + """ + return FunctionExpression("utcOffset", (unit, date, step)) + + @classmethod + def utcSequence( + cls, + unit: IntoExpression, + start: IntoExpression, + stop: IntoExpression, + step: IntoExpression = None, + /, + ) -> Expression: + """ + Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in UTC time. + + The optional ``step`` argument indicates the number of time unit steps to take between each + sequence entry (default 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units + """ + return FunctionExpression("utcSequence", (unit, start, stop, step)) + + @classmethod + def extent(cls, array: IntoExpression, /) -> Expression: + """Returns a new *[min, max]* array with the minimum and maximum values of the input array, ignoring ``null``, ``undefined``, and ``NaN`` values.""" + return FunctionExpression("extent", (array)) + + @classmethod + def clampRange( + cls, range: IntoExpression, min: IntoExpression, max: IntoExpression, / + ) -> Expression: + """ + Clamps a two-element ``range`` array in a span-preserving manner. + + If the span of the input ``range`` is less than *(max - min)* and an endpoint exceeds either + the ``min`` or ``max`` value, the range is translated such that the span is preserved and + one endpoint touches the boundary of the *[min, max]* range. If the span exceeds *(max - + min)*, the range *[min, max]* is returned. + """ + return FunctionExpression("clampRange", (range, min, max)) + + @classmethod + def indexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: + """Returns the first index of ``value`` in the input ``array``.""" + return FunctionExpression("indexof", (array, value)) + + @classmethod + def inrange(cls, value: IntoExpression, range: IntoExpression, /) -> Expression: + """Tests whether ``value`` lies within (or is equal to either) the first and last values of the ``range`` array.""" + return FunctionExpression("inrange", (value, range)) + + @classmethod + def join( + cls, array: IntoExpression, separator: IntoExpression = None, / + ) -> Expression: + """Returns a new string by concatenating all of the elements of the input ``array``, separated by commas or a specified ``separator`` string.""" + return FunctionExpression("join", (array, separator)) + + @classmethod + def lastindexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: + """Returns the last index of ``value`` in the input ``array``.""" + return FunctionExpression("lastindexof", (array, value)) + + @classmethod + def length(cls, array: IntoExpression, /) -> Expression: + """Returns the length of the input ``array``.""" + return FunctionExpression("length", (array)) + + @classmethod + def lerp(cls, array: IntoExpression, fraction: IntoExpression, /) -> Expression: + """ + Returns the linearly interpolated value between the first and last entries in the ``array`` for the provided interpolation ``fraction`` (typically between 0 and 1). + + For example, ``alt.expr.lerp([0, 50], 0.5)`` returns 25. + """ + return FunctionExpression("lerp", (array, fraction)) + + @classmethod + def peek(cls, array: IntoExpression, /) -> Expression: + """ + Returns the last element in the input ``array``. + + Similar to the built-in ``Array.pop`` method, except that it does not remove the last + element. This method is a convenient shorthand for ``array[array.length - 1]``. + """ + return FunctionExpression("peek", (array)) + + @classmethod + def pluck(cls, array: IntoExpression, field: IntoExpression, /) -> Expression: + """ + Retrieves the value for the specified ``field`` from a given ``array`` of objects. + + The input ``field`` string may include nested properties (e.g., ``foo.bar.bz``). + """ + return FunctionExpression("pluck", (array, field)) + + @classmethod + def reverse(cls, array: IntoExpression, /) -> Expression: + """ + Returns a new array with elements in a reverse order of the input ``array``. + + The first array element becomes the last, and the last array element becomes the first. + """ + return FunctionExpression("reverse", (array)) + + @classmethod + def sequence(cls, *args: Any) -> Expression: + """ + Returns an array containing an arithmetic sequence of numbers. + + If ``step`` is omitted, it defaults to 1. If ``start`` is omitted, it defaults to 0. The + ``stop`` value is exclusive; it is not included in the result. If ``step`` is positive, the + last element is the largest *start + i * step* less than ``stop``; if ``step`` is negative, + the last element is the smallest *start + i * step* greater than ``stop``. If the returned + array would contain an infinite number of values, an empty range is returned. The arguments + are not required to be integers. + """ + return FunctionExpression("sequence", args) + + @classmethod + def slice( + cls, array: IntoExpression, start: IntoExpression, end: IntoExpression = None, / + ) -> Expression: + """ + Returns a section of ``array`` between the ``start`` and ``end`` indices. + + If the ``end`` argument is negative, it is treated as an offset from the end of the array + (*alt.expr.length(array) + end*). + """ + return FunctionExpression("slice", (array, start, end)) + + @classmethod + def span(cls, array: IntoExpression, /) -> Expression: + """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" + return FunctionExpression("span", (array)) + + @classmethod + def lower(cls, string: IntoExpression, /) -> Expression: + """Transforms ``string`` to lower-case letters.""" + return FunctionExpression("lower", (string)) + + @classmethod + def pad( + cls, + string: IntoExpression, + length: IntoExpression, + character: IntoExpression = None, + align: IntoExpression = None, + /, + ) -> Expression: + """ + Pads a ``string`` value with repeated instances of a ``character`` up to a specified ``length``. + + If ``character`` is not specified, a space (' ') is used. By default, padding is added to + the end of a string. An optional ``align`` parameter specifies if padding should be added to + the ``'left'`` (beginning), ``'center'``, or ``'right'`` (end) of the input string. + """ + return FunctionExpression("pad", (string, length, character, align)) + + @classmethod + def parseFloat(cls, string: IntoExpression, /) -> Expression: + """ + Parses the input ``string`` to a floating-point value. + + Same as JavaScript's ``parseFloat``. + """ + return FunctionExpression("parseFloat", (string)) + + @classmethod + def parseInt(cls, string: IntoExpression, /) -> Expression: + """ + Parses the input ``string`` to an integer value. + + Same as JavaScript's ``parseInt``. + """ + return FunctionExpression("parseInt", (string)) + + @classmethod + def replace( + cls, + string: IntoExpression, + pattern: IntoExpression, + replacement: IntoExpression, + /, + ) -> Expression: + """ + Returns a new string with some or all matches of ``pattern`` replaced by a ``replacement`` string. + + The ``pattern`` can be a string or a regular expression. If ``pattern`` is a string, only + the first instance will be replaced. Same as `JavaScript's String.replace`_. + + .. _JavaScript's String.replace: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + """ + return FunctionExpression("replace", (string, pattern, replacement)) + + @classmethod + def substring( + cls, + string: IntoExpression, + start: IntoExpression, + end: IntoExpression = None, + /, + ) -> Expression: + """Returns a section of ``string`` between the ``start`` and ``end`` indices.""" + return FunctionExpression("substring", (string, start, end)) + + @classmethod + def trim(cls, string: IntoExpression, /) -> Expression: + """Returns a trimmed string with preceding and trailing whitespace removed.""" + return FunctionExpression("trim", (string)) + + @classmethod + def truncate( + cls, + string: IntoExpression, + length: IntoExpression, + align: IntoExpression = None, + ellipsis: IntoExpression = None, + /, + ) -> Expression: + """ + Truncates an input ``string`` to a target ``length``. + + The optional ``align`` argument indicates what part of the string should be truncated: + ``'left'`` (the beginning), ``'center'``, or ``'right'`` (the end). By default, the + ``'right'`` end of the string is truncated. The optional ``ellipsis`` argument indicates the + string to use to indicate truncated content; by default the ellipsis character ``…`` + (``\u2026``) is used. + """ + return FunctionExpression("truncate", (string, length, align, ellipsis)) + + @classmethod + def upper(cls, string: IntoExpression, /) -> Expression: + """Transforms ``string`` to upper-case letters.""" + return FunctionExpression("upper", (string)) + + @classmethod + def merge( + cls, object1: IntoExpression, object2: IntoExpression = None, *args: Any + ) -> Expression: + """ + Merges the input objects ``object1``, ``object2``, etc into a new output object. + + Inputs are visited in sequential order, such that key values from later arguments can + overwrite those from earlier arguments. Example: ``alt.expr.merge({a:1, b:2}, {a:3}) -> + {a:3, b:2}``. + """ + return FunctionExpression("merge", (object1, object2, *args)) + + @classmethod + def dayFormat(cls, day: IntoExpression, /) -> Expression: + """ + Formats a (0-6) *weekday* number as a full week day name, according to the current locale. + + For example: ``alt.expr.dayFormat(0) -> "Sunday"``. + """ + return FunctionExpression("dayFormat", (day)) + + @classmethod + def dayAbbrevFormat(cls, day: IntoExpression, /) -> Expression: + """ + Formats a (0-6) *weekday* number as an abbreviated week day name, according to the current locale. + + For example: ``alt.expr.dayAbbrevFormat(0) -> "Sun"``. + """ + return FunctionExpression("dayAbbrevFormat", (day)) + + @classmethod + def format(cls, value: IntoExpression, specifier: IntoExpression, /) -> Expression: + """ + Formats a numeric ``value`` as a string. + + The ``specifier`` must be a valid `d3-format specifier`_ (e.g., ``alt.expr.format(value, + ',.2f')``. Null values are formatted as ``"null"``. + + .. _d3-format specifier: + https://github.com/d3/d3-format/ + """ + return FunctionExpression("format", (value, specifier)) + + @classmethod + def monthFormat(cls, month: IntoExpression, /) -> Expression: + """ + Formats a (zero-based) ``month`` number as a full month name, according to the current locale. + + For example: ``alt.expr.monthFormat(0) -> "January"``. + """ + return FunctionExpression("monthFormat", (month)) + + @classmethod + def monthAbbrevFormat(cls, month: IntoExpression, /) -> Expression: + """ + Formats a (zero-based) ``month`` number as an abbreviated month name, according to the current locale. + + For example: ``alt.expr.monthAbbrevFormat(0) -> "Jan"``. + """ + return FunctionExpression("monthAbbrevFormat", (month)) + + @classmethod + def timeUnitSpecifier( + cls, units: IntoExpression, specifiers: IntoExpression = None, / + ) -> Expression: + """ + Returns a time format specifier string for the given time `*units*`_. + + The optional ``specifiers`` object provides a set of specifier sub-strings for customizing + the format; for more, see the `timeUnitSpecifier API documentation`_. The resulting + specifier string can then be used as input to the `timeFormat`_ or `utcFormat`_ functions, + or as the *format* parameter of an axis or legend. For example: ``alt.expr.timeFormat(date, + alt.expr.timeUnitSpecifier('year'))`` or ``alt.expr.timeFormat(date, + alt.expr.timeUnitSpecifier(['hours', 'minutes']))``. + + .. _*units*: + https://vega.github.io/vega/docs/api/time/#time-units + .. _timeUnitSpecifier API documentation: + https://vega.github.io/vega/docs/api/time/#timeUnitSpecifier + .. _timeFormat: + https://vega.github.io/vega/docs/expressions/#timeFormat + .. _utcFormat: + https://vega.github.io/vega/docs/expressions/#utcFormat + """ + return FunctionExpression("timeUnitSpecifier", (units, specifiers)) + + @classmethod + def timeFormat( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: + """ + Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to the local time. + + The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. + For example: ``alt.expr.timeFormat(timestamp, '%A')``. Null values are formatted as + ``"null"``. + + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + .. _TimeMultiFormat object: + https://vega.github.io/vega/docs/types/#TimeMultiFormat + """ + return FunctionExpression("timeFormat", (value, specifier)) + + @classmethod + def timeParse( + cls, string: IntoExpression, specifier: IntoExpression, / + ) -> Expression: + """ + Parses a ``string`` value to a Date object, according to the local time. + + The ``specifier`` must be a valid `d3-time-format specifier`_. For example: + ``alt.expr.timeParse('June 30, 2015', '%B %d, %Y')``. + + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + """ + return FunctionExpression("timeParse", (string, specifier)) + + @classmethod + def utcFormat( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: + """ + Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to `UTC`_ time. + + The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. + For example: ``alt.expr.utcFormat(timestamp, '%A')``. Null values are formatted as + ``"null"``. + + .. _UTC: + https://en.wikipedia.org/wiki/Coordinated_Universal_Time + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + .. _TimeMultiFormat object: + https://vega.github.io/vega/docs/types/#TimeMultiFormat + """ + return FunctionExpression("utcFormat", (value, specifier)) + + @classmethod + def utcParse( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: + """ + Parses a *string* value to a Date object, according to `UTC`_ time. + + The ``specifier`` must be a valid `d3-time-format specifier`_. For example: + ``alt.expr.utcParse('June 30, 2015', '%B %d, %Y')``. + + .. _UTC: + https://en.wikipedia.org/wiki/Coordinated_Universal_Time + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + """ + return FunctionExpression("utcParse", (value, specifier)) + + @classmethod + def regexp( + cls, pattern: IntoExpression, flags: IntoExpression = None, / + ) -> Expression: + """ + Creates a regular expression instance from an input ``pattern`` string and optional ``flags``. + + Same as `JavaScript's RegExp`_. + + .. _JavaScript's RegExp: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp + """ + return FunctionExpression("regexp", (pattern, flags)) + + @classmethod + def test( + cls, regexp: IntoExpression, string: IntoExpression = None, / + ) -> Expression: + r""" + Evaluates a regular expression ``regexp`` against the input ``string``, returning ``true`` if the string matches the pattern, ``false`` otherwise. + + For example: ``alt.expr.test(/\\d{3}/, "32-21-9483") -> true``. + """ + return FunctionExpression("test", (regexp, string)) + + @classmethod + def rgb(cls, *args: Any) -> Expression: + """ + Constructs a new `RGB`_ color. + + If ``r``, ``g`` and ``b`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the RGB color space. Uses + `d3-color's rgb function`_. + + .. _RGB: + https://en.wikipedia.org/wiki/RGB_color_model + .. _d3-color's rgb function: + https://github.com/d3/d3-color#rgb + """ + return FunctionExpression("rgb", args) + + @classmethod + def hsl(cls, *args: Any) -> Expression: + """ + Constructs a new `HSL`_ color. + + If ``h``, ``s`` and ``l`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the HSL color space. Uses + `d3-color's hsl function`_. + + .. _HSL: + https://en.wikipedia.org/wiki/HSL_and_HSV + .. _d3-color's hsl function: + https://github.com/d3/d3-color#hsl + """ + return FunctionExpression("hsl", args) + + @classmethod + def lab(cls, *args: Any) -> Expression: + """ + Constructs a new `CIE LAB`_ color. + + If ``l``, ``a`` and ``b`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the LAB color space. Uses + `d3-color's lab function`_. + + .. _CIE LAB: + https://en.wikipedia.org/wiki/Lab_color_space#CIELAB + .. _d3-color's lab function: + https://github.com/d3/d3-color#lab + """ + return FunctionExpression("lab", args) + + @classmethod + def hcl(cls, *args: Any) -> Expression: + """ + Constructs a new `HCL`_ (hue, chroma, luminance) color. + + If ``h``, ``c`` and ``l`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the HCL color space. Uses + `d3-color's hcl function`_. + + .. _HCL: + https://en.wikipedia.org/wiki/Lab_color_space#CIELAB + .. _d3-color's hcl function: + https://github.com/d3/d3-color#hcl + """ + return FunctionExpression("hcl", args) + + @classmethod + def group(cls, name: IntoExpression = None, /) -> Expression: + """ + Returns the scenegraph group mark item in which the current event has occurred. + + If no arguments are provided, the immediate parent group is returned. If a group name is + provided, the matching ancestor group item is returned. + """ + return FunctionExpression("group", (name)) + + @classmethod + def xy(cls, item: IntoExpression = None, /) -> Expression: + """ + Returns the x- and y-coordinates for the current event as a two-element array. + + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. + """ + return FunctionExpression("xy", (item)) + + @classmethod + def x(cls, item: IntoExpression = None, /) -> Expression: + """ + Returns the x coordinate for the current event. + + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. + """ + return FunctionExpression("x", (item)) + + @classmethod + def y(cls, item: IntoExpression = None, /) -> Expression: + """ + Returns the y coordinate for the current event. + + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. + """ + return FunctionExpression("y", (item)) + + @classmethod + def pinchDistance(cls, event: IntoExpression, /) -> Expression: + """Returns the pixel distance between the first two touch points of a multi-touch event.""" + return FunctionExpression("pinchDistance", (event)) + + @classmethod + def pinchAngle(cls, event: IntoExpression, /) -> Expression: + """Returns the angle of the line connecting the first two touch points of a multi-touch event.""" + return FunctionExpression("pinchAngle", (event)) + + @classmethod + def inScope(cls, item: IntoExpression, /) -> Expression: + """Returns true if the given scenegraph ``item`` is a descendant of the group mark in which the event handler was defined, false otherwise.""" + return FunctionExpression("inScope", (item)) + + @classmethod + def data(cls, name: IntoExpression, /) -> Expression: + """ + Returns the array of data objects for the Vega data set with the given ``name``. + + If the data set is not found, returns an empty array. + """ + return FunctionExpression("data", (name)) + + @classmethod + def indata( + cls, name: IntoExpression, field: IntoExpression, value: IntoExpression, / + ) -> Expression: + """ + Tests if the data set with a given ``name`` contains a datum with a ``field`` value that matches the input ``value``. + + For example: ``alt.expr.indata('table', 'category', value)``. + """ + return FunctionExpression("indata", (name, field, value)) + + @classmethod + def scale( + cls, + name: IntoExpression, + value: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: + """ + Applies the named scale transform (or projection) to the specified ``value``. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. + """ + return FunctionExpression("scale", (name, value, group)) + + @classmethod + def invert( + cls, + name: IntoExpression, + value: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: + """ + Inverts the named scale transform (or projection) for the specified ``value``. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. + """ + return FunctionExpression("invert", (name, value, group)) + + @classmethod + def copy(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: # type: ignore[override] + """ + Returns a copy (a new cloned instance) of the named scale transform of projection, or ``undefined`` if no scale or projection is found. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. + """ + return FunctionExpression("copy", (name, group)) + + @classmethod + def domain( + cls, name: IntoExpression, group: IntoExpression = None, / + ) -> Expression: + """ + Returns the scale domain array for the named scale transform, or an empty array if the scale is not found. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. + """ + return FunctionExpression("domain", (name, group)) + + @classmethod + def range(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: + """ + Returns the scale range array for the named scale transform, or an empty array if the scale is not found. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. + """ + return FunctionExpression("range", (name, group)) + + @classmethod + def bandwidth( + cls, name: IntoExpression, group: IntoExpression = None, / + ) -> Expression: + """ + Returns the current band width for the named band scale transform, or zero if the scale is not found or is not a band scale. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. + """ + return FunctionExpression("bandwidth", (name, group)) + + @classmethod + def bandspace( + cls, + count: IntoExpression, + paddingInner: IntoExpression = None, + paddingOuter: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the number of steps needed within a band scale, based on the ``count`` of domain elements and the inner and outer padding values. + + While normally calculated within the scale itself, this function can be helpful for + determining the size of a chart's layout. + """ + return FunctionExpression("bandspace", (count, paddingInner, paddingOuter)) + + @classmethod + def gradient( + cls, + scale: IntoExpression, + p0: IntoExpression, + p1: IntoExpression, + count: IntoExpression = None, + /, + ) -> Expression: + """ + Returns a linear color gradient for the ``scale`` (whose range must be a `continuous color scheme`_) and starting and ending points ``p0`` and ``p1``, each an *[x, y]* array. + + The points ``p0`` and ``p1`` should be expressed in normalized coordinates in the domain [0, + 1], relative to the bounds of the item being colored. If unspecified, ``p0`` defaults to + ``[0, 0]`` and ``p1`` defaults to ``[1, 0]``, for a horizontal gradient that spans the full + bounds of an item. The optional ``count`` argument indicates a desired target number of + sample points to take from the color scale. + + .. _continuous color scheme: + https://vega.github.io/vega/docs/schemes + """ + return FunctionExpression("gradient", (scale, p0, p1, count)) + + @classmethod + def panLinear(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: + """ + Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. + + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. + """ + return FunctionExpression("panLinear", (domain, delta)) + + @classmethod + def panLog(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: + """ + Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. + + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. + """ + return FunctionExpression("panLog", (domain, delta)) + + @classmethod + def panPow( + cls, domain: IntoExpression, delta: IntoExpression, exponent: IntoExpression, / + ) -> Expression: + """ + Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. + + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. + """ + return FunctionExpression("panPow", (domain, delta, exponent)) + + @classmethod + def panSymlog( + cls, domain: IntoExpression, delta: IntoExpression, constant: IntoExpression, / + ) -> Expression: + """ + Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. + + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. + """ + return FunctionExpression("panSymlog", (domain, delta, constant)) + + @classmethod + def zoomLinear( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + /, + ) -> Expression: + """ + Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. + + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. + """ + return FunctionExpression("zoomLinear", (domain, anchor, scaleFactor)) + + @classmethod + def zoomLog( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + /, + ) -> Expression: + """ + Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. + + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. + """ + return FunctionExpression("zoomLog", (domain, anchor, scaleFactor)) + + @classmethod + def zoomPow( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + exponent: IntoExpression, + /, + ) -> Expression: + """ + Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. + + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. + """ + return FunctionExpression("zoomPow", (domain, anchor, scaleFactor, exponent)) + + @classmethod + def zoomSymlog( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + constant: IntoExpression, + /, + ) -> Expression: + """ + Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. + + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. + """ + return FunctionExpression("zoomSymlog", (domain, anchor, scaleFactor, constant)) + + @classmethod + def geoArea( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the projected planar area (typically in square pixels) of a GeoJSON ``feature`` according to the named ``projection``. + + If the ``projection`` argument is ``null``, computes the spherical area in steradians using + unprojected longitude, latitude coordinates. The optional ``group`` argument takes a + scenegraph group mark item to indicate the specific scope in which to look up the + projection. Uses d3-geo's `geoArea`_ and `path.area`_ methods. + + .. _geoArea: + https://github.com/d3/d3-geo#geoArea + .. _path.area: + https://github.com/d3/d3-geo#path_area + """ + return FunctionExpression("geoArea", (projection, feature, group)) + + @classmethod + def geoBounds( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. + + The bounding box is represented by a two-dimensional array: [[*x₀*, *y₀*], [*x₁*, *y₁*]], + where *x₀* is the minimum x-coordinate, *y₀* is the minimum y-coordinate, *x₁* is the + maximum x-coordinate, and *y₁* is the maximum y-coordinate. If the ``projection`` argument + is ``null``, computes the spherical bounding box using unprojected longitude, latitude + coordinates. The optional ``group`` argument takes a scenegraph group mark item to indicate + the specific scope in which to look up the projection. Uses d3-geo's `geoBounds`_ and + `path.bounds`_ methods. + + .. _geoBounds: + https://github.com/d3/d3-geo#geoBounds + .. _path.bounds: + https://github.com/d3/d3-geo#path_bounds + """ + return FunctionExpression("geoBounds", (projection, feature, group)) + + @classmethod + def geoCentroid( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the projected planar centroid (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. + + If the ``projection`` argument is ``null``, computes the spherical centroid using + unprojected longitude, latitude coordinates. The optional ``group`` argument takes a + scenegraph group mark item to indicate the specific scope in which to look up the + projection. Uses d3-geo's `geoCentroid`_ and `path.centroid`_ methods. + + .. _geoCentroid: + https://github.com/d3/d3-geo#geoCentroid + .. _path.centroid: + https://github.com/d3/d3-geo#path_centroid + """ + return FunctionExpression("geoCentroid", (projection, feature, group)) + + @classmethod + def geoScale( + cls, projection: IntoExpression, group: IntoExpression = None, / + ) -> Expression: + """ + Returns the scale value for the named ``projection``. + + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the projection. + """ + return FunctionExpression("geoScale", (projection, group)) + + @classmethod + def treePath( + cls, name: IntoExpression, source: IntoExpression, target: IntoExpression, / + ) -> Expression: + """ + For the hierarchy data set with the given ``name``, returns the shortest path through from the ``source`` node id to the ``target`` node id. + + The path starts at the ``source`` node, ascends to the least common ancestor of the + ``source`` node and the ``target`` node, and then descends to the ``target`` node. + """ + return FunctionExpression("treePath", (name, source, target)) + + @classmethod + def treeAncestors(cls, name: IntoExpression, node: IntoExpression, /) -> Expression: + """For the hierarchy data set with the given ``name``, returns the array of ancestors nodes, starting with the input ``node``, then followed by each parent up to the root.""" + return FunctionExpression("treeAncestors", (name, node)) + + @classmethod + def warn( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: + """ + Logs a warning message and returns the last argument. + + For the message to appear in the console, the visualization view must have the appropriate + logging level set. + """ + return FunctionExpression("warn", (value1, value2, *args)) + + @classmethod + def info( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: + """ + Logs an informative message and returns the last argument. + + For the message to appear in the console, the visualization view must have the appropriate + logging level set. + """ + return FunctionExpression("info", (value1, value2, *args)) + + @classmethod + def debug( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: + """ + Logs a debugging message and returns the last argument. + + For the message to appear in the console, the visualization view must have the appropriate + logging level set. + """ + return FunctionExpression("debug", (value1, value2, *args)) + + +_ExprType = expr +# NOTE: Compatibility alias for previous type of `alt.expr`. +# `_ExprType` was not referenced in any internal imports/tests. From 222d03e1462513f4898933d209d4c5b62532875c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:10:10 +0100 Subject: [PATCH 42/77] chore: Add note to `test_expr.py` Need to make a change here to start a GH thread --- tests/expr/test_expr.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index 8842e0ced..8aa97f30a 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -88,7 +88,14 @@ def test_abs(): @pytest.mark.parametrize(("veganame", "methodname"), _remap_classmethod_names(expr)) def test_expr_funcs(veganame: str, methodname: str): - """Test all functions defined in expr.funcs.""" + """ + Test all functions defined in expr.funcs. + + # FIXME: These tests are no longer suitable + They only work for functions with a **single** argument: + + TypeError: expr.if_() missing 2 required positional arguments: 'thenValue' and 'elseValue'. + """ func = getattr(expr, methodname) z = func(datum.xxx) assert repr(z) == f"{veganame}(datum.xxx)" From f7a47a54af99df1da596b806eeb054a6e791e1bf Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 21:17:06 +0100 Subject: [PATCH 43/77] fix: Add trailing comma to single arg methods --- altair/expr/dummy.py | 150 ++++++++++++++++++------------------ tools/schemapi/vega_expr.py | 6 +- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/altair/expr/dummy.py b/altair/expr/dummy.py index 78b72f96e..bc7e743a6 100644 --- a/altair/expr/dummy.py +++ b/altair/expr/dummy.py @@ -121,12 +121,12 @@ def __new__(cls: type[_ExprRef], expr: str) -> _ExprRef: # type: ignore[misc] @classmethod def isArray(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is an array, false otherwise.""" - return FunctionExpression("isArray", (value)) + return FunctionExpression("isArray", (value,)) @classmethod def isBoolean(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is a boolean (``true`` or ``false``), false otherwise.""" - return FunctionExpression("isBoolean", (value)) + return FunctionExpression("isBoolean", (value,)) @classmethod def isDate(cls, value: IntoExpression, /) -> Expression: @@ -136,7 +136,7 @@ def isDate(cls, value: IntoExpression, /) -> Expression: This method will return false for timestamp numbers or date-formatted strings; it recognizes Date objects only. """ - return FunctionExpression("isDate", (value)) + return FunctionExpression("isDate", (value,)) @classmethod def isDefined(cls, value: IntoExpression, /) -> Expression: @@ -145,7 +145,7 @@ def isDefined(cls, value: IntoExpression, /) -> Expression: This method will return true for ``null`` and ``NaN`` values. """ - return FunctionExpression("isDefined", (value)) + return FunctionExpression("isDefined", (value,)) @classmethod def isNumber(cls, value: IntoExpression, /) -> Expression: @@ -154,27 +154,27 @@ def isNumber(cls, value: IntoExpression, /) -> Expression: ``NaN`` and ``Infinity`` are considered numbers. """ - return FunctionExpression("isNumber", (value)) + return FunctionExpression("isNumber", (value,)) @classmethod def isObject(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is an object (including arrays and Dates), false otherwise.""" - return FunctionExpression("isObject", (value)) + return FunctionExpression("isObject", (value,)) @classmethod def isRegExp(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is a RegExp (regular expression) object, false otherwise.""" - return FunctionExpression("isRegExp", (value)) + return FunctionExpression("isRegExp", (value,)) @classmethod def isString(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is a string, false otherwise.""" - return FunctionExpression("isString", (value)) + return FunctionExpression("isString", (value,)) @classmethod def isValid(cls, value: IntoExpression, /) -> Expression: """Returns true if ``value`` is not ``null``, ``undefined``, or ``NaN``, false otherwise.""" - return FunctionExpression("isValid", (value)) + return FunctionExpression("isValid", (value,)) @classmethod def toBoolean(cls, value: IntoExpression, /) -> Expression: @@ -183,7 +183,7 @@ def toBoolean(cls, value: IntoExpression, /) -> Expression: Null values and empty strings are mapped to ``null``. """ - return FunctionExpression("toBoolean", (value)) + return FunctionExpression("toBoolean", (value,)) @classmethod def toDate(cls, value: IntoExpression, /) -> Expression: @@ -194,7 +194,7 @@ def toDate(cls, value: IntoExpression, /) -> Expression: provided, it is used to perform date parsing, otherwise ``Date.parse`` is used. Be aware that ``Date.parse`` has different implementations across browsers! """ - return FunctionExpression("toDate", (value)) + return FunctionExpression("toDate", (value,)) @classmethod def toNumber(cls, value: IntoExpression, /) -> Expression: @@ -203,7 +203,7 @@ def toNumber(cls, value: IntoExpression, /) -> Expression: Null values and empty strings are mapped to ``null``. """ - return FunctionExpression("toNumber", (value)) + return FunctionExpression("toNumber", (value,)) @classmethod def toString(cls, value: IntoExpression, /) -> Expression: @@ -212,7 +212,7 @@ def toString(cls, value: IntoExpression, /) -> Expression: Null values and empty strings are mapped to ``null``. """ - return FunctionExpression("toString", (value)) + return FunctionExpression("toString", (value,)) @classmethod def if_( @@ -240,7 +240,7 @@ def isNaN(cls, value: IntoExpression, /) -> Expression: .. _Number.isNaN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNan """ - return FunctionExpression("isNaN", (value)) + return FunctionExpression("isNaN", (value,)) @classmethod def isFinite(cls, value: IntoExpression, /) -> Expression: @@ -252,7 +252,7 @@ def isFinite(cls, value: IntoExpression, /) -> Expression: .. _Number.isFinite: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite """ - return FunctionExpression("isFinite", (value)) + return FunctionExpression("isFinite", (value,)) @classmethod def abs(cls, value: IntoExpression, /) -> Expression: @@ -264,7 +264,7 @@ def abs(cls, value: IntoExpression, /) -> Expression: .. _Math.abs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs """ - return FunctionExpression("abs", (value)) + return FunctionExpression("abs", (value,)) @classmethod def acos(cls, value: IntoExpression, /) -> Expression: @@ -276,7 +276,7 @@ def acos(cls, value: IntoExpression, /) -> Expression: .. _Math.acos: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos """ - return FunctionExpression("acos", (value)) + return FunctionExpression("acos", (value,)) @classmethod def asin(cls, value: IntoExpression, /) -> Expression: @@ -288,7 +288,7 @@ def asin(cls, value: IntoExpression, /) -> Expression: .. _Math.asin: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin """ - return FunctionExpression("asin", (value)) + return FunctionExpression("asin", (value,)) @classmethod def atan(cls, value: IntoExpression, /) -> Expression: @@ -300,7 +300,7 @@ def atan(cls, value: IntoExpression, /) -> Expression: .. _Math.atan: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan """ - return FunctionExpression("atan", (value)) + return FunctionExpression("atan", (value,)) @classmethod def atan2(cls, dy: IntoExpression, dx: IntoExpression, /) -> Expression: @@ -324,7 +324,7 @@ def ceil(cls, value: IntoExpression, /) -> Expression: .. _Math.ceil: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil """ - return FunctionExpression("ceil", (value)) + return FunctionExpression("ceil", (value,)) @classmethod def clamp( @@ -343,7 +343,7 @@ def cos(cls, value: IntoExpression, /) -> Expression: .. _Math.cos: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos """ - return FunctionExpression("cos", (value)) + return FunctionExpression("cos", (value,)) @classmethod def exp(cls, exponent: IntoExpression, /) -> Expression: @@ -355,7 +355,7 @@ def exp(cls, exponent: IntoExpression, /) -> Expression: .. _Math.exp: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp """ - return FunctionExpression("exp", (exponent)) + return FunctionExpression("exp", (exponent,)) @classmethod def floor(cls, value: IntoExpression, /) -> Expression: @@ -367,7 +367,7 @@ def floor(cls, value: IntoExpression, /) -> Expression: .. _Math.floor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor """ - return FunctionExpression("floor", (value)) + return FunctionExpression("floor", (value,)) @classmethod def hypot(cls, value: IntoExpression, /) -> Expression: @@ -379,7 +379,7 @@ def hypot(cls, value: IntoExpression, /) -> Expression: .. _Math.hypot: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot """ - return FunctionExpression("hypot", (value)) + return FunctionExpression("hypot", (value,)) @classmethod def log(cls, value: IntoExpression, /) -> Expression: @@ -391,7 +391,7 @@ def log(cls, value: IntoExpression, /) -> Expression: .. _Math.log: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log """ - return FunctionExpression("log", (value)) + return FunctionExpression("log", (value,)) @classmethod def max( @@ -443,7 +443,7 @@ def round(cls, value: IntoExpression, /) -> Expression: .. _Math.round: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round """ - return FunctionExpression("round", (value)) + return FunctionExpression("round", (value,)) @classmethod def sin(cls, value: IntoExpression, /) -> Expression: @@ -455,7 +455,7 @@ def sin(cls, value: IntoExpression, /) -> Expression: .. _Math.sin: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin """ - return FunctionExpression("sin", (value)) + return FunctionExpression("sin", (value,)) @classmethod def sqrt(cls, value: IntoExpression, /) -> Expression: @@ -467,7 +467,7 @@ def sqrt(cls, value: IntoExpression, /) -> Expression: .. _Math.sqrt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt """ - return FunctionExpression("sqrt", (value)) + return FunctionExpression("sqrt", (value,)) @classmethod def tan(cls, value: IntoExpression, /) -> Expression: @@ -479,7 +479,7 @@ def tan(cls, value: IntoExpression, /) -> Expression: .. _Math.tan: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan """ - return FunctionExpression("tan", (value)) + return FunctionExpression("tan", (value,)) @classmethod def sampleNormal( @@ -549,32 +549,32 @@ def datetime( @classmethod def date(cls, datetime: IntoExpression, /) -> Expression: """Returns the day of the month for the given ``datetime`` value, in local time.""" - return FunctionExpression("date", (datetime)) + return FunctionExpression("date", (datetime,)) @classmethod def day(cls, datetime: IntoExpression, /) -> Expression: """Returns the day of the week for the given ``datetime`` value, in local time.""" - return FunctionExpression("day", (datetime)) + return FunctionExpression("day", (datetime,)) @classmethod def dayofyear(cls, datetime: IntoExpression, /) -> Expression: """Returns the one-based day of the year for the given ``datetime`` value, in local time.""" - return FunctionExpression("dayofyear", (datetime)) + return FunctionExpression("dayofyear", (datetime,)) @classmethod def year(cls, datetime: IntoExpression, /) -> Expression: """Returns the year for the given ``datetime`` value, in local time.""" - return FunctionExpression("year", (datetime)) + return FunctionExpression("year", (datetime,)) @classmethod def quarter(cls, datetime: IntoExpression, /) -> Expression: """Returns the quarter of the year (0-3) for the given ``datetime`` value, in local time.""" - return FunctionExpression("quarter", (datetime)) + return FunctionExpression("quarter", (datetime,)) @classmethod def month(cls, datetime: IntoExpression, /) -> Expression: """Returns the (zero-based) month for the given ``datetime`` value, in local time.""" - return FunctionExpression("month", (datetime)) + return FunctionExpression("month", (datetime,)) @classmethod def week(cls, date: IntoExpression, /) -> Expression: @@ -585,37 +585,37 @@ def week(cls, date: IntoExpression, /) -> Expression: considered to be in week 0, the first Sunday of the year is the start of week 1, the second Sunday week 2, *etc.*. """ - return FunctionExpression("week", (date)) + return FunctionExpression("week", (date,)) @classmethod def hours(cls, datetime: IntoExpression, /) -> Expression: """Returns the hours component for the given ``datetime`` value, in local time.""" - return FunctionExpression("hours", (datetime)) + return FunctionExpression("hours", (datetime,)) @classmethod def minutes(cls, datetime: IntoExpression, /) -> Expression: """Returns the minutes component for the given ``datetime`` value, in local time.""" - return FunctionExpression("minutes", (datetime)) + return FunctionExpression("minutes", (datetime,)) @classmethod def seconds(cls, datetime: IntoExpression, /) -> Expression: """Returns the seconds component for the given ``datetime`` value, in local time.""" - return FunctionExpression("seconds", (datetime)) + return FunctionExpression("seconds", (datetime,)) @classmethod def milliseconds(cls, datetime: IntoExpression, /) -> Expression: """Returns the milliseconds component for the given ``datetime`` value, in local time.""" - return FunctionExpression("milliseconds", (datetime)) + return FunctionExpression("milliseconds", (datetime,)) @classmethod def time(cls, datetime: IntoExpression, /) -> Expression: """Returns the epoch-based timestamp for the given ``datetime`` value.""" - return FunctionExpression("time", (datetime)) + return FunctionExpression("time", (datetime,)) @classmethod def timezoneoffset(cls, datetime: IntoExpression, /) -> Expression: """Returns the timezone offset from the local timezone to UTC for the given ``datetime`` value.""" - return FunctionExpression("timezoneoffset", (datetime)) + return FunctionExpression("timezoneoffset", (datetime,)) @classmethod def timeOffset( @@ -674,32 +674,32 @@ def utc( @classmethod def utcdate(cls, datetime: IntoExpression, /) -> Expression: """Returns the day of the month for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcdate", (datetime)) + return FunctionExpression("utcdate", (datetime,)) @classmethod def utcday(cls, datetime: IntoExpression, /) -> Expression: """Returns the day of the week for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcday", (datetime)) + return FunctionExpression("utcday", (datetime,)) @classmethod def utcdayofyear(cls, datetime: IntoExpression, /) -> Expression: """Returns the one-based day of the year for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcdayofyear", (datetime)) + return FunctionExpression("utcdayofyear", (datetime,)) @classmethod def utcyear(cls, datetime: IntoExpression, /) -> Expression: """Returns the year for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcyear", (datetime)) + return FunctionExpression("utcyear", (datetime,)) @classmethod def utcquarter(cls, datetime: IntoExpression, /) -> Expression: """Returns the quarter of the year (0-3) for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcquarter", (datetime)) + return FunctionExpression("utcquarter", (datetime,)) @classmethod def utcmonth(cls, datetime: IntoExpression, /) -> Expression: """Returns the (zero-based) month for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcmonth", (datetime)) + return FunctionExpression("utcmonth", (datetime,)) @classmethod def utcweek(cls, date: IntoExpression, /) -> Expression: @@ -710,27 +710,27 @@ def utcweek(cls, date: IntoExpression, /) -> Expression: considered to be in week 0, the first Sunday of the year is the start of week 1, the second Sunday week 2, *etc.*. """ - return FunctionExpression("utcweek", (date)) + return FunctionExpression("utcweek", (date,)) @classmethod def utchours(cls, datetime: IntoExpression, /) -> Expression: """Returns the hours component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utchours", (datetime)) + return FunctionExpression("utchours", (datetime,)) @classmethod def utcminutes(cls, datetime: IntoExpression, /) -> Expression: """Returns the minutes component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcminutes", (datetime)) + return FunctionExpression("utcminutes", (datetime,)) @classmethod def utcseconds(cls, datetime: IntoExpression, /) -> Expression: """Returns the seconds component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcseconds", (datetime)) + return FunctionExpression("utcseconds", (datetime,)) @classmethod def utcmilliseconds(cls, datetime: IntoExpression, /) -> Expression: """Returns the milliseconds component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcmilliseconds", (datetime)) + return FunctionExpression("utcmilliseconds", (datetime,)) @classmethod def utcOffset( @@ -770,7 +770,7 @@ def utcSequence( @classmethod def extent(cls, array: IntoExpression, /) -> Expression: """Returns a new *[min, max]* array with the minimum and maximum values of the input array, ignoring ``null``, ``undefined``, and ``NaN`` values.""" - return FunctionExpression("extent", (array)) + return FunctionExpression("extent", (array,)) @classmethod def clampRange( @@ -811,7 +811,7 @@ def lastindexof(cls, array: IntoExpression, value: IntoExpression, /) -> Express @classmethod def length(cls, array: IntoExpression, /) -> Expression: """Returns the length of the input ``array``.""" - return FunctionExpression("length", (array)) + return FunctionExpression("length", (array,)) @classmethod def lerp(cls, array: IntoExpression, fraction: IntoExpression, /) -> Expression: @@ -830,7 +830,7 @@ def peek(cls, array: IntoExpression, /) -> Expression: Similar to the built-in ``Array.pop`` method, except that it does not remove the last element. This method is a convenient shorthand for ``array[array.length - 1]``. """ - return FunctionExpression("peek", (array)) + return FunctionExpression("peek", (array,)) @classmethod def pluck(cls, array: IntoExpression, field: IntoExpression, /) -> Expression: @@ -848,7 +848,7 @@ def reverse(cls, array: IntoExpression, /) -> Expression: The first array element becomes the last, and the last array element becomes the first. """ - return FunctionExpression("reverse", (array)) + return FunctionExpression("reverse", (array,)) @classmethod def sequence(cls, *args: Any) -> Expression: @@ -879,12 +879,12 @@ def slice( @classmethod def span(cls, array: IntoExpression, /) -> Expression: """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" - return FunctionExpression("span", (array)) + return FunctionExpression("span", (array,)) @classmethod def lower(cls, string: IntoExpression, /) -> Expression: """Transforms ``string`` to lower-case letters.""" - return FunctionExpression("lower", (string)) + return FunctionExpression("lower", (string,)) @classmethod def pad( @@ -911,7 +911,7 @@ def parseFloat(cls, string: IntoExpression, /) -> Expression: Same as JavaScript's ``parseFloat``. """ - return FunctionExpression("parseFloat", (string)) + return FunctionExpression("parseFloat", (string,)) @classmethod def parseInt(cls, string: IntoExpression, /) -> Expression: @@ -920,7 +920,7 @@ def parseInt(cls, string: IntoExpression, /) -> Expression: Same as JavaScript's ``parseInt``. """ - return FunctionExpression("parseInt", (string)) + return FunctionExpression("parseInt", (string,)) @classmethod def replace( @@ -955,7 +955,7 @@ def substring( @classmethod def trim(cls, string: IntoExpression, /) -> Expression: """Returns a trimmed string with preceding and trailing whitespace removed.""" - return FunctionExpression("trim", (string)) + return FunctionExpression("trim", (string,)) @classmethod def truncate( @@ -980,7 +980,7 @@ def truncate( @classmethod def upper(cls, string: IntoExpression, /) -> Expression: """Transforms ``string`` to upper-case letters.""" - return FunctionExpression("upper", (string)) + return FunctionExpression("upper", (string,)) @classmethod def merge( @@ -1002,7 +1002,7 @@ def dayFormat(cls, day: IntoExpression, /) -> Expression: For example: ``alt.expr.dayFormat(0) -> "Sunday"``. """ - return FunctionExpression("dayFormat", (day)) + return FunctionExpression("dayFormat", (day,)) @classmethod def dayAbbrevFormat(cls, day: IntoExpression, /) -> Expression: @@ -1011,7 +1011,7 @@ def dayAbbrevFormat(cls, day: IntoExpression, /) -> Expression: For example: ``alt.expr.dayAbbrevFormat(0) -> "Sun"``. """ - return FunctionExpression("dayAbbrevFormat", (day)) + return FunctionExpression("dayAbbrevFormat", (day,)) @classmethod def format(cls, value: IntoExpression, specifier: IntoExpression, /) -> Expression: @@ -1033,7 +1033,7 @@ def monthFormat(cls, month: IntoExpression, /) -> Expression: For example: ``alt.expr.monthFormat(0) -> "January"``. """ - return FunctionExpression("monthFormat", (month)) + return FunctionExpression("monthFormat", (month,)) @classmethod def monthAbbrevFormat(cls, month: IntoExpression, /) -> Expression: @@ -1042,7 +1042,7 @@ def monthAbbrevFormat(cls, month: IntoExpression, /) -> Expression: For example: ``alt.expr.monthAbbrevFormat(0) -> "Jan"``. """ - return FunctionExpression("monthAbbrevFormat", (month)) + return FunctionExpression("monthAbbrevFormat", (month,)) @classmethod def timeUnitSpecifier( @@ -1240,7 +1240,7 @@ def group(cls, name: IntoExpression = None, /) -> Expression: If no arguments are provided, the immediate parent group is returned. If a group name is provided, the matching ancestor group item is returned. """ - return FunctionExpression("group", (name)) + return FunctionExpression("group", (name,)) @classmethod def xy(cls, item: IntoExpression = None, /) -> Expression: @@ -1251,7 +1251,7 @@ def xy(cls, item: IntoExpression = None, /) -> Expression: scenegraph ``item`` (or string group name) is provided, the coordinate space of the group item is used. """ - return FunctionExpression("xy", (item)) + return FunctionExpression("xy", (item,)) @classmethod def x(cls, item: IntoExpression = None, /) -> Expression: @@ -1262,7 +1262,7 @@ def x(cls, item: IntoExpression = None, /) -> Expression: scenegraph ``item`` (or string group name) is provided, the coordinate space of the group item is used. """ - return FunctionExpression("x", (item)) + return FunctionExpression("x", (item,)) @classmethod def y(cls, item: IntoExpression = None, /) -> Expression: @@ -1273,22 +1273,22 @@ def y(cls, item: IntoExpression = None, /) -> Expression: scenegraph ``item`` (or string group name) is provided, the coordinate space of the group item is used. """ - return FunctionExpression("y", (item)) + return FunctionExpression("y", (item,)) @classmethod def pinchDistance(cls, event: IntoExpression, /) -> Expression: """Returns the pixel distance between the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchDistance", (event)) + return FunctionExpression("pinchDistance", (event,)) @classmethod def pinchAngle(cls, event: IntoExpression, /) -> Expression: """Returns the angle of the line connecting the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchAngle", (event)) + return FunctionExpression("pinchAngle", (event,)) @classmethod def inScope(cls, item: IntoExpression, /) -> Expression: """Returns true if the given scenegraph ``item`` is a descendant of the group mark in which the event handler was defined, false otherwise.""" - return FunctionExpression("inScope", (item)) + return FunctionExpression("inScope", (item,)) @classmethod def data(cls, name: IntoExpression, /) -> Expression: @@ -1297,7 +1297,7 @@ def data(cls, name: IntoExpression, /) -> Expression: If the data set is not found, returns an empty array. """ - return FunctionExpression("data", (name)) + return FunctionExpression("data", (name,)) @classmethod def indata( diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 14ce9cfa7..8a723baed 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -922,7 +922,11 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: if node.is_overloaded(): body_params = STAR_ARGS[1:] else: - body_params = f"({', '.join(node.parameter_names())})" + body_params = ", ".join(node.parameter_names()) + if "," not in body_params: + body_params = f"({body_params}, )" + else: + body_params = f"({body_params})" body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" return EXPR_METHOD_TEMPLATE.format( decorator=DECORATOR, signature=node.signature, doc=node.doc, body=body From 9238fb635c37c3527851a3f59244ff71fe1571b2 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Tue, 24 Sep 2024 21:30:49 +0100 Subject: [PATCH 44/77] test: Adds `test_dummy_expr_funcs` https://github.com/vega/altair/pull/3600#discussion_r1773774533 --- tests/expr/test_expr.py | 51 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index 8aa97f30a..1304b1435 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -2,14 +2,23 @@ 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, Iterator, cast import pytest from jsonschema.exceptions import ValidationError from altair import datum, expr, ExprRef from altair.expr import _ConstExpressionType +from altair.expr import dummy as dummy +from altair.expr.core import GetAttrExpression + +if TYPE_CHECKING: + from inspect import Signature + from typing import Callable, Container + + from altair.expr.core import Expression + # This maps vega expression function names to the Python name VEGA_REMAP = {"if_": "if"} @@ -35,6 +44,44 @@ def _get_property_names(tp: type[Any], /) -> Iterator[str]: yield nm +def signature_n_params( + sig: Signature, /, *, exclude: Container[str] = frozenset(("cls", "self")) +) -> int: + return len([p for p in sig.parameters.values() if p.name not in exclude]) + + +def _get_classmethod_members( + tp: type[Any], / +) -> Iterator[tuple[str, Callable[..., Any]]]: + for m in classify_class_attrs(tp): + if m.kind == "class method" and m.defining_class is tp: + yield m.name, cast("classmethod[Any, Any, Any]", m.object).__func__ + + +def _get_classmethod_signatures( + tp: type[Any], / +) -> Iterator[tuple[str, Callable[..., Expression], int]]: + for name, fn in _get_classmethod_members(tp): + yield ( + VEGA_REMAP.get(name, name), + fn.__get__(tp), + signature_n_params(signature(fn)), + ) + + +@pytest.mark.parametrize( + ("veganame", "fn", "n_params"), _get_classmethod_signatures(dummy.expr) +) +def test_dummy_expr_funcs( + 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})" + + def test_unary_operations(): OP_MAP = {"-": operator.neg, "+": operator.pos} for op, func in OP_MAP.items(): From 7311fdf5bb7f5991fdaefe04aa1435919fbc37b8 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:58:26 +0100 Subject: [PATCH 45/77] test: Refactor `test_expr` - Combined some single use functions - `signature_n_params` - Collect the signature here, since the function is prefixed with `signature_` - Use a cheaper method of calculating number of target params - Reordered, renamed, prepare `test_expr_methods` for the new `expr` type https://github.com/vega/altair/pull/3600#discussion_r1773774533 --- tests/expr/test_expr.py | 63 ++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index 1304b1435..65935f739 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -3,7 +3,7 @@ import operator import sys from inspect import classify_class_attrs, getmembers, signature -from typing import TYPE_CHECKING, Any, Iterator, cast +from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, TypeVar, cast import pytest from jsonschema.exceptions import ValidationError @@ -11,14 +11,12 @@ from altair import datum, expr, ExprRef from altair.expr import _ConstExpressionType from altair.expr import dummy as dummy -from altair.expr.core import GetAttrExpression +from altair.expr.core import Expression, GetAttrExpression if TYPE_CHECKING: - from inspect import Signature - from typing import Callable, Container - - from altair.expr.core import Expression + from inspect import _IntrospectableCallable +T = TypeVar("T") # This maps vega expression function names to the Python name VEGA_REMAP = {"if_": "if"} @@ -45,41 +43,23 @@ def _get_property_names(tp: type[Any], /) -> Iterator[str]: def signature_n_params( - sig: Signature, /, *, exclude: Container[str] = frozenset(("cls", "self")) + obj: _IntrospectableCallable, + /, + *, + exclude: Iterable[str] = frozenset(("cls", "self")), ) -> int: - return len([p for p in sig.parameters.values() if p.name not in exclude]) + sig = signature(obj) + return len(set(sig.parameters).difference(exclude)) -def _get_classmethod_members( - tp: type[Any], / -) -> Iterator[tuple[str, Callable[..., Any]]]: +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: - yield m.name, cast("classmethod[Any, Any, Any]", m.object).__func__ - - -def _get_classmethod_signatures( - tp: type[Any], / -) -> Iterator[tuple[str, Callable[..., Expression], int]]: - for name, fn in _get_classmethod_members(tp): - yield ( - VEGA_REMAP.get(name, name), - fn.__get__(tp), - signature_n_params(signature(fn)), - ) - - -@pytest.mark.parametrize( - ("veganame", "fn", "n_params"), _get_classmethod_signatures(dummy.expr) -) -def test_dummy_expr_funcs( - 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})" + 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(): @@ -133,6 +113,17 @@ def test_abs(): assert repr(z) == "abs(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(("veganame", "methodname"), _remap_classmethod_names(expr)) def test_expr_funcs(veganame: str, methodname: str): """ From 6077075b45c223b9d4b44072b5870b97859892af Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:13:20 +0100 Subject: [PATCH 46/77] fix: Move some imports to `TYPE_CHECKING` block `ruff` was warning that `TYPE_CHECKING` was unused --- tools/schemapi/vega_expr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 8a723baed..d77ba2433 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -780,7 +780,7 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: import sys from typing import Any, TYPE_CHECKING -from altair.expr.core import {const}, {func}, {return_ann}, {input_ann} +from altair.expr.core import {const}, {func} from altair.vegalite.v5.schema.core import ExprRef as _ExprRef if sys.version_info >= (3, 12): @@ -788,6 +788,9 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: else: from typing_extensions import override +if TYPE_CHECKING: + from altair.expr.core import {return_ann}, {input_ann} + class {metaclass}(type): """Metaclass providing read-only class properties for :class:`expr`.""" From 0094596217fd07a18e310db706c5cb0b28f1ad59 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:15:38 +0100 Subject: [PATCH 47/77] chore: Delete `expr.dummy.py` https://github.com/vega/altair/pull/3600#discussion_r1774050096 --- altair/expr/dummy.py | 1678 --------------------------------------- tests/expr/test_expr.py | 1 - 2 files changed, 1679 deletions(-) delete mode 100644 altair/expr/dummy.py diff --git a/altair/expr/dummy.py b/altair/expr/dummy.py deleted file mode 100644 index bc7e743a6..000000000 --- a/altair/expr/dummy.py +++ /dev/null @@ -1,1678 +0,0 @@ -"""Tools for creating transform & filter expressions with a python syntax.""" - -from __future__ import annotations - -import sys -from typing import Any - -from altair.expr.core import ( - ConstExpression, - Expression, - FunctionExpression, - IntoExpression, -) -from altair.vegalite.v5.schema.core import ExprRef as _ExprRef - -if sys.version_info >= (3, 12): - from typing import override -else: - from typing_extensions import override - - -class _ConstExpressionType(type): - """Metaclass providing read-only class properties for :class:`expr`.""" - - @property - def NaN(cls) -> Expression: - """Not a number (same as JavaScript literal NaN).""" - return ConstExpression("NaN") - - @property - def LN10(cls) -> Expression: - """The natural log of 10 (alias to Math.LN10).""" - return ConstExpression("LN10") - - @property - def E(cls) -> Expression: - """The transcendental number e (alias to Math.E).""" - return ConstExpression("E") - - @property - def LOG10E(cls) -> Expression: - """The base 10 logarithm e (alias to Math.LOG10E).""" - return ConstExpression("LOG10E") - - @property - def LOG2E(cls) -> Expression: - """The base 2 logarithm of e (alias to Math.LOG2E).""" - return ConstExpression("LOG2E") - - @property - def SQRT1_2(cls) -> Expression: - """The square root of 0.5 (alias to Math.SQRT1_2).""" - return ConstExpression("SQRT1_2") - - @property - def LN2(cls) -> Expression: - """The natural log of 2 (alias to Math.LN2).""" - return ConstExpression("LN2") - - @property - def SQRT2(cls) -> Expression: - """The square root of 2 (alias to Math.SQRT1_2).""" - return ConstExpression("SQRT2") - - @property - def PI(cls) -> Expression: - """The transcendental number pi (alias to Math.PI).""" - return ConstExpression("PI") - - -class expr(_ExprRef, metaclass=_ConstExpressionType): - """ - Utility providing *constants* and *classmethods* to construct expressions. - - `Expressions`_ can be used to write basic formulas that enable custom interactions. - - Alternatively, an `inline expression`_ may be defined via :class:`expr()`. - - Parameters - ---------- - expr: str - A `vega expression`_ string. - - Returns - ------- - ``ExprRef`` - - .. _Expressions: - https://altair-viz.github.io/user_guide/interactions.html#expressions - .. _inline expression: - https://altair-viz.github.io/user_guide/interactions.html#inline-expressions - .. _vega expression: - https://vega.github.io/vega/docs/expressions/ - - Examples - -------- - >>> import altair as alt - - >>> bind_range = alt.binding_range(min=100, max=300, name="Slider value: ") - >>> param_width = alt.param(bind=bind_range, name="param_width") - >>> param_color = alt.param( - ... expr=alt.expr.if_(param_width < 200, "red", "black"), - ... name="param_color", - ... ) - >>> y = alt.Y("yval").axis(titleColor=param_color) - - >>> y - Y({ - axis: {'titleColor': Parameter('param_color', VariableParameter({ - expr: if((param_width < 200),'red','black'), - name: 'param_color' - }))}, - shorthand: 'yval' - }) - """ - - @override - def __new__(cls: type[_ExprRef], expr: str) -> _ExprRef: # type: ignore[misc] - return _ExprRef(expr=expr) - - @classmethod - def isArray(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is an array, false otherwise.""" - return FunctionExpression("isArray", (value,)) - - @classmethod - def isBoolean(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is a boolean (``true`` or ``false``), false otherwise.""" - return FunctionExpression("isBoolean", (value,)) - - @classmethod - def isDate(cls, value: IntoExpression, /) -> Expression: - """ - Returns true if ``value`` is a Date object, false otherwise. - - This method will return false for timestamp numbers or date-formatted strings; it recognizes - Date objects only. - """ - return FunctionExpression("isDate", (value,)) - - @classmethod - def isDefined(cls, value: IntoExpression, /) -> Expression: - """ - Returns true if ``value`` is a defined value, false if ``value`` equals ``undefined``. - - This method will return true for ``null`` and ``NaN`` values. - """ - return FunctionExpression("isDefined", (value,)) - - @classmethod - def isNumber(cls, value: IntoExpression, /) -> Expression: - """ - Returns true if ``value`` is a number, false otherwise. - - ``NaN`` and ``Infinity`` are considered numbers. - """ - return FunctionExpression("isNumber", (value,)) - - @classmethod - def isObject(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is an object (including arrays and Dates), false otherwise.""" - return FunctionExpression("isObject", (value,)) - - @classmethod - def isRegExp(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is a RegExp (regular expression) object, false otherwise.""" - return FunctionExpression("isRegExp", (value,)) - - @classmethod - def isString(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is a string, false otherwise.""" - return FunctionExpression("isString", (value,)) - - @classmethod - def isValid(cls, value: IntoExpression, /) -> Expression: - """Returns true if ``value`` is not ``null``, ``undefined``, or ``NaN``, false otherwise.""" - return FunctionExpression("isValid", (value,)) - - @classmethod - def toBoolean(cls, value: IntoExpression, /) -> Expression: - """ - Coerces the input ``value`` to a string. - - Null values and empty strings are mapped to ``null``. - """ - return FunctionExpression("toBoolean", (value,)) - - @classmethod - def toDate(cls, value: IntoExpression, /) -> Expression: - """ - Coerces the input ``value`` to a Date instance. - - Null values and empty strings are mapped to ``null``. If an optional *parser* function is - provided, it is used to perform date parsing, otherwise ``Date.parse`` is used. Be aware - that ``Date.parse`` has different implementations across browsers! - """ - return FunctionExpression("toDate", (value,)) - - @classmethod - def toNumber(cls, value: IntoExpression, /) -> Expression: - """ - Coerces the input ``value`` to a number. - - Null values and empty strings are mapped to ``null``. - """ - return FunctionExpression("toNumber", (value,)) - - @classmethod - def toString(cls, value: IntoExpression, /) -> Expression: - """ - Coerces the input ``value`` to a string. - - Null values and empty strings are mapped to ``null``. - """ - return FunctionExpression("toString", (value,)) - - @classmethod - def if_( - cls, - test: IntoExpression, - thenValue: IntoExpression, - elseValue: IntoExpression, - /, - ) -> Expression: - """ - If ``test`` is truthy, returns ``thenValue``. - - Otherwise, returns ``elseValue``. The *if* function is equivalent to the ternary operator - ``a ? b : c``. - """ - return FunctionExpression("if", (test, thenValue, elseValue)) - - @classmethod - def isNaN(cls, value: IntoExpression, /) -> Expression: - """ - Returns true if ``value`` is not a number. - - Same as JavaScript's `Number.isNaN`_. - - .. _Number.isNaN: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNan - """ - return FunctionExpression("isNaN", (value,)) - - @classmethod - def isFinite(cls, value: IntoExpression, /) -> Expression: - """ - Returns true if ``value`` is a finite number. - - Same as JavaScript's `Number.isFinite`_. - - .. _Number.isFinite: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite - """ - return FunctionExpression("isFinite", (value,)) - - @classmethod - def abs(cls, value: IntoExpression, /) -> Expression: - """ - Returns the absolute value of ``value``. - - Same as JavaScript's `Math.abs`_. - - .. _Math.abs: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs - """ - return FunctionExpression("abs", (value,)) - - @classmethod - def acos(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric arccosine. - - Same as JavaScript's `Math.acos`_. - - .. _Math.acos: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos - """ - return FunctionExpression("acos", (value,)) - - @classmethod - def asin(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric arcsine. - - Same as JavaScript's `Math.asin`_. - - .. _Math.asin: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin - """ - return FunctionExpression("asin", (value,)) - - @classmethod - def atan(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric arctangent. - - Same as JavaScript's `Math.atan`_. - - .. _Math.atan: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan - """ - return FunctionExpression("atan", (value,)) - - @classmethod - def atan2(cls, dy: IntoExpression, dx: IntoExpression, /) -> Expression: - """ - Returns the arctangent of *dy / dx*. - - Same as JavaScript's `Math.atan2`_. - - .. _Math.atan2: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 - """ - return FunctionExpression("atan2", (dy, dx)) - - @classmethod - def ceil(cls, value: IntoExpression, /) -> Expression: - """ - Rounds ``value`` to the nearest integer of equal or greater value. - - Same as JavaScript's `Math.ceil`_. - - .. _Math.ceil: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil - """ - return FunctionExpression("ceil", (value,)) - - @classmethod - def clamp( - cls, value: IntoExpression, min: IntoExpression, max: IntoExpression, / - ) -> Expression: - """Restricts ``value`` to be between the specified ``min`` and ``max``.""" - return FunctionExpression("clamp", (value, min, max)) - - @classmethod - def cos(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric cosine. - - Same as JavaScript's `Math.cos`_. - - .. _Math.cos: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos - """ - return FunctionExpression("cos", (value,)) - - @classmethod - def exp(cls, exponent: IntoExpression, /) -> Expression: - """ - Returns the value of *e* raised to the provided ``exponent``. - - Same as JavaScript's `Math.exp`_. - - .. _Math.exp: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp - """ - return FunctionExpression("exp", (exponent,)) - - @classmethod - def floor(cls, value: IntoExpression, /) -> Expression: - """ - Rounds ``value`` to the nearest integer of equal or lower value. - - Same as JavaScript's `Math.floor`_. - - .. _Math.floor: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor - """ - return FunctionExpression("floor", (value,)) - - @classmethod - def hypot(cls, value: IntoExpression, /) -> Expression: - """ - Returns the square root of the sum of squares of its arguments. - - Same as JavaScript's `Math.hypot`_. - - .. _Math.hypot: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot - """ - return FunctionExpression("hypot", (value,)) - - @classmethod - def log(cls, value: IntoExpression, /) -> Expression: - """ - Returns the natural logarithm of ``value``. - - Same as JavaScript's `Math.log`_. - - .. _Math.log: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log - """ - return FunctionExpression("log", (value,)) - - @classmethod - def max( - cls, value1: IntoExpression, value2: IntoExpression, *args: Any - ) -> Expression: - """ - Returns the maximum argument value. - - Same as JavaScript's `Math.max`_. - - .. _Math.max: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max - """ - return FunctionExpression("max", (value1, value2, *args)) - - @classmethod - def min( - cls, value1: IntoExpression, value2: IntoExpression, *args: Any - ) -> Expression: - """ - Returns the minimum argument value. - - Same as JavaScript's `Math.min`_. - - .. _Math.min: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min - """ - return FunctionExpression("min", (value1, value2, *args)) - - @classmethod - def pow(cls, value: IntoExpression, exponent: IntoExpression, /) -> Expression: - """ - Returns ``value`` raised to the given ``exponent``. - - Same as JavaScript's `Math.pow`_. - - .. _Math.pow: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow - """ - return FunctionExpression("pow", (value, exponent)) - - @classmethod - def round(cls, value: IntoExpression, /) -> Expression: - """ - Rounds ``value`` to the nearest integer. - - Same as JavaScript's `Math.round`_. - - .. _Math.round: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round - """ - return FunctionExpression("round", (value,)) - - @classmethod - def sin(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric sine. - - Same as JavaScript's `Math.sin`_. - - .. _Math.sin: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin - """ - return FunctionExpression("sin", (value,)) - - @classmethod - def sqrt(cls, value: IntoExpression, /) -> Expression: - """ - Square root function. - - Same as JavaScript's `Math.sqrt`_. - - .. _Math.sqrt: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt - """ - return FunctionExpression("sqrt", (value,)) - - @classmethod - def tan(cls, value: IntoExpression, /) -> Expression: - """ - Trigonometric tangent. - - Same as JavaScript's `Math.tan`_. - - .. _Math.tan: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan - """ - return FunctionExpression("tan", (value,)) - - @classmethod - def sampleNormal( - cls, mean: IntoExpression = None, stdev: IntoExpression = None, / - ) -> Expression: - """ - Returns a sample from a univariate `normal (Gaussian) probability distribution`_ with specified ``mean`` and standard deviation ``stdev``. - - If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. - - .. _normal (Gaussian) probability distribution: - https://en.wikipedia.org/wiki/Normal_distribution - """ - return FunctionExpression("sampleNormal", (mean, stdev)) - - @classmethod - def sampleLogNormal( - cls, mean: IntoExpression = None, stdev: IntoExpression = None, / - ) -> Expression: - """ - Returns a sample from a univariate `log-normal probability distribution`_ with specified log ``mean`` and log standard deviation ``stdev``. - - If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to - ``1``. - - .. _log-normal probability distribution: - https://en.wikipedia.org/wiki/Log-normal_distribution - """ - return FunctionExpression("sampleLogNormal", (mean, stdev)) - - @classmethod - def sampleUniform( - cls, min: IntoExpression = None, max: IntoExpression = None, / - ) -> Expression: - """ - Returns a sample from a univariate `continuous uniform probability distribution`_) over the interval [``min``, ``max``). - - If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one - argument is provided, it is interpreted as the ``max`` value. - - .. _continuous uniform probability distribution: - https://en.wikipedia.org/wiki/Uniform_distribution_(continuous - """ - return FunctionExpression("sampleUniform", (min, max)) - - @classmethod - def datetime( - cls, - year: IntoExpression, - month: IntoExpression, - day: IntoExpression = None, - hour: IntoExpression = None, - min: IntoExpression = None, - sec: IntoExpression = None, - millisec: IntoExpression = None, - /, - ) -> Expression: - """ - Returns a new ``Date`` instance. - - The ``month`` is 0-based, such that ``1`` represents February. - """ - return FunctionExpression( - "datetime", (year, month, day, hour, min, sec, millisec) - ) - - @classmethod - def date(cls, datetime: IntoExpression, /) -> Expression: - """Returns the day of the month for the given ``datetime`` value, in local time.""" - return FunctionExpression("date", (datetime,)) - - @classmethod - def day(cls, datetime: IntoExpression, /) -> Expression: - """Returns the day of the week for the given ``datetime`` value, in local time.""" - return FunctionExpression("day", (datetime,)) - - @classmethod - def dayofyear(cls, datetime: IntoExpression, /) -> Expression: - """Returns the one-based day of the year for the given ``datetime`` value, in local time.""" - return FunctionExpression("dayofyear", (datetime,)) - - @classmethod - def year(cls, datetime: IntoExpression, /) -> Expression: - """Returns the year for the given ``datetime`` value, in local time.""" - return FunctionExpression("year", (datetime,)) - - @classmethod - def quarter(cls, datetime: IntoExpression, /) -> Expression: - """Returns the quarter of the year (0-3) for the given ``datetime`` value, in local time.""" - return FunctionExpression("quarter", (datetime,)) - - @classmethod - def month(cls, datetime: IntoExpression, /) -> Expression: - """Returns the (zero-based) month for the given ``datetime`` value, in local time.""" - return FunctionExpression("month", (datetime,)) - - @classmethod - def week(cls, date: IntoExpression, /) -> Expression: - """ - Returns the week number of the year for the given *datetime*, in local time. - - This function assumes Sunday-based weeks. Days before the first Sunday of the year are - considered to be in week 0, the first Sunday of the year is the start of week 1, the second - Sunday week 2, *etc.*. - """ - return FunctionExpression("week", (date,)) - - @classmethod - def hours(cls, datetime: IntoExpression, /) -> Expression: - """Returns the hours component for the given ``datetime`` value, in local time.""" - return FunctionExpression("hours", (datetime,)) - - @classmethod - def minutes(cls, datetime: IntoExpression, /) -> Expression: - """Returns the minutes component for the given ``datetime`` value, in local time.""" - return FunctionExpression("minutes", (datetime,)) - - @classmethod - def seconds(cls, datetime: IntoExpression, /) -> Expression: - """Returns the seconds component for the given ``datetime`` value, in local time.""" - return FunctionExpression("seconds", (datetime,)) - - @classmethod - def milliseconds(cls, datetime: IntoExpression, /) -> Expression: - """Returns the milliseconds component for the given ``datetime`` value, in local time.""" - return FunctionExpression("milliseconds", (datetime,)) - - @classmethod - def time(cls, datetime: IntoExpression, /) -> Expression: - """Returns the epoch-based timestamp for the given ``datetime`` value.""" - return FunctionExpression("time", (datetime,)) - - @classmethod - def timezoneoffset(cls, datetime: IntoExpression, /) -> Expression: - """Returns the timezone offset from the local timezone to UTC for the given ``datetime`` value.""" - return FunctionExpression("timezoneoffset", (datetime,)) - - @classmethod - def timeOffset( - cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / - ) -> Expression: - """ - Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in the local timezone. - - The optional ``step`` argument indicates the number of time unit steps to offset by (default - 1). - - .. _*unit*: - https://vega.github.io/vega/docs/api/time/#time-units - """ - return FunctionExpression("timeOffset", (unit, date, step)) - - @classmethod - def timeSequence( - cls, - unit: IntoExpression, - start: IntoExpression, - stop: IntoExpression, - step: IntoExpression = None, - /, - ) -> Expression: - """ - Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in the local timezone. - - The optional ``step`` argument indicates the number of time unit steps to take between each - sequence entry (default 1). - - .. _*unit*: - https://vega.github.io/vega/docs/api/time/#time-units - """ - return FunctionExpression("timeSequence", (unit, start, stop, step)) - - @classmethod - def utc( - cls, - year: IntoExpression, - month: IntoExpression, - day: IntoExpression = None, - hour: IntoExpression = None, - min: IntoExpression = None, - sec: IntoExpression = None, - millisec: IntoExpression = None, - /, - ) -> Expression: - """ - Returns a timestamp for the given UTC date. - - The ``month`` is 0-based, such that ``1`` represents February. - """ - return FunctionExpression("utc", (year, month, day, hour, min, sec, millisec)) - - @classmethod - def utcdate(cls, datetime: IntoExpression, /) -> Expression: - """Returns the day of the month for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcdate", (datetime,)) - - @classmethod - def utcday(cls, datetime: IntoExpression, /) -> Expression: - """Returns the day of the week for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcday", (datetime,)) - - @classmethod - def utcdayofyear(cls, datetime: IntoExpression, /) -> Expression: - """Returns the one-based day of the year for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcdayofyear", (datetime,)) - - @classmethod - def utcyear(cls, datetime: IntoExpression, /) -> Expression: - """Returns the year for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcyear", (datetime,)) - - @classmethod - def utcquarter(cls, datetime: IntoExpression, /) -> Expression: - """Returns the quarter of the year (0-3) for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcquarter", (datetime,)) - - @classmethod - def utcmonth(cls, datetime: IntoExpression, /) -> Expression: - """Returns the (zero-based) month for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcmonth", (datetime,)) - - @classmethod - def utcweek(cls, date: IntoExpression, /) -> Expression: - """ - Returns the week number of the year for the given *datetime*, in UTC time. - - This function assumes Sunday-based weeks. Days before the first Sunday of the year are - considered to be in week 0, the first Sunday of the year is the start of week 1, the second - Sunday week 2, *etc.*. - """ - return FunctionExpression("utcweek", (date,)) - - @classmethod - def utchours(cls, datetime: IntoExpression, /) -> Expression: - """Returns the hours component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utchours", (datetime,)) - - @classmethod - def utcminutes(cls, datetime: IntoExpression, /) -> Expression: - """Returns the minutes component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcminutes", (datetime,)) - - @classmethod - def utcseconds(cls, datetime: IntoExpression, /) -> Expression: - """Returns the seconds component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcseconds", (datetime,)) - - @classmethod - def utcmilliseconds(cls, datetime: IntoExpression, /) -> Expression: - """Returns the milliseconds component for the given ``datetime`` value, in UTC time.""" - return FunctionExpression("utcmilliseconds", (datetime,)) - - @classmethod - def utcOffset( - cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / - ) -> Expression: - """ - Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in UTC time. - - The optional ``step`` argument indicates the number of time unit steps to offset by (default - 1). - - .. _*unit*: - https://vega.github.io/vega/docs/api/time/#time-units - """ - return FunctionExpression("utcOffset", (unit, date, step)) - - @classmethod - def utcSequence( - cls, - unit: IntoExpression, - start: IntoExpression, - stop: IntoExpression, - step: IntoExpression = None, - /, - ) -> Expression: - """ - Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in UTC time. - - The optional ``step`` argument indicates the number of time unit steps to take between each - sequence entry (default 1). - - .. _*unit*: - https://vega.github.io/vega/docs/api/time/#time-units - """ - return FunctionExpression("utcSequence", (unit, start, stop, step)) - - @classmethod - def extent(cls, array: IntoExpression, /) -> Expression: - """Returns a new *[min, max]* array with the minimum and maximum values of the input array, ignoring ``null``, ``undefined``, and ``NaN`` values.""" - return FunctionExpression("extent", (array,)) - - @classmethod - def clampRange( - cls, range: IntoExpression, min: IntoExpression, max: IntoExpression, / - ) -> Expression: - """ - Clamps a two-element ``range`` array in a span-preserving manner. - - If the span of the input ``range`` is less than *(max - min)* and an endpoint exceeds either - the ``min`` or ``max`` value, the range is translated such that the span is preserved and - one endpoint touches the boundary of the *[min, max]* range. If the span exceeds *(max - - min)*, the range *[min, max]* is returned. - """ - return FunctionExpression("clampRange", (range, min, max)) - - @classmethod - def indexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: - """Returns the first index of ``value`` in the input ``array``.""" - return FunctionExpression("indexof", (array, value)) - - @classmethod - def inrange(cls, value: IntoExpression, range: IntoExpression, /) -> Expression: - """Tests whether ``value`` lies within (or is equal to either) the first and last values of the ``range`` array.""" - return FunctionExpression("inrange", (value, range)) - - @classmethod - def join( - cls, array: IntoExpression, separator: IntoExpression = None, / - ) -> Expression: - """Returns a new string by concatenating all of the elements of the input ``array``, separated by commas or a specified ``separator`` string.""" - return FunctionExpression("join", (array, separator)) - - @classmethod - def lastindexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: - """Returns the last index of ``value`` in the input ``array``.""" - return FunctionExpression("lastindexof", (array, value)) - - @classmethod - def length(cls, array: IntoExpression, /) -> Expression: - """Returns the length of the input ``array``.""" - return FunctionExpression("length", (array,)) - - @classmethod - def lerp(cls, array: IntoExpression, fraction: IntoExpression, /) -> Expression: - """ - Returns the linearly interpolated value between the first and last entries in the ``array`` for the provided interpolation ``fraction`` (typically between 0 and 1). - - For example, ``alt.expr.lerp([0, 50], 0.5)`` returns 25. - """ - return FunctionExpression("lerp", (array, fraction)) - - @classmethod - def peek(cls, array: IntoExpression, /) -> Expression: - """ - Returns the last element in the input ``array``. - - Similar to the built-in ``Array.pop`` method, except that it does not remove the last - element. This method is a convenient shorthand for ``array[array.length - 1]``. - """ - return FunctionExpression("peek", (array,)) - - @classmethod - def pluck(cls, array: IntoExpression, field: IntoExpression, /) -> Expression: - """ - Retrieves the value for the specified ``field`` from a given ``array`` of objects. - - The input ``field`` string may include nested properties (e.g., ``foo.bar.bz``). - """ - return FunctionExpression("pluck", (array, field)) - - @classmethod - def reverse(cls, array: IntoExpression, /) -> Expression: - """ - Returns a new array with elements in a reverse order of the input ``array``. - - The first array element becomes the last, and the last array element becomes the first. - """ - return FunctionExpression("reverse", (array,)) - - @classmethod - def sequence(cls, *args: Any) -> Expression: - """ - Returns an array containing an arithmetic sequence of numbers. - - If ``step`` is omitted, it defaults to 1. If ``start`` is omitted, it defaults to 0. The - ``stop`` value is exclusive; it is not included in the result. If ``step`` is positive, the - last element is the largest *start + i * step* less than ``stop``; if ``step`` is negative, - the last element is the smallest *start + i * step* greater than ``stop``. If the returned - array would contain an infinite number of values, an empty range is returned. The arguments - are not required to be integers. - """ - return FunctionExpression("sequence", args) - - @classmethod - def slice( - cls, array: IntoExpression, start: IntoExpression, end: IntoExpression = None, / - ) -> Expression: - """ - Returns a section of ``array`` between the ``start`` and ``end`` indices. - - If the ``end`` argument is negative, it is treated as an offset from the end of the array - (*alt.expr.length(array) + end*). - """ - return FunctionExpression("slice", (array, start, end)) - - @classmethod - def span(cls, array: IntoExpression, /) -> Expression: - """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" - return FunctionExpression("span", (array,)) - - @classmethod - def lower(cls, string: IntoExpression, /) -> Expression: - """Transforms ``string`` to lower-case letters.""" - return FunctionExpression("lower", (string,)) - - @classmethod - def pad( - cls, - string: IntoExpression, - length: IntoExpression, - character: IntoExpression = None, - align: IntoExpression = None, - /, - ) -> Expression: - """ - Pads a ``string`` value with repeated instances of a ``character`` up to a specified ``length``. - - If ``character`` is not specified, a space (' ') is used. By default, padding is added to - the end of a string. An optional ``align`` parameter specifies if padding should be added to - the ``'left'`` (beginning), ``'center'``, or ``'right'`` (end) of the input string. - """ - return FunctionExpression("pad", (string, length, character, align)) - - @classmethod - def parseFloat(cls, string: IntoExpression, /) -> Expression: - """ - Parses the input ``string`` to a floating-point value. - - Same as JavaScript's ``parseFloat``. - """ - return FunctionExpression("parseFloat", (string,)) - - @classmethod - def parseInt(cls, string: IntoExpression, /) -> Expression: - """ - Parses the input ``string`` to an integer value. - - Same as JavaScript's ``parseInt``. - """ - return FunctionExpression("parseInt", (string,)) - - @classmethod - def replace( - cls, - string: IntoExpression, - pattern: IntoExpression, - replacement: IntoExpression, - /, - ) -> Expression: - """ - Returns a new string with some or all matches of ``pattern`` replaced by a ``replacement`` string. - - The ``pattern`` can be a string or a regular expression. If ``pattern`` is a string, only - the first instance will be replaced. Same as `JavaScript's String.replace`_. - - .. _JavaScript's String.replace: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace - """ - return FunctionExpression("replace", (string, pattern, replacement)) - - @classmethod - def substring( - cls, - string: IntoExpression, - start: IntoExpression, - end: IntoExpression = None, - /, - ) -> Expression: - """Returns a section of ``string`` between the ``start`` and ``end`` indices.""" - return FunctionExpression("substring", (string, start, end)) - - @classmethod - def trim(cls, string: IntoExpression, /) -> Expression: - """Returns a trimmed string with preceding and trailing whitespace removed.""" - return FunctionExpression("trim", (string,)) - - @classmethod - def truncate( - cls, - string: IntoExpression, - length: IntoExpression, - align: IntoExpression = None, - ellipsis: IntoExpression = None, - /, - ) -> Expression: - """ - Truncates an input ``string`` to a target ``length``. - - The optional ``align`` argument indicates what part of the string should be truncated: - ``'left'`` (the beginning), ``'center'``, or ``'right'`` (the end). By default, the - ``'right'`` end of the string is truncated. The optional ``ellipsis`` argument indicates the - string to use to indicate truncated content; by default the ellipsis character ``…`` - (``\u2026``) is used. - """ - return FunctionExpression("truncate", (string, length, align, ellipsis)) - - @classmethod - def upper(cls, string: IntoExpression, /) -> Expression: - """Transforms ``string`` to upper-case letters.""" - return FunctionExpression("upper", (string,)) - - @classmethod - def merge( - cls, object1: IntoExpression, object2: IntoExpression = None, *args: Any - ) -> Expression: - """ - Merges the input objects ``object1``, ``object2``, etc into a new output object. - - Inputs are visited in sequential order, such that key values from later arguments can - overwrite those from earlier arguments. Example: ``alt.expr.merge({a:1, b:2}, {a:3}) -> - {a:3, b:2}``. - """ - return FunctionExpression("merge", (object1, object2, *args)) - - @classmethod - def dayFormat(cls, day: IntoExpression, /) -> Expression: - """ - Formats a (0-6) *weekday* number as a full week day name, according to the current locale. - - For example: ``alt.expr.dayFormat(0) -> "Sunday"``. - """ - return FunctionExpression("dayFormat", (day,)) - - @classmethod - def dayAbbrevFormat(cls, day: IntoExpression, /) -> Expression: - """ - Formats a (0-6) *weekday* number as an abbreviated week day name, according to the current locale. - - For example: ``alt.expr.dayAbbrevFormat(0) -> "Sun"``. - """ - return FunctionExpression("dayAbbrevFormat", (day,)) - - @classmethod - def format(cls, value: IntoExpression, specifier: IntoExpression, /) -> Expression: - """ - Formats a numeric ``value`` as a string. - - The ``specifier`` must be a valid `d3-format specifier`_ (e.g., ``alt.expr.format(value, - ',.2f')``. Null values are formatted as ``"null"``. - - .. _d3-format specifier: - https://github.com/d3/d3-format/ - """ - return FunctionExpression("format", (value, specifier)) - - @classmethod - def monthFormat(cls, month: IntoExpression, /) -> Expression: - """ - Formats a (zero-based) ``month`` number as a full month name, according to the current locale. - - For example: ``alt.expr.monthFormat(0) -> "January"``. - """ - return FunctionExpression("monthFormat", (month,)) - - @classmethod - def monthAbbrevFormat(cls, month: IntoExpression, /) -> Expression: - """ - Formats a (zero-based) ``month`` number as an abbreviated month name, according to the current locale. - - For example: ``alt.expr.monthAbbrevFormat(0) -> "Jan"``. - """ - return FunctionExpression("monthAbbrevFormat", (month,)) - - @classmethod - def timeUnitSpecifier( - cls, units: IntoExpression, specifiers: IntoExpression = None, / - ) -> Expression: - """ - Returns a time format specifier string for the given time `*units*`_. - - The optional ``specifiers`` object provides a set of specifier sub-strings for customizing - the format; for more, see the `timeUnitSpecifier API documentation`_. The resulting - specifier string can then be used as input to the `timeFormat`_ or `utcFormat`_ functions, - or as the *format* parameter of an axis or legend. For example: ``alt.expr.timeFormat(date, - alt.expr.timeUnitSpecifier('year'))`` or ``alt.expr.timeFormat(date, - alt.expr.timeUnitSpecifier(['hours', 'minutes']))``. - - .. _*units*: - https://vega.github.io/vega/docs/api/time/#time-units - .. _timeUnitSpecifier API documentation: - https://vega.github.io/vega/docs/api/time/#timeUnitSpecifier - .. _timeFormat: - https://vega.github.io/vega/docs/expressions/#timeFormat - .. _utcFormat: - https://vega.github.io/vega/docs/expressions/#utcFormat - """ - return FunctionExpression("timeUnitSpecifier", (units, specifiers)) - - @classmethod - def timeFormat( - cls, value: IntoExpression, specifier: IntoExpression, / - ) -> Expression: - """ - Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to the local time. - - The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. - For example: ``alt.expr.timeFormat(timestamp, '%A')``. Null values are formatted as - ``"null"``. - - .. _d3-time-format specifier: - https://github.com/d3/d3-time-format/ - .. _TimeMultiFormat object: - https://vega.github.io/vega/docs/types/#TimeMultiFormat - """ - return FunctionExpression("timeFormat", (value, specifier)) - - @classmethod - def timeParse( - cls, string: IntoExpression, specifier: IntoExpression, / - ) -> Expression: - """ - Parses a ``string`` value to a Date object, according to the local time. - - The ``specifier`` must be a valid `d3-time-format specifier`_. For example: - ``alt.expr.timeParse('June 30, 2015', '%B %d, %Y')``. - - .. _d3-time-format specifier: - https://github.com/d3/d3-time-format/ - """ - return FunctionExpression("timeParse", (string, specifier)) - - @classmethod - def utcFormat( - cls, value: IntoExpression, specifier: IntoExpression, / - ) -> Expression: - """ - Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to `UTC`_ time. - - The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. - For example: ``alt.expr.utcFormat(timestamp, '%A')``. Null values are formatted as - ``"null"``. - - .. _UTC: - https://en.wikipedia.org/wiki/Coordinated_Universal_Time - .. _d3-time-format specifier: - https://github.com/d3/d3-time-format/ - .. _TimeMultiFormat object: - https://vega.github.io/vega/docs/types/#TimeMultiFormat - """ - return FunctionExpression("utcFormat", (value, specifier)) - - @classmethod - def utcParse( - cls, value: IntoExpression, specifier: IntoExpression, / - ) -> Expression: - """ - Parses a *string* value to a Date object, according to `UTC`_ time. - - The ``specifier`` must be a valid `d3-time-format specifier`_. For example: - ``alt.expr.utcParse('June 30, 2015', '%B %d, %Y')``. - - .. _UTC: - https://en.wikipedia.org/wiki/Coordinated_Universal_Time - .. _d3-time-format specifier: - https://github.com/d3/d3-time-format/ - """ - return FunctionExpression("utcParse", (value, specifier)) - - @classmethod - def regexp( - cls, pattern: IntoExpression, flags: IntoExpression = None, / - ) -> Expression: - """ - Creates a regular expression instance from an input ``pattern`` string and optional ``flags``. - - Same as `JavaScript's RegExp`_. - - .. _JavaScript's RegExp: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp - """ - return FunctionExpression("regexp", (pattern, flags)) - - @classmethod - def test( - cls, regexp: IntoExpression, string: IntoExpression = None, / - ) -> Expression: - r""" - Evaluates a regular expression ``regexp`` against the input ``string``, returning ``true`` if the string matches the pattern, ``false`` otherwise. - - For example: ``alt.expr.test(/\\d{3}/, "32-21-9483") -> true``. - """ - return FunctionExpression("test", (regexp, string)) - - @classmethod - def rgb(cls, *args: Any) -> Expression: - """ - Constructs a new `RGB`_ color. - - If ``r``, ``g`` and ``b`` are specified, these represent the channel values of the returned - color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* - string is specified, it is parsed and then converted to the RGB color space. Uses - `d3-color's rgb function`_. - - .. _RGB: - https://en.wikipedia.org/wiki/RGB_color_model - .. _d3-color's rgb function: - https://github.com/d3/d3-color#rgb - """ - return FunctionExpression("rgb", args) - - @classmethod - def hsl(cls, *args: Any) -> Expression: - """ - Constructs a new `HSL`_ color. - - If ``h``, ``s`` and ``l`` are specified, these represent the channel values of the returned - color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* - string is specified, it is parsed and then converted to the HSL color space. Uses - `d3-color's hsl function`_. - - .. _HSL: - https://en.wikipedia.org/wiki/HSL_and_HSV - .. _d3-color's hsl function: - https://github.com/d3/d3-color#hsl - """ - return FunctionExpression("hsl", args) - - @classmethod - def lab(cls, *args: Any) -> Expression: - """ - Constructs a new `CIE LAB`_ color. - - If ``l``, ``a`` and ``b`` are specified, these represent the channel values of the returned - color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* - string is specified, it is parsed and then converted to the LAB color space. Uses - `d3-color's lab function`_. - - .. _CIE LAB: - https://en.wikipedia.org/wiki/Lab_color_space#CIELAB - .. _d3-color's lab function: - https://github.com/d3/d3-color#lab - """ - return FunctionExpression("lab", args) - - @classmethod - def hcl(cls, *args: Any) -> Expression: - """ - Constructs a new `HCL`_ (hue, chroma, luminance) color. - - If ``h``, ``c`` and ``l`` are specified, these represent the channel values of the returned - color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* - string is specified, it is parsed and then converted to the HCL color space. Uses - `d3-color's hcl function`_. - - .. _HCL: - https://en.wikipedia.org/wiki/Lab_color_space#CIELAB - .. _d3-color's hcl function: - https://github.com/d3/d3-color#hcl - """ - return FunctionExpression("hcl", args) - - @classmethod - def group(cls, name: IntoExpression = None, /) -> Expression: - """ - Returns the scenegraph group mark item in which the current event has occurred. - - If no arguments are provided, the immediate parent group is returned. If a group name is - provided, the matching ancestor group item is returned. - """ - return FunctionExpression("group", (name,)) - - @classmethod - def xy(cls, item: IntoExpression = None, /) -> Expression: - """ - Returns the x- and y-coordinates for the current event as a two-element array. - - If no arguments are provided, the top-level coordinate space of the view is used. If a - scenegraph ``item`` (or string group name) is provided, the coordinate space of the group - item is used. - """ - return FunctionExpression("xy", (item,)) - - @classmethod - def x(cls, item: IntoExpression = None, /) -> Expression: - """ - Returns the x coordinate for the current event. - - If no arguments are provided, the top-level coordinate space of the view is used. If a - scenegraph ``item`` (or string group name) is provided, the coordinate space of the group - item is used. - """ - return FunctionExpression("x", (item,)) - - @classmethod - def y(cls, item: IntoExpression = None, /) -> Expression: - """ - Returns the y coordinate for the current event. - - If no arguments are provided, the top-level coordinate space of the view is used. If a - scenegraph ``item`` (or string group name) is provided, the coordinate space of the group - item is used. - """ - return FunctionExpression("y", (item,)) - - @classmethod - def pinchDistance(cls, event: IntoExpression, /) -> Expression: - """Returns the pixel distance between the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchDistance", (event,)) - - @classmethod - def pinchAngle(cls, event: IntoExpression, /) -> Expression: - """Returns the angle of the line connecting the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchAngle", (event,)) - - @classmethod - def inScope(cls, item: IntoExpression, /) -> Expression: - """Returns true if the given scenegraph ``item`` is a descendant of the group mark in which the event handler was defined, false otherwise.""" - return FunctionExpression("inScope", (item,)) - - @classmethod - def data(cls, name: IntoExpression, /) -> Expression: - """ - Returns the array of data objects for the Vega data set with the given ``name``. - - If the data set is not found, returns an empty array. - """ - return FunctionExpression("data", (name,)) - - @classmethod - def indata( - cls, name: IntoExpression, field: IntoExpression, value: IntoExpression, / - ) -> Expression: - """ - Tests if the data set with a given ``name`` contains a datum with a ``field`` value that matches the input ``value``. - - For example: ``alt.expr.indata('table', 'category', value)``. - """ - return FunctionExpression("indata", (name, field, value)) - - @classmethod - def scale( - cls, - name: IntoExpression, - value: IntoExpression, - group: IntoExpression = None, - /, - ) -> Expression: - """ - Applies the named scale transform (or projection) to the specified ``value``. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale or projection. - """ - return FunctionExpression("scale", (name, value, group)) - - @classmethod - def invert( - cls, - name: IntoExpression, - value: IntoExpression, - group: IntoExpression = None, - /, - ) -> Expression: - """ - Inverts the named scale transform (or projection) for the specified ``value``. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale or projection. - """ - return FunctionExpression("invert", (name, value, group)) - - @classmethod - def copy(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: # type: ignore[override] - """ - Returns a copy (a new cloned instance) of the named scale transform of projection, or ``undefined`` if no scale or projection is found. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale or projection. - """ - return FunctionExpression("copy", (name, group)) - - @classmethod - def domain( - cls, name: IntoExpression, group: IntoExpression = None, / - ) -> Expression: - """ - Returns the scale domain array for the named scale transform, or an empty array if the scale is not found. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale. - """ - return FunctionExpression("domain", (name, group)) - - @classmethod - def range(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: - """ - Returns the scale range array for the named scale transform, or an empty array if the scale is not found. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale. - """ - return FunctionExpression("range", (name, group)) - - @classmethod - def bandwidth( - cls, name: IntoExpression, group: IntoExpression = None, / - ) -> Expression: - """ - Returns the current band width for the named band scale transform, or zero if the scale is not found or is not a band scale. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the scale. - """ - return FunctionExpression("bandwidth", (name, group)) - - @classmethod - def bandspace( - cls, - count: IntoExpression, - paddingInner: IntoExpression = None, - paddingOuter: IntoExpression = None, - /, - ) -> Expression: - """ - Returns the number of steps needed within a band scale, based on the ``count`` of domain elements and the inner and outer padding values. - - While normally calculated within the scale itself, this function can be helpful for - determining the size of a chart's layout. - """ - return FunctionExpression("bandspace", (count, paddingInner, paddingOuter)) - - @classmethod - def gradient( - cls, - scale: IntoExpression, - p0: IntoExpression, - p1: IntoExpression, - count: IntoExpression = None, - /, - ) -> Expression: - """ - Returns a linear color gradient for the ``scale`` (whose range must be a `continuous color scheme`_) and starting and ending points ``p0`` and ``p1``, each an *[x, y]* array. - - The points ``p0`` and ``p1`` should be expressed in normalized coordinates in the domain [0, - 1], relative to the bounds of the item being colored. If unspecified, ``p0`` defaults to - ``[0, 0]`` and ``p1`` defaults to ``[1, 0]``, for a horizontal gradient that spans the full - bounds of an item. The optional ``count`` argument indicates a desired target number of - sample points to take from the color scale. - - .. _continuous color scheme: - https://vega.github.io/vega/docs/schemes - """ - return FunctionExpression("gradient", (scale, p0, p1, count)) - - @classmethod - def panLinear(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: - """ - Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - - The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` - indicates panning the scale domain to the right by half the scale range. - """ - return FunctionExpression("panLinear", (domain, delta)) - - @classmethod - def panLog(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: - """ - Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - - The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` - indicates panning the scale domain to the right by half the scale range. - """ - return FunctionExpression("panLog", (domain, delta)) - - @classmethod - def panPow( - cls, domain: IntoExpression, delta: IntoExpression, exponent: IntoExpression, / - ) -> Expression: - """ - Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - - The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` - indicates panning the scale domain to the right by half the scale range. - """ - return FunctionExpression("panPow", (domain, delta, exponent)) - - @classmethod - def panSymlog( - cls, domain: IntoExpression, delta: IntoExpression, constant: IntoExpression, / - ) -> Expression: - """ - Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - - The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` - indicates panning the scale domain to the right by half the scale range. - """ - return FunctionExpression("panSymlog", (domain, delta, constant)) - - @classmethod - def zoomLinear( - cls, - domain: IntoExpression, - anchor: IntoExpression, - scaleFactor: IntoExpression, - /, - ) -> Expression: - """ - Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - - The ``anchor`` value represents the zoom position in terms of fractional units of the scale - range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. - """ - return FunctionExpression("zoomLinear", (domain, anchor, scaleFactor)) - - @classmethod - def zoomLog( - cls, - domain: IntoExpression, - anchor: IntoExpression, - scaleFactor: IntoExpression, - /, - ) -> Expression: - """ - Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - - The ``anchor`` value represents the zoom position in terms of fractional units of the scale - range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. - """ - return FunctionExpression("zoomLog", (domain, anchor, scaleFactor)) - - @classmethod - def zoomPow( - cls, - domain: IntoExpression, - anchor: IntoExpression, - scaleFactor: IntoExpression, - exponent: IntoExpression, - /, - ) -> Expression: - """ - Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - - The ``anchor`` value represents the zoom position in terms of fractional units of the scale - range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. - """ - return FunctionExpression("zoomPow", (domain, anchor, scaleFactor, exponent)) - - @classmethod - def zoomSymlog( - cls, - domain: IntoExpression, - anchor: IntoExpression, - scaleFactor: IntoExpression, - constant: IntoExpression, - /, - ) -> Expression: - """ - Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - - The ``anchor`` value represents the zoom position in terms of fractional units of the scale - range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. - """ - return FunctionExpression("zoomSymlog", (domain, anchor, scaleFactor, constant)) - - @classmethod - def geoArea( - cls, - projection: IntoExpression, - feature: IntoExpression, - group: IntoExpression = None, - /, - ) -> Expression: - """ - Returns the projected planar area (typically in square pixels) of a GeoJSON ``feature`` according to the named ``projection``. - - If the ``projection`` argument is ``null``, computes the spherical area in steradians using - unprojected longitude, latitude coordinates. The optional ``group`` argument takes a - scenegraph group mark item to indicate the specific scope in which to look up the - projection. Uses d3-geo's `geoArea`_ and `path.area`_ methods. - - .. _geoArea: - https://github.com/d3/d3-geo#geoArea - .. _path.area: - https://github.com/d3/d3-geo#path_area - """ - return FunctionExpression("geoArea", (projection, feature, group)) - - @classmethod - def geoBounds( - cls, - projection: IntoExpression, - feature: IntoExpression, - group: IntoExpression = None, - /, - ) -> Expression: - """ - Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. - - The bounding box is represented by a two-dimensional array: [[*x₀*, *y₀*], [*x₁*, *y₁*]], - where *x₀* is the minimum x-coordinate, *y₀* is the minimum y-coordinate, *x₁* is the - maximum x-coordinate, and *y₁* is the maximum y-coordinate. If the ``projection`` argument - is ``null``, computes the spherical bounding box using unprojected longitude, latitude - coordinates. The optional ``group`` argument takes a scenegraph group mark item to indicate - the specific scope in which to look up the projection. Uses d3-geo's `geoBounds`_ and - `path.bounds`_ methods. - - .. _geoBounds: - https://github.com/d3/d3-geo#geoBounds - .. _path.bounds: - https://github.com/d3/d3-geo#path_bounds - """ - return FunctionExpression("geoBounds", (projection, feature, group)) - - @classmethod - def geoCentroid( - cls, - projection: IntoExpression, - feature: IntoExpression, - group: IntoExpression = None, - /, - ) -> Expression: - """ - Returns the projected planar centroid (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. - - If the ``projection`` argument is ``null``, computes the spherical centroid using - unprojected longitude, latitude coordinates. The optional ``group`` argument takes a - scenegraph group mark item to indicate the specific scope in which to look up the - projection. Uses d3-geo's `geoCentroid`_ and `path.centroid`_ methods. - - .. _geoCentroid: - https://github.com/d3/d3-geo#geoCentroid - .. _path.centroid: - https://github.com/d3/d3-geo#path_centroid - """ - return FunctionExpression("geoCentroid", (projection, feature, group)) - - @classmethod - def geoScale( - cls, projection: IntoExpression, group: IntoExpression = None, / - ) -> Expression: - """ - Returns the scale value for the named ``projection``. - - The optional ``group`` argument takes a scenegraph group mark item to indicate the specific - scope in which to look up the projection. - """ - return FunctionExpression("geoScale", (projection, group)) - - @classmethod - def treePath( - cls, name: IntoExpression, source: IntoExpression, target: IntoExpression, / - ) -> Expression: - """ - For the hierarchy data set with the given ``name``, returns the shortest path through from the ``source`` node id to the ``target`` node id. - - The path starts at the ``source`` node, ascends to the least common ancestor of the - ``source`` node and the ``target`` node, and then descends to the ``target`` node. - """ - return FunctionExpression("treePath", (name, source, target)) - - @classmethod - def treeAncestors(cls, name: IntoExpression, node: IntoExpression, /) -> Expression: - """For the hierarchy data set with the given ``name``, returns the array of ancestors nodes, starting with the input ``node``, then followed by each parent up to the root.""" - return FunctionExpression("treeAncestors", (name, node)) - - @classmethod - def warn( - cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any - ) -> Expression: - """ - Logs a warning message and returns the last argument. - - For the message to appear in the console, the visualization view must have the appropriate - logging level set. - """ - return FunctionExpression("warn", (value1, value2, *args)) - - @classmethod - def info( - cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any - ) -> Expression: - """ - Logs an informative message and returns the last argument. - - For the message to appear in the console, the visualization view must have the appropriate - logging level set. - """ - return FunctionExpression("info", (value1, value2, *args)) - - @classmethod - def debug( - cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any - ) -> Expression: - """ - Logs a debugging message and returns the last argument. - - For the message to appear in the console, the visualization view must have the appropriate - logging level set. - """ - return FunctionExpression("debug", (value1, value2, *args)) - - -_ExprType = expr -# NOTE: Compatibility alias for previous type of `alt.expr`. -# `_ExprType` was not referenced in any internal imports/tests. diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index 65935f739..68462523b 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -10,7 +10,6 @@ from altair import datum, expr, ExprRef from altair.expr import _ConstExpressionType -from altair.expr import dummy as dummy from altair.expr.core import Expression, GetAttrExpression if TYPE_CHECKING: From 16a92a45721c33c83240954327ea15914bc15cb6 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:16:52 +0100 Subject: [PATCH 48/77] test: Remove old `test_expr` functions --- tests/expr/test_expr.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index 68462523b..d0523cc33 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -25,17 +25,6 @@ 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 _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 _get_property_names(tp: type[Any], /) -> Iterator[str]: for nm, _ in getmembers(tp, _is_property): yield nm @@ -123,21 +112,6 @@ def test_expr_methods( assert repr(fn_call) == f"{veganame}({datum_args})" -@pytest.mark.parametrize(("veganame", "methodname"), _remap_classmethod_names(expr)) -def test_expr_funcs(veganame: str, methodname: str): - """ - Test all functions defined in expr.funcs. - - # FIXME: These tests are no longer suitable - They only work for functions with a **single** argument: - - TypeError: expr.if_() missing 2 required positional arguments: 'thenValue' and 'elseValue'. - """ - func = getattr(expr, methodname) - z = func(datum.xxx) - assert repr(z) == f"{veganame}(datum.xxx)" - - @pytest.mark.parametrize("constname", _get_property_names(_ConstExpressionType)) def test_expr_consts(constname: str): """Test all constants defined in expr.consts.""" From 17da9d0a6dce122803ac1cf7fd915200dfcf9877 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:17:51 +0100 Subject: [PATCH 49/77] build: Generate new `alt.expr.__init__.py` https://github.com/vega/altair/pull/3600#discussion_r1774050096 --- altair/expr/__init__.py | 1664 +++++++++++++++++------------- tools/generate_schema_wrapper.py | 2 +- 2 files changed, 955 insertions(+), 711 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index 1f93ac2b7..c61dddbfc 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import sys +from typing import TYPE_CHECKING, Any from altair.expr.core import ConstExpression, FunctionExpression from altair.vegalite.v5.schema.core import ExprRef as _ExprRef @@ -12,58 +13,61 @@ else: from typing_extensions import override +if TYPE_CHECKING: + from altair.expr.core import Expression, IntoExpression + class _ConstExpressionType(type): """Metaclass providing read-only class properties for :class:`expr`.""" @property - def NaN(cls) -> ConstExpression: + def NaN(cls) -> Expression: """Not a number (same as JavaScript literal NaN).""" return ConstExpression("NaN") @property - def LN10(cls) -> ConstExpression: + def LN10(cls) -> Expression: """The natural log of 10 (alias to Math.LN10).""" return ConstExpression("LN10") @property - def E(cls) -> ConstExpression: + def E(cls) -> Expression: """The transcendental number e (alias to Math.E).""" return ConstExpression("E") @property - def LOG10E(cls) -> ConstExpression: + def LOG10E(cls) -> Expression: """The base 10 logarithm e (alias to Math.LOG10E).""" return ConstExpression("LOG10E") @property - def LOG2E(cls) -> ConstExpression: + def LOG2E(cls) -> Expression: """The base 2 logarithm of e (alias to Math.LOG2E).""" return ConstExpression("LOG2E") @property - def SQRT1_2(cls) -> ConstExpression: + def SQRT1_2(cls) -> Expression: """The square root of 0.5 (alias to Math.SQRT1_2).""" return ConstExpression("SQRT1_2") @property - def LN2(cls) -> ConstExpression: + def LN2(cls) -> Expression: """The natural log of 2 (alias to Math.LN2).""" return ConstExpression("LN2") @property - def SQRT2(cls) -> ConstExpression: + def SQRT2(cls) -> Expression: """The square root of 2 (alias to Math.SQRT1_2).""" return ConstExpression("SQRT2") @property - def PI(cls) -> ConstExpression: + def PI(cls) -> Expression: """The transcendental number pi (alias to Math.PI).""" return ConstExpression("PI") class expr(_ExprRef, metaclass=_ConstExpressionType): - r""" + """ Utility providing *constants* and *classmethods* to construct expressions. `Expressions`_ can be used to write basic formulas that enable custom interactions. @@ -110,1321 +114,1561 @@ class expr(_ExprRef, metaclass=_ConstExpressionType): @override def __new__(cls: type[_ExprRef], expr: str) -> _ExprRef: # type: ignore[misc] - # NOTE: `mypy<=1.10.1` is not consistent with typing spec - # https://github.com/python/mypy/issues/1020 - # https://docs.python.org/3/reference/datamodel.html#object.__new__ - # https://typing.readthedocs.io/en/latest/spec/constructors.html#new-method return _ExprRef(expr=expr) @classmethod - def if_(cls, *args) -> FunctionExpression: - """ - If *test* is truthy, returns *thenValue*. Otherwise, returns *elseValue*. - - The *if* function is equivalent to the ternary operator `a ? b : c`. - """ - return FunctionExpression("if", args) - - @classmethod - def isArray(cls, *args) -> FunctionExpression: - """Returns true if *value* is an array, false otherwise.""" - return FunctionExpression("isArray", args) + def isArray(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is an array, false otherwise.""" + return FunctionExpression("isArray", (value,)) @classmethod - def isBoolean(cls, *args) -> FunctionExpression: - """Returns true if *value* is a boolean (`true` or `false`), false otherwise.""" - return FunctionExpression("isBoolean", args) + def isBoolean(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a boolean (``true`` or ``false``), false otherwise.""" + return FunctionExpression("isBoolean", (value,)) @classmethod - def isDate(cls, *args) -> FunctionExpression: + def isDate(cls, value: IntoExpression, /) -> Expression: """ - Returns true if *value* is a Date object, false otherwise. + Returns true if ``value`` is a Date object, false otherwise. - This method will return false for timestamp numbers or date-formatted strings; it recognizes Date objects only. + This method will return false for timestamp numbers or date-formatted strings; it recognizes + Date objects only. """ - return FunctionExpression("isDate", args) + return FunctionExpression("isDate", (value,)) @classmethod - def isDefined(cls, *args) -> FunctionExpression: + def isDefined(cls, value: IntoExpression, /) -> Expression: """ - Returns true if *value* is a defined value, false if *value* equals `undefined`. + Returns true if ``value`` is a defined value, false if ``value`` equals ``undefined``. - This method will return true for `null` and `NaN` values. + This method will return true for ``null`` and ``NaN`` values. """ - return FunctionExpression("isDefined", args) + return FunctionExpression("isDefined", (value,)) @classmethod - def isNumber(cls, *args) -> FunctionExpression: + def isNumber(cls, value: IntoExpression, /) -> Expression: """ - Returns true if *value* is a number, false otherwise. + Returns true if ``value`` is a number, false otherwise. - `NaN` and `Infinity` are considered numbers. + ``NaN`` and ``Infinity`` are considered numbers. """ - return FunctionExpression("isNumber", args) + return FunctionExpression("isNumber", (value,)) + + @classmethod + def isObject(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is an object (including arrays and Dates), false otherwise.""" + return FunctionExpression("isObject", (value,)) @classmethod - def isObject(cls, *args) -> FunctionExpression: - """Returns true if *value* is an object (including arrays and Dates), false otherwise.""" - return FunctionExpression("isObject", args) + def isRegExp(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a RegExp (regular expression) object, false otherwise.""" + return FunctionExpression("isRegExp", (value,)) @classmethod - def isRegExp(cls, *args) -> FunctionExpression: - """Returns true if *value* is a RegExp (regular expression) object, false otherwise.""" - return FunctionExpression("isRegExp", args) + def isString(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is a string, false otherwise.""" + return FunctionExpression("isString", (value,)) @classmethod - def isString(cls, *args) -> FunctionExpression: - """Returns true if *value* is a string, false otherwise.""" - return FunctionExpression("isString", args) + def isValid(cls, value: IntoExpression, /) -> Expression: + """Returns true if ``value`` is not ``null``, ``undefined``, or ``NaN``, false otherwise.""" + return FunctionExpression("isValid", (value,)) @classmethod - def isValid(cls, *args) -> FunctionExpression: - """Returns true if *value* is not `null`, `undefined`, or `NaN`, false otherwise.""" - return FunctionExpression("isValid", args) + def toBoolean(cls, value: IntoExpression, /) -> Expression: + """ + Coerces the input ``value`` to a string. + + Null values and empty strings are mapped to ``null``. + """ + return FunctionExpression("toBoolean", (value,)) @classmethod - def toBoolean(cls, *args) -> FunctionExpression: + def toDate(cls, value: IntoExpression, /) -> Expression: """ - Coerces the input *value* to a string. + Coerces the input ``value`` to a Date instance. - Null values and empty strings are mapped to `null`. + Null values and empty strings are mapped to ``null``. If an optional *parser* function is + provided, it is used to perform date parsing, otherwise ``Date.parse`` is used. Be aware + that ``Date.parse`` has different implementations across browsers! """ - return FunctionExpression("toBoolean", args) + return FunctionExpression("toDate", (value,)) @classmethod - def toDate(cls, *args) -> FunctionExpression: + def toNumber(cls, value: IntoExpression, /) -> Expression: """ - Coerces the input *value* to a Date instance. + Coerces the input ``value`` to a number. - Null values and empty strings are mapped to `null`. - If an optional *parser* function is provided, it is used to perform date parsing, otherwise `Date.parse` is used. - Be aware that `Date.parse` has different implementations across browsers! + Null values and empty strings are mapped to ``null``. """ - return FunctionExpression("toDate", args) + return FunctionExpression("toNumber", (value,)) @classmethod - def toNumber(cls, *args) -> FunctionExpression: + def toString(cls, value: IntoExpression, /) -> Expression: """ - Coerces the input *value* to a number. + Coerces the input ``value`` to a string. - Null values and empty strings are mapped to `null`. + Null values and empty strings are mapped to ``null``. """ - return FunctionExpression("toNumber", args) + return FunctionExpression("toString", (value,)) @classmethod - def toString(cls, *args) -> FunctionExpression: + def if_( + cls, + test: IntoExpression, + thenValue: IntoExpression, + elseValue: IntoExpression, + /, + ) -> Expression: """ - Coerces the input *value* to a string. + If ``test`` is truthy, returns ``thenValue``. - Null values and empty strings are mapped to `null`. + Otherwise, returns ``elseValue``. The *if* function is equivalent to the ternary operator + ``a ? b : c``. """ - return FunctionExpression("toString", args) + return FunctionExpression("if", (test, thenValue, elseValue)) @classmethod - def isNaN(cls, *args) -> FunctionExpression: + def isNaN(cls, value: IntoExpression, /) -> Expression: """ - Returns true if *value* is not a number. + Returns true if ``value`` is not a number. - Same as JavaScript's `isNaN`. + Same as JavaScript's `Number.isNaN`_. + + .. _Number.isNaN: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNan """ - return FunctionExpression("isNaN", args) + return FunctionExpression("isNaN", (value,)) @classmethod - def isFinite(cls, *args) -> FunctionExpression: + def isFinite(cls, value: IntoExpression, /) -> Expression: """ - Returns true if *value* is a finite number. + Returns true if ``value`` is a finite number. + + Same as JavaScript's `Number.isFinite`_. - Same as JavaScript's `isFinite`. + .. _Number.isFinite: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite """ - return FunctionExpression("isFinite", args) + return FunctionExpression("isFinite", (value,)) @classmethod - def abs(cls, *args) -> FunctionExpression: + def abs(cls, value: IntoExpression, /) -> Expression: """ - Returns the absolute value of *value*. + Returns the absolute value of ``value``. - Same as JavaScript's `Math.abs`. + Same as JavaScript's `Math.abs`_. + + .. _Math.abs: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs """ - return FunctionExpression("abs", args) + return FunctionExpression("abs", (value,)) @classmethod - def acos(cls, *args) -> FunctionExpression: + def acos(cls, value: IntoExpression, /) -> Expression: """ Trigonometric arccosine. - Same as JavaScript's `Math.acos`. + Same as JavaScript's `Math.acos`_. + + .. _Math.acos: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos """ - return FunctionExpression("acos", args) + return FunctionExpression("acos", (value,)) @classmethod - def asin(cls, *args) -> FunctionExpression: + def asin(cls, value: IntoExpression, /) -> Expression: """ Trigonometric arcsine. - Same as JavaScript's `Math.asin`. + Same as JavaScript's `Math.asin`_. + + .. _Math.asin: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin """ - return FunctionExpression("asin", args) + return FunctionExpression("asin", (value,)) @classmethod - def atan(cls, *args) -> FunctionExpression: + def atan(cls, value: IntoExpression, /) -> Expression: """ Trigonometric arctangent. - Same as JavaScript's `Math.atan`. + Same as JavaScript's `Math.atan`_. + + .. _Math.atan: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan """ - return FunctionExpression("atan", args) + return FunctionExpression("atan", (value,)) @classmethod - def atan2(cls, *args) -> FunctionExpression: + def atan2(cls, dy: IntoExpression, dx: IntoExpression, /) -> Expression: """ Returns the arctangent of *dy / dx*. - Same as JavaScript's `Math.atan2`. + Same as JavaScript's `Math.atan2`_. + + .. _Math.atan2: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 """ - return FunctionExpression("atan2", args) + return FunctionExpression("atan2", (dy, dx)) @classmethod - def ceil(cls, *args) -> FunctionExpression: + def ceil(cls, value: IntoExpression, /) -> Expression: """ - Rounds *value* to the nearest integer of equal or greater value. + Rounds ``value`` to the nearest integer of equal or greater value. + + Same as JavaScript's `Math.ceil`_. - Same as JavaScript's `Math.ceil`. + .. _Math.ceil: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil """ - return FunctionExpression("ceil", args) + return FunctionExpression("ceil", (value,)) @classmethod - def clamp(cls, *args) -> FunctionExpression: - """Restricts *value* to be between the specified *min* and *max*.""" - return FunctionExpression("clamp", args) + def clamp( + cls, value: IntoExpression, min: IntoExpression, max: IntoExpression, / + ) -> Expression: + """Restricts ``value`` to be between the specified ``min`` and ``max``.""" + return FunctionExpression("clamp", (value, min, max)) @classmethod - def cos(cls, *args) -> FunctionExpression: + def cos(cls, value: IntoExpression, /) -> Expression: """ Trigonometric cosine. - Same as JavaScript's `Math.cos`. + Same as JavaScript's `Math.cos`_. + + .. _Math.cos: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos """ - return FunctionExpression("cos", args) + return FunctionExpression("cos", (value,)) @classmethod - def exp(cls, *args) -> FunctionExpression: + def exp(cls, exponent: IntoExpression, /) -> Expression: """ - Returns the value of *e* raised to the provided *exponent*. + Returns the value of *e* raised to the provided ``exponent``. + + Same as JavaScript's `Math.exp`_. - Same as JavaScript's `Math.exp`. + .. _Math.exp: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp """ - return FunctionExpression("exp", args) + return FunctionExpression("exp", (exponent,)) @classmethod - def floor(cls, *args) -> FunctionExpression: + def floor(cls, value: IntoExpression, /) -> Expression: """ - Rounds *value* to the nearest integer of equal or lower value. + Rounds ``value`` to the nearest integer of equal or lower value. - Same as JavaScript's `Math.floor`. + Same as JavaScript's `Math.floor`_. + + .. _Math.floor: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor """ - return FunctionExpression("floor", args) + return FunctionExpression("floor", (value,)) @classmethod - def hypot(cls, *args) -> FunctionExpression: + def hypot(cls, value: IntoExpression, /) -> Expression: """ Returns the square root of the sum of squares of its arguments. - Same as JavaScript's `Math.hypot`. + Same as JavaScript's `Math.hypot`_. + + .. _Math.hypot: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot """ - return FunctionExpression("hypot", args) + return FunctionExpression("hypot", (value,)) @classmethod - def log(cls, *args) -> FunctionExpression: + def log(cls, value: IntoExpression, /) -> Expression: """ - Returns the natural logarithm of *value*. + Returns the natural logarithm of ``value``. - Same as JavaScript's `Math.log`. + Same as JavaScript's `Math.log`_. + + .. _Math.log: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log """ - return FunctionExpression("log", args) + return FunctionExpression("log", (value,)) @classmethod - def max(cls, *args) -> FunctionExpression: + def max( + cls, value1: IntoExpression, value2: IntoExpression, *args: Any + ) -> Expression: """ Returns the maximum argument value. - Same as JavaScript's `Math.max`. + Same as JavaScript's `Math.max`_. + + .. _Math.max: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max """ - return FunctionExpression("max", args) + return FunctionExpression("max", (value1, value2, *args)) @classmethod - def min(cls, *args) -> FunctionExpression: + def min( + cls, value1: IntoExpression, value2: IntoExpression, *args: Any + ) -> Expression: """ Returns the minimum argument value. - Same as JavaScript's `Math.min`. - """ - return FunctionExpression("min", args) - - @classmethod - def pow(cls, *args) -> FunctionExpression: - """ - Returns *value* raised to the given *exponent*. + Same as JavaScript's `Math.min`_. - Same as JavaScript's `Math.pow`. + .. _Math.min: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min """ - return FunctionExpression("pow", args) + return FunctionExpression("min", (value1, value2, *args)) @classmethod - def random(cls, *args) -> FunctionExpression: + def pow(cls, value: IntoExpression, exponent: IntoExpression, /) -> Expression: """ - Returns a pseudo-random number in the range `[0, 1]`. + Returns ``value`` raised to the given ``exponent``. - Same as JavaScript's `Math.random`. - """ - return FunctionExpression("random", args) + Same as JavaScript's `Math.pow`_. - @classmethod - def round(cls, *args) -> FunctionExpression: + .. _Math.pow: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow """ - Rounds *value* to the nearest integer. - - Same as JavaScript's `Math.round`. - """ - return FunctionExpression("round", args) + return FunctionExpression("pow", (value, exponent)) @classmethod - def sin(cls, *args) -> FunctionExpression: - """ - Trigonometric sine. - - Same as JavaScript's `Math.sin`. + def round(cls, value: IntoExpression, /) -> Expression: """ - return FunctionExpression("sin", args) + Rounds ``value`` to the nearest integer. - @classmethod - def sqrt(cls, *args) -> FunctionExpression: - """ - Square root function. + Same as JavaScript's `Math.round`_. - Same as JavaScript's `Math.sqrt`. + .. _Math.round: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round """ - return FunctionExpression("sqrt", args) + return FunctionExpression("round", (value,)) @classmethod - def tan(cls, *args) -> FunctionExpression: + def sin(cls, value: IntoExpression, /) -> Expression: """ - Trigonometric tangent. - - Same as JavaScript's `Math.tan`. - """ - return FunctionExpression("tan", args) + Trigonometric sine. - @classmethod - def sampleNormal(cls, *args) -> FunctionExpression: - """ - Returns a sample from a univariate `normal (Gaussian) probability distribution `__ with specified *mean* and standard deviation *stdev*. + Same as JavaScript's `Math.sin`_. - If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`. + .. _Math.sin: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin """ - return FunctionExpression("sampleNormal", args) + return FunctionExpression("sin", (value,)) @classmethod - def cumulativeNormal(cls, *args) -> FunctionExpression: - """ - Returns the value of the `cumulative distribution function `__ at the given input domain *value* for a normal distribution with specified *mean* and standard deviation *stdev*. - - If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`. + def sqrt(cls, value: IntoExpression, /) -> Expression: """ - return FunctionExpression("cumulativeNormal", args) + Square root function. - @classmethod - def densityNormal(cls, *args) -> FunctionExpression: - """ - Returns the value of the `probability density function `__ at the given input domain *value*, for a normal distribution with specified *mean* and standard deviation *stdev*. + Same as JavaScript's `Math.sqrt`_. - If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`. + .. _Math.sqrt: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt """ - return FunctionExpression("densityNormal", args) + return FunctionExpression("sqrt", (value,)) @classmethod - def quantileNormal(cls, *args) -> FunctionExpression: - """ - Returns the quantile value (the inverse of the `cumulative distribution function `__ for the given input *probability*, for a normal distribution with specified *mean* and standard deviation *stdev*. - - If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`. + def tan(cls, value: IntoExpression, /) -> Expression: """ - return FunctionExpression("quantileNormal", args) + Trigonometric tangent. - @classmethod - def sampleLogNormal(cls, *args) -> FunctionExpression: - """ - Returns a sample from a univariate `log-normal probability distribution `__ with specified log *mean* and log standard deviation *stdev*. + Same as JavaScript's `Math.tan`_. - If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`. + .. _Math.tan: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan """ - return FunctionExpression("sampleLogNormal", args) + return FunctionExpression("tan", (value,)) @classmethod - def cumulativeLogNormal(cls, *args) -> FunctionExpression: + def sampleNormal( + cls, mean: IntoExpression = None, stdev: IntoExpression = None, / + ) -> Expression: """ - Returns the value of the `cumulative distribution function `__ at the given input domain *value* for a log-normal distribution with specified log *mean* and log standard deviation *stdev*. + Returns a sample from a univariate `normal (Gaussian) probability distribution`_ with specified ``mean`` and standard deviation ``stdev``. - If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`. - """ - return FunctionExpression("cumulativeLogNormal", args) - - @classmethod - def densityLogNormal(cls, *args) -> FunctionExpression: - """ - Returns the value of the `probability density function `__ at the given input domain *value*, for a log-normal distribution with specified log *mean* and log standard deviation *stdev*. + If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. - If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`. + .. _normal (Gaussian) probability distribution: + https://en.wikipedia.org/wiki/Normal_distribution """ - return FunctionExpression("densityLogNormal", args) + return FunctionExpression("sampleNormal", (mean, stdev)) @classmethod - def quantileLogNormal(cls, *args) -> FunctionExpression: + def sampleLogNormal( + cls, mean: IntoExpression = None, stdev: IntoExpression = None, / + ) -> Expression: """ - Returns the quantile value (the inverse of the `cumulative distribution function `__ for the given input *probability*, for a log-normal distribution with specified log *mean* and log standard deviation *stdev*. + Returns a sample from a univariate `log-normal probability distribution`_ with specified log ``mean`` and log standard deviation ``stdev``. - If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`. - """ - return FunctionExpression("quantileLogNormal", args) + If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to + ``1``. - @classmethod - def sampleUniform(cls, *args) -> FunctionExpression: + .. _log-normal probability distribution: + https://en.wikipedia.org/wiki/Log-normal_distribution """ - Returns a sample from a univariate `continuous uniform probability distribution `__ over the interval `[min, max]`. - - If unspecified, *min* defaults to `0` and *max* defaults to `1`. - If only one argument is provided, it is interpreted as the *max* value. - """ - return FunctionExpression("sampleUniform", args) - - @classmethod - def cumulativeUniform(cls, *args) -> FunctionExpression: - """ - Returns the value of the `cumulative distribution function `__ at the given input domain *value* for a uniform distribution over the interval `[min, max]`. - - If unspecified, *min* defaults to `0` and *max* defaults to `1`. - If only one argument is provided, it is interpreted as the *max* value. - """ - return FunctionExpression("cumulativeUniform", args) + return FunctionExpression("sampleLogNormal", (mean, stdev)) @classmethod - def densityUniform(cls, *args) -> FunctionExpression: - """ - Returns the value of the `probability density function `__ at the given input domain *value*, for a uniform distribution over the interval `[min, max]`. - - If unspecified, *min* defaults to `0` and *max* defaults to `1`. - If only one argument is provided, it is interpreted as the *max* value. + def sampleUniform( + cls, min: IntoExpression = None, max: IntoExpression = None, / + ) -> Expression: """ - return FunctionExpression("densityUniform", args) + Returns a sample from a univariate `continuous uniform probability distribution`_) over the interval [``min``, ``max``). - @classmethod - def quantileUniform(cls, *args) -> FunctionExpression: - """ - Returns the quantile value (the inverse of the `cumulative distribution function `__ for the given input *probability*, for a uniform distribution over the interval `[min, max]`. + If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one + argument is provided, it is interpreted as the ``max`` value. - If unspecified, *min* defaults to `0` and *max* defaults to `1`. - If only one argument is provided, it is interpreted as the *max* value. + .. _continuous uniform probability distribution: + https://en.wikipedia.org/wiki/Uniform_distribution_(continuous """ - return FunctionExpression("quantileUniform", args) - - @classmethod - def now(cls, *args) -> FunctionExpression: - """Returns the timestamp for the current time.""" - return FunctionExpression("now", args) + return FunctionExpression("sampleUniform", (min, max)) @classmethod - def datetime(cls, *args) -> FunctionExpression: + def datetime( + cls, + year: IntoExpression, + month: IntoExpression, + day: IntoExpression = None, + hour: IntoExpression = None, + min: IntoExpression = None, + sec: IntoExpression = None, + millisec: IntoExpression = None, + /, + ) -> Expression: """ - Returns a new `Date` instance. + Returns a new ``Date`` instance. - The *month* is 0-based, such that `1` represents February. + The ``month`` is 0-based, such that ``1`` represents February. """ - return FunctionExpression("datetime", args) + return FunctionExpression( + "datetime", (year, month, day, hour, min, sec, millisec) + ) @classmethod - def date(cls, *args) -> FunctionExpression: - """Returns the day of the month for the given *datetime* value, in local time.""" - return FunctionExpression("date", args) + def date(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the month for the given ``datetime`` value, in local time.""" + return FunctionExpression("date", (datetime,)) @classmethod - def day(cls, *args) -> FunctionExpression: - """Returns the day of the week for the given *datetime* value, in local time.""" - return FunctionExpression("day", args) + def day(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the week for the given ``datetime`` value, in local time.""" + return FunctionExpression("day", (datetime,)) @classmethod - def dayofyear(cls, *args) -> FunctionExpression: - """Returns the one-based day of the year for the given *datetime* value, in local time.""" - return FunctionExpression("dayofyear", args) + def dayofyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the one-based day of the year for the given ``datetime`` value, in local time.""" + return FunctionExpression("dayofyear", (datetime,)) @classmethod - def year(cls, *args) -> FunctionExpression: - """Returns the year for the given *datetime* value, in local time.""" - return FunctionExpression("year", args) + def year(cls, datetime: IntoExpression, /) -> Expression: + """Returns the year for the given ``datetime`` value, in local time.""" + return FunctionExpression("year", (datetime,)) @classmethod - def quarter(cls, *args) -> FunctionExpression: - """Returns the quarter of the year (0-3) for the given *datetime* value, in local time.""" - return FunctionExpression("quarter", args) + def quarter(cls, datetime: IntoExpression, /) -> Expression: + """Returns the quarter of the year (0-3) for the given ``datetime`` value, in local time.""" + return FunctionExpression("quarter", (datetime,)) @classmethod - def month(cls, *args) -> FunctionExpression: - """Returns the (zero-based) month for the given *datetime* value, in local time.""" - return FunctionExpression("month", args) + def month(cls, datetime: IntoExpression, /) -> Expression: + """Returns the (zero-based) month for the given ``datetime`` value, in local time.""" + return FunctionExpression("month", (datetime,)) @classmethod - def week(cls, *args) -> FunctionExpression: + def week(cls, date: IntoExpression, /) -> Expression: """ Returns the week number of the year for the given *datetime*, in local time. - This function assumes Sunday-based weeks. - Days before the first Sunday of the year are considered to be in week 0, - the first Sunday of the year is the start of week 1, - the second Sunday week 2, etc. + This function assumes Sunday-based weeks. Days before the first Sunday of the year are + considered to be in week 0, the first Sunday of the year is the start of week 1, the second + Sunday week 2, *etc.*. """ - return FunctionExpression("week", args) + return FunctionExpression("week", (date,)) @classmethod - def hours(cls, *args) -> FunctionExpression: - """Returns the hours component for the given *datetime* value, in local time.""" - return FunctionExpression("hours", args) + def hours(cls, datetime: IntoExpression, /) -> Expression: + """Returns the hours component for the given ``datetime`` value, in local time.""" + return FunctionExpression("hours", (datetime,)) @classmethod - def minutes(cls, *args) -> FunctionExpression: - """Returns the minutes component for the given *datetime* value, in local time.""" - return FunctionExpression("minutes", args) + def minutes(cls, datetime: IntoExpression, /) -> Expression: + """Returns the minutes component for the given ``datetime`` value, in local time.""" + return FunctionExpression("minutes", (datetime,)) @classmethod - def seconds(cls, *args) -> FunctionExpression: - """Returns the seconds component for the given *datetime* value, in local time.""" - return FunctionExpression("seconds", args) + def seconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the seconds component for the given ``datetime`` value, in local time.""" + return FunctionExpression("seconds", (datetime,)) @classmethod - def milliseconds(cls, *args) -> FunctionExpression: - """Returns the milliseconds component for the given *datetime* value, in local time.""" - return FunctionExpression("milliseconds", args) + def milliseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the milliseconds component for the given ``datetime`` value, in local time.""" + return FunctionExpression("milliseconds", (datetime,)) @classmethod - def time(cls, *args) -> FunctionExpression: - """Returns the epoch-based timestamp for the given *datetime* value.""" - return FunctionExpression("time", args) + def time(cls, datetime: IntoExpression, /) -> Expression: + """Returns the epoch-based timestamp for the given ``datetime`` value.""" + return FunctionExpression("time", (datetime,)) @classmethod - def timezoneoffset(cls, *args) -> FunctionExpression: - """Returns the timezone offset from the local timezone to UTC for the given *datetime* value.""" - return FunctionExpression("timezoneoffset", args) + def timezoneoffset(cls, datetime: IntoExpression, /) -> Expression: + """Returns the timezone offset from the local timezone to UTC for the given ``datetime`` value.""" + return FunctionExpression("timezoneoffset", (datetime,)) @classmethod - def timeOffset(cls, *args) -> FunctionExpression: + def timeOffset( + cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / + ) -> Expression: """ - Returns a new `Date` instance that offsets the given *date* by the specified time `unit `__ in the local timezone. + Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in the local timezone. - The optional *step* argument indicates the number of time unit steps to offset by (default 1). + The optional ``step`` argument indicates the number of time unit steps to offset by (default + 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units """ - return FunctionExpression("timeOffset", args) + return FunctionExpression("timeOffset", (unit, date, step)) @classmethod - def timeSequence(cls, *args) -> FunctionExpression: + def timeSequence( + cls, + unit: IntoExpression, + start: IntoExpression, + stop: IntoExpression, + step: IntoExpression = None, + /, + ) -> Expression: """ - Returns an array of `Date` instances from *start* (inclusive) to *stop* (exclusive), with each entry separated by the given time `unit `__ in the local timezone. + Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in the local timezone. + + The optional ``step`` argument indicates the number of time unit steps to take between each + sequence entry (default 1). - The optional *step* argument indicates the number of time unit steps to take between each sequence entry (default 1). + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units """ - return FunctionExpression("timeSequence", args) + return FunctionExpression("timeSequence", (unit, start, stop, step)) @classmethod - def utc(cls, *args) -> FunctionExpression: - """Returns a timestamp for the given UTC date. The *month* is 0-based, such that `1` represents February.""" - return FunctionExpression("utc", args) + def utc( + cls, + year: IntoExpression, + month: IntoExpression, + day: IntoExpression = None, + hour: IntoExpression = None, + min: IntoExpression = None, + sec: IntoExpression = None, + millisec: IntoExpression = None, + /, + ) -> Expression: + """ + Returns a timestamp for the given UTC date. + + The ``month`` is 0-based, such that ``1`` represents February. + """ + return FunctionExpression("utc", (year, month, day, hour, min, sec, millisec)) @classmethod - def utcdate(cls, *args) -> FunctionExpression: - """Returns the day of the month for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcdate", args) + def utcdate(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the month for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcdate", (datetime,)) @classmethod - def utcday(cls, *args) -> FunctionExpression: - """Returns the day of the week for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcday", args) + def utcday(cls, datetime: IntoExpression, /) -> Expression: + """Returns the day of the week for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcday", (datetime,)) @classmethod - def utcdayofyear(cls, *args) -> FunctionExpression: - """Returns the one-based day of the year for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcdayofyear", args) + def utcdayofyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the one-based day of the year for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcdayofyear", (datetime,)) @classmethod - def utcyear(cls, *args) -> FunctionExpression: - """Returns the year for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcyear", args) + def utcyear(cls, datetime: IntoExpression, /) -> Expression: + """Returns the year for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcyear", (datetime,)) @classmethod - def utcquarter(cls, *args) -> FunctionExpression: - """Returns the quarter of the year (0-3) for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcquarter", args) + def utcquarter(cls, datetime: IntoExpression, /) -> Expression: + """Returns the quarter of the year (0-3) for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcquarter", (datetime,)) @classmethod - def utcmonth(cls, *args) -> FunctionExpression: - """Returns the (zero-based) month for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcmonth", args) + def utcmonth(cls, datetime: IntoExpression, /) -> Expression: + """Returns the (zero-based) month for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcmonth", (datetime,)) @classmethod - def utcweek(cls, *args) -> FunctionExpression: + def utcweek(cls, date: IntoExpression, /) -> Expression: """ Returns the week number of the year for the given *datetime*, in UTC time. - This function assumes Sunday-based weeks. - Days before the first Sunday of the year are considered to be in week 0, - the first Sunday of the year is the start of week 1, - the second Sunday week 2, etc. + This function assumes Sunday-based weeks. Days before the first Sunday of the year are + considered to be in week 0, the first Sunday of the year is the start of week 1, the second + Sunday week 2, *etc.*. """ - return FunctionExpression("utcweek", args) + return FunctionExpression("utcweek", (date,)) @classmethod - def utchours(cls, *args) -> FunctionExpression: - """Returns the hours component for the given *datetime* value, in UTC time.""" - return FunctionExpression("utchours", args) + def utchours(cls, datetime: IntoExpression, /) -> Expression: + """Returns the hours component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utchours", (datetime,)) @classmethod - def utcminutes(cls, *args) -> FunctionExpression: - """Returns the minutes component for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcminutes", args) + def utcminutes(cls, datetime: IntoExpression, /) -> Expression: + """Returns the minutes component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcminutes", (datetime,)) @classmethod - def utcseconds(cls, *args) -> FunctionExpression: - """Returns the seconds component for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcseconds", args) + def utcseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the seconds component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcseconds", (datetime,)) @classmethod - def utcmilliseconds(cls, *args) -> FunctionExpression: - """Returns the milliseconds component for the given *datetime* value, in UTC time.""" - return FunctionExpression("utcmilliseconds", args) + def utcmilliseconds(cls, datetime: IntoExpression, /) -> Expression: + """Returns the milliseconds component for the given ``datetime`` value, in UTC time.""" + return FunctionExpression("utcmilliseconds", (datetime,)) @classmethod - def utcOffset(cls, *args) -> FunctionExpression: + def utcOffset( + cls, unit: IntoExpression, date: IntoExpression, step: IntoExpression = None, / + ) -> Expression: """ - Returns a new `Date` instance that offsets the given *date* by the specified time `unit `__ in UTC time. + Returns a new ``Date`` instance that offsets the given ``date`` by the specified time `*unit*`_ in UTC time. + + The optional ``step`` argument indicates the number of time unit steps to offset by (default + 1). - The optional *step* argument indicates the number of time unit steps to offset by (default 1). + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units """ - return FunctionExpression("utcOffset", args) + return FunctionExpression("utcOffset", (unit, date, step)) @classmethod - def utcSequence(cls, *args) -> FunctionExpression: + def utcSequence( + cls, + unit: IntoExpression, + start: IntoExpression, + stop: IntoExpression, + step: IntoExpression = None, + /, + ) -> Expression: """ - Returns an array of `Date` instances from *start* (inclusive) to *stop* (exclusive), with each entry separated by the given time `unit `__ in UTC time. + Returns an array of ``Date`` instances from ``start`` (inclusive) to ``stop`` (exclusive), with each entry separated by the given time `*unit*`_ in UTC time. - The optional *step* argument indicates the number of time unit steps to take between each sequence entry (default 1). + The optional ``step`` argument indicates the number of time unit steps to take between each + sequence entry (default 1). + + .. _*unit*: + https://vega.github.io/vega/docs/api/time/#time-units """ - return FunctionExpression("utcSequence", args) + return FunctionExpression("utcSequence", (unit, start, stop, step)) @classmethod - def extent(cls, *args) -> FunctionExpression: - """Returns a new `[min, max]` array with the minimum and maximum values of the input array, ignoring `null`, `undefined`, and `NaN` values.""" - return FunctionExpression("extent", args) + def extent(cls, array: IntoExpression, /) -> Expression: + """Returns a new *[min, max]* array with the minimum and maximum values of the input array, ignoring ``null``, ``undefined``, and ``NaN`` values.""" + return FunctionExpression("extent", (array,)) @classmethod - def clampRange(cls, *args) -> FunctionExpression: + def clampRange( + cls, range: IntoExpression, min: IntoExpression, max: IntoExpression, / + ) -> Expression: """ - Clamps a two-element *range* array in a span-preserving manner. + Clamps a two-element ``range`` array in a span-preserving manner. - If the span of the input *range* is less than `(max - min)` and an endpoint exceeds either the *min* or *max* value, - the range is translated such that the span is preserved and one endpoint touches the boundary of the `[min, max]` range. - If the span exceeds `(max - min)`, the range `[min, max]` is returned. + If the span of the input ``range`` is less than *(max - min)* and an endpoint exceeds either + the ``min`` or ``max`` value, the range is translated such that the span is preserved and + one endpoint touches the boundary of the *[min, max]* range. If the span exceeds *(max - + min)*, the range *[min, max]* is returned. """ - return FunctionExpression("clampRange", args) + return FunctionExpression("clampRange", (range, min, max)) @classmethod - def indexof(cls, *args) -> FunctionExpression: - """Returns the first index of *value* in the input *array*, or the first index of *substring* in the input *string*.""" - return FunctionExpression("indexof", args) + def indexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: + """Returns the first index of ``value`` in the input ``array``.""" + return FunctionExpression("indexof", (array, value)) @classmethod - def inrange(cls, *args) -> FunctionExpression: - """Tests whether *value* lies within (or is equal to either) the first and last values of the *range* array.""" - return FunctionExpression("inrange", args) + def inrange(cls, value: IntoExpression, range: IntoExpression, /) -> Expression: + """Tests whether ``value`` lies within (or is equal to either) the first and last values of the ``range`` array.""" + return FunctionExpression("inrange", (value, range)) @classmethod - def join(cls, *args) -> FunctionExpression: - """Returns a new string by concatenating all of the elements of the input *array*, separated by commas or a specified *separator* string.""" - return FunctionExpression("join", args) + def join( + cls, array: IntoExpression, separator: IntoExpression = None, / + ) -> Expression: + """Returns a new string by concatenating all of the elements of the input ``array``, separated by commas or a specified ``separator`` string.""" + return FunctionExpression("join", (array, separator)) @classmethod - def lastindexof(cls, *args) -> FunctionExpression: - """Returns the last index of *value* in the input *array*, or the last index of *substring* in the input *string*.""" - return FunctionExpression("lastindexof", args) + def lastindexof(cls, array: IntoExpression, value: IntoExpression, /) -> Expression: + """Returns the last index of ``value`` in the input ``array``.""" + return FunctionExpression("lastindexof", (array, value)) @classmethod - def length(cls, *args) -> FunctionExpression: - """Returns the length of the input *array*, or the length of the input *string*.""" - return FunctionExpression("length", args) + def length(cls, array: IntoExpression, /) -> Expression: + """Returns the length of the input ``array``.""" + return FunctionExpression("length", (array,)) @classmethod - def lerp(cls, *args) -> FunctionExpression: + def lerp(cls, array: IntoExpression, fraction: IntoExpression, /) -> Expression: """ - Returns the linearly interpolated value between the first and last entries in the *array* for the provided interpolation *fraction* (typically between 0 and 1). + Returns the linearly interpolated value between the first and last entries in the ``array`` for the provided interpolation ``fraction`` (typically between 0 and 1). - For example, `lerp([0, 50], 0.5)` returns 25. + For example, ``alt.expr.lerp([0, 50], 0.5)`` returns 25. """ - return FunctionExpression("lerp", args) + return FunctionExpression("lerp", (array, fraction)) @classmethod - def peek(cls, *args) -> FunctionExpression: + def peek(cls, array: IntoExpression, /) -> Expression: """ - Returns the last element in the input *array*. + Returns the last element in the input ``array``. - Similar to the built-in `Array.pop` method, except that it does not remove the last element. - This method is a convenient shorthand for `array[array.length - 1]`. + Similar to the built-in ``Array.pop`` method, except that it does not remove the last + element. This method is a convenient shorthand for ``array[array.length - 1]``. """ - return FunctionExpression("peek", args) + return FunctionExpression("peek", (array,)) @classmethod - def pluck(cls, *args) -> FunctionExpression: + def pluck(cls, array: IntoExpression, field: IntoExpression, /) -> Expression: """ - Retrieves the value for the specified *field* from a given *array* of objects. + Retrieves the value for the specified ``field`` from a given ``array`` of objects. - The input *field* string may include nested properties (e.g., `foo.bar.bz`). + The input ``field`` string may include nested properties (e.g., ``foo.bar.bz``). """ - return FunctionExpression("pluck", args) + return FunctionExpression("pluck", (array, field)) @classmethod - def reverse(cls, *args) -> FunctionExpression: + def reverse(cls, array: IntoExpression, /) -> Expression: """ - Returns a new array with elements in a reverse order of the input *array*. + Returns a new array with elements in a reverse order of the input ``array``. The first array element becomes the last, and the last array element becomes the first. """ - return FunctionExpression("reverse", args) + return FunctionExpression("reverse", (array,)) @classmethod - def sequence(cls, *args) -> FunctionExpression: - r""" + def sequence(cls, *args: Any) -> Expression: + """ Returns an array containing an arithmetic sequence of numbers. - If *step* is omitted, it defaults to 1. - If *start* is omitted, it defaults to 0. - - The *stop* value is exclusive; it is not included in the result. - If *step* is positive, the last element is the largest `start + i * step` less than *stop*; - if *step* is negative, the last element is the smallest `start + i * step` greater than *stop*. - - If the returned array would contain an infinite number of values, an empty range is returned. - The arguments are not required to be integers. + If ``step`` is omitted, it defaults to 1. If ``start`` is omitted, it defaults to 0. The + ``stop`` value is exclusive; it is not included in the result. If ``step`` is positive, the + last element is the largest *start + i * step* less than ``stop``; if ``step`` is negative, + the last element is the smallest *start + i * step* greater than ``stop``. If the returned + array would contain an infinite number of values, an empty range is returned. The arguments + are not required to be integers. """ return FunctionExpression("sequence", args) @classmethod - def slice(cls, *args) -> FunctionExpression: + def slice( + cls, array: IntoExpression, start: IntoExpression, end: IntoExpression = None, / + ) -> Expression: """ - Returns a section of *array* between the *start* and *end* indices. + Returns a section of ``array`` between the ``start`` and ``end`` indices. - If the *end* argument is negative, it is treated as an offset from the end of the array `length(array) + end`. + If the ``end`` argument is negative, it is treated as an offset from the end of the array + (*alt.expr.length(array) + end*). """ - return FunctionExpression("slice", args) + return FunctionExpression("slice", (array, start, end)) @classmethod - def span(cls, *args) -> FunctionExpression: - """ - Returns the span of *array*: the difference between the last and first elements, or `array[array.length-1] - array[0]`. - - Or if input is a string: a section of *string* between the *start* and *end* indices. - If the *end* argument is negative, it is treated as an offset from the end of the string `length(string) + end`. - """ - return FunctionExpression("span", args) + def span(cls, array: IntoExpression, /) -> Expression: + """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" + return FunctionExpression("span", (array,)) @classmethod - def lower(cls, *args) -> FunctionExpression: - """Transforms *string* to lower-case letters.""" - return FunctionExpression("lower", args) + def lower(cls, string: IntoExpression, /) -> Expression: + """Transforms ``string`` to lower-case letters.""" + return FunctionExpression("lower", (string,)) @classmethod - def pad(cls, *args) -> FunctionExpression: + def pad( + cls, + string: IntoExpression, + length: IntoExpression, + character: IntoExpression = None, + align: IntoExpression = None, + /, + ) -> Expression: """ - Pads a *string* value with repeated instances of a *character* up to a specified *length*. + Pads a ``string`` value with repeated instances of a ``character`` up to a specified ``length``. - If *character* is not specified, a space (' ') is used. - By default, padding is added to the end of a string. - An optional *align* parameter specifies if padding should be added to the `'left'` (beginning), `'center'`, or `'right'` (end) of the input string. + If ``character`` is not specified, a space (' ') is used. By default, padding is added to + the end of a string. An optional ``align`` parameter specifies if padding should be added to + the ``'left'`` (beginning), ``'center'``, or ``'right'`` (end) of the input string. """ - return FunctionExpression("pad", args) + return FunctionExpression("pad", (string, length, character, align)) @classmethod - def parseFloat(cls, *args) -> FunctionExpression: + def parseFloat(cls, string: IntoExpression, /) -> Expression: """ - Parses the input *string* to a floating-point value. + Parses the input ``string`` to a floating-point value. - Same as JavaScript's `parseFloat`. + Same as JavaScript's ``parseFloat``. """ - return FunctionExpression("parseFloat", args) + return FunctionExpression("parseFloat", (string,)) @classmethod - def parseInt(cls, *args) -> FunctionExpression: + def parseInt(cls, string: IntoExpression, /) -> Expression: """ - Parses the input *string* to an integer value. + Parses the input ``string`` to an integer value. - Same as JavaScript's `parseInt`. + Same as JavaScript's ``parseInt``. """ - return FunctionExpression("parseInt", args) + return FunctionExpression("parseInt", (string,)) @classmethod - def replace(cls, *args) -> FunctionExpression: + def replace( + cls, + string: IntoExpression, + pattern: IntoExpression, + replacement: IntoExpression, + /, + ) -> Expression: """ - Returns a new string with some or all matches of *pattern* replaced by a *replacement* string. + Returns a new string with some or all matches of ``pattern`` replaced by a ``replacement`` string. - The *pattern* can be a string or a regular expression. - If *pattern* is a string, only the first instance will be replaced. - Same as `JavaScript's String.replace `__. - """ - return FunctionExpression("replace", args) + The ``pattern`` can be a string or a regular expression. If ``pattern`` is a string, only + the first instance will be replaced. Same as `JavaScript's String.replace`_. - @classmethod - def split(cls, *args) -> FunctionExpression: + .. _JavaScript's String.replace: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace """ - Returns an array of tokens created by splitting the input *string* according to a provided *separator* pattern. - - The result can optionally be constrained to return at most *limit* tokens. - """ - return FunctionExpression("split", args) + return FunctionExpression("replace", (string, pattern, replacement)) @classmethod - def substring(cls, *args) -> FunctionExpression: - """Returns a section of *string* between the *start* and *end* indices.""" - return FunctionExpression("substring", args) + def substring( + cls, + string: IntoExpression, + start: IntoExpression, + end: IntoExpression = None, + /, + ) -> Expression: + """Returns a section of ``string`` between the ``start`` and ``end`` indices.""" + return FunctionExpression("substring", (string, start, end)) @classmethod - def trim(cls, *args) -> FunctionExpression: + def trim(cls, string: IntoExpression, /) -> Expression: """Returns a trimmed string with preceding and trailing whitespace removed.""" - return FunctionExpression("trim", args) + return FunctionExpression("trim", (string,)) @classmethod - def truncate(cls, *args) -> FunctionExpression: - r""" - Truncates an input *string* to a target *length*. + def truncate( + cls, + string: IntoExpression, + length: IntoExpression, + align: IntoExpression = None, + ellipsis: IntoExpression = None, + /, + ) -> Expression: + """ + Truncates an input ``string`` to a target ``length``. - The optional *align* argument indicates what part of the string should be truncated: `'left'` (the beginning), `'center'`, or `'right'` (the end). - By default, the `'right'` end of the string is truncated. - The optional *ellipsis* argument indicates the string to use to indicate truncated content; - by default the ellipsis character `...` (`\\u2026`) is used. + The optional ``align`` argument indicates what part of the string should be truncated: + ``'left'`` (the beginning), ``'center'``, or ``'right'`` (the end). By default, the + ``'right'`` end of the string is truncated. The optional ``ellipsis`` argument indicates the + string to use to indicate truncated content; by default the ellipsis character ``…`` + (``\u2026``) is used. """ - return FunctionExpression("truncate", args) + return FunctionExpression("truncate", (string, length, align, ellipsis)) @classmethod - def upper(cls, *args) -> FunctionExpression: - """Transforms *string* to upper-case letters.""" - return FunctionExpression("upper", args) + def upper(cls, string: IntoExpression, /) -> Expression: + """Transforms ``string`` to upper-case letters.""" + return FunctionExpression("upper", (string,)) @classmethod - def merge(cls, *args) -> FunctionExpression: + def merge( + cls, object1: IntoExpression, object2: IntoExpression = None, *args: Any + ) -> Expression: """ - Merges the input objects *object1*, *object2*, etc into a new output object. - - Inputs are visited in sequential order, such that key values from later arguments can overwrite those from earlier arguments. + Merges the input objects ``object1``, ``object2``, etc into a new output object. - Example: `merge({a:1, b:2}, {a:3}) -> {a:3, b:2}`. + Inputs are visited in sequential order, such that key values from later arguments can + overwrite those from earlier arguments. Example: ``alt.expr.merge({a:1, b:2}, {a:3}) -> + {a:3, b:2}``. """ - return FunctionExpression("merge", args) + return FunctionExpression("merge", (object1, object2, *args)) @classmethod - def dayFormat(cls, *args) -> FunctionExpression: + def dayFormat(cls, day: IntoExpression, /) -> Expression: """ Formats a (0-6) *weekday* number as a full week day name, according to the current locale. - For example: `dayFormat(0) -> "Sunday"`. + For example: ``alt.expr.dayFormat(0) -> "Sunday"``. """ - return FunctionExpression("dayFormat", args) + return FunctionExpression("dayFormat", (day,)) @classmethod - def dayAbbrevFormat(cls, *args) -> FunctionExpression: + def dayAbbrevFormat(cls, day: IntoExpression, /) -> Expression: """ Formats a (0-6) *weekday* number as an abbreviated week day name, according to the current locale. - For example: `dayAbbrevFormat(0) -> "Sun"`. + For example: ``alt.expr.dayAbbrevFormat(0) -> "Sun"``. """ - return FunctionExpression("dayAbbrevFormat", args) + return FunctionExpression("dayAbbrevFormat", (day,)) @classmethod - def format(cls, *args) -> FunctionExpression: + def format(cls, value: IntoExpression, specifier: IntoExpression, /) -> Expression: """ - Formats a numeric *value* as a string. + Formats a numeric ``value`` as a string. - The *specifier* must be a valid `d3-format specifier `__ (e.g., `format(value, ',.2f')`. + The ``specifier`` must be a valid `d3-format specifier`_ (e.g., ``alt.expr.format(value, + ',.2f')``. Null values are formatted as ``"null"``. + + .. _d3-format specifier: + https://github.com/d3/d3-format/ """ - return FunctionExpression("format", args) + return FunctionExpression("format", (value, specifier)) @classmethod - def monthFormat(cls, *args) -> FunctionExpression: + def monthFormat(cls, month: IntoExpression, /) -> Expression: """ - Formats a (zero-based) *month* number as a full month name, according to the current locale. + Formats a (zero-based) ``month`` number as a full month name, according to the current locale. - For example: `monthFormat(0) -> "January"`. + For example: ``alt.expr.monthFormat(0) -> "January"``. """ - return FunctionExpression("monthFormat", args) + return FunctionExpression("monthFormat", (month,)) @classmethod - def monthAbbrevFormat(cls, *args) -> FunctionExpression: + def monthAbbrevFormat(cls, month: IntoExpression, /) -> Expression: """ - Formats a (zero-based) *month* number as an abbreviated month name, according to the current locale. + Formats a (zero-based) ``month`` number as an abbreviated month name, according to the current locale. - For example: `monthAbbrevFormat(0) -> "Jan"`. + For example: ``alt.expr.monthAbbrevFormat(0) -> "Jan"``. """ - return FunctionExpression("monthAbbrevFormat", args) + return FunctionExpression("monthAbbrevFormat", (month,)) @classmethod - def timeUnitSpecifier(cls, *args) -> FunctionExpression: + def timeUnitSpecifier( + cls, units: IntoExpression, specifiers: IntoExpression = None, / + ) -> Expression: """ - Returns a time format specifier string for the given time `unit `__. - - The optional *specifiers* object provides a set of specifier sub-strings for customizing the format; - for more, see the `timeUnitSpecifier API documentation `__. + Returns a time format specifier string for the given time `*units*`_. - The resulting specifier string can then be used as input to the `timeFormat `__ or - `utcFormat `__ functions, or as the *format* parameter of an axis or legend. + The optional ``specifiers`` object provides a set of specifier sub-strings for customizing + the format; for more, see the `timeUnitSpecifier API documentation`_. The resulting + specifier string can then be used as input to the `timeFormat`_ or `utcFormat`_ functions, + or as the *format* parameter of an axis or legend. For example: ``alt.expr.timeFormat(date, + alt.expr.timeUnitSpecifier('year'))`` or ``alt.expr.timeFormat(date, + alt.expr.timeUnitSpecifier(['hours', 'minutes']))``. - For example: `timeFormat(date, timeUnitSpecifier('year'))` or `timeFormat(date, timeUnitSpecifier(['hours', 'minutes']))`. + .. _*units*: + https://vega.github.io/vega/docs/api/time/#time-units + .. _timeUnitSpecifier API documentation: + https://vega.github.io/vega/docs/api/time/#timeUnitSpecifier + .. _timeFormat: + https://vega.github.io/vega/docs/expressions/#timeFormat + .. _utcFormat: + https://vega.github.io/vega/docs/expressions/#utcFormat """ - return FunctionExpression("timeUnitSpecifier", args) + return FunctionExpression("timeUnitSpecifier", (units, specifiers)) @classmethod - def timeFormat(cls, *args) -> FunctionExpression: + def timeFormat( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: """ - Formats a datetime *value* (either a `Date` object or timestamp) as a string, according to the local time. + Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to the local time. - The *specifier* must be a valid `d3-time-format specifier `__. - For example: `timeFormat(timestamp, '%A')`. + The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. + For example: ``alt.expr.timeFormat(timestamp, '%A')``. Null values are formatted as + ``"null"``. + + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + .. _TimeMultiFormat object: + https://vega.github.io/vega/docs/types/#TimeMultiFormat """ - return FunctionExpression("timeFormat", args) + return FunctionExpression("timeFormat", (value, specifier)) @classmethod - def timeParse(cls, *args) -> FunctionExpression: + def timeParse( + cls, string: IntoExpression, specifier: IntoExpression, / + ) -> Expression: """ - Parses a *string* value to a Date object, according to the local time. + Parses a ``string`` value to a Date object, according to the local time. + + The ``specifier`` must be a valid `d3-time-format specifier`_. For example: + ``alt.expr.timeParse('June 30, 2015', '%B %d, %Y')``. - The *specifier* must be a valid `d3-time-format specifier `__. - For example: `timeParse('June 30, 2015', '%B %d, %Y')`. + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ """ - return FunctionExpression("timeParse", args) + return FunctionExpression("timeParse", (string, specifier)) @classmethod - def utcFormat(cls, *args) -> FunctionExpression: + def utcFormat( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: """ - Formats a datetime *value* (either a `Date` object or timestamp) as a string, according to `UTC `__ time. + Formats a datetime ``value`` (either a ``Date`` object or timestamp) as a string, according to `UTC`_ time. - The *specifier* must be a valid `d3-time-format specifier `__. - For example: `utcFormat(timestamp, '%A')`. + The ``specifier`` must be a valid `d3-time-format specifier`_ or `TimeMultiFormat object`_. + For example: ``alt.expr.utcFormat(timestamp, '%A')``. Null values are formatted as + ``"null"``. + + .. _UTC: + https://en.wikipedia.org/wiki/Coordinated_Universal_Time + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ + .. _TimeMultiFormat object: + https://vega.github.io/vega/docs/types/#TimeMultiFormat """ - return FunctionExpression("utcFormat", args) + return FunctionExpression("utcFormat", (value, specifier)) @classmethod - def utcParse(cls, *args) -> FunctionExpression: + def utcParse( + cls, value: IntoExpression, specifier: IntoExpression, / + ) -> Expression: """ - Parses a *string* value to a Date object, according to `UTC `__ time. + Parses a *string* value to a Date object, according to `UTC`_ time. + + The ``specifier`` must be a valid `d3-time-format specifier`_. For example: + ``alt.expr.utcParse('June 30, 2015', '%B %d, %Y')``. - The *specifier* must be a valid `d3-time-format specifier `__. - For example: `utcParse('June 30, 2015', '%B %d, %Y')`. + .. _UTC: + https://en.wikipedia.org/wiki/Coordinated_Universal_Time + .. _d3-time-format specifier: + https://github.com/d3/d3-time-format/ """ - return FunctionExpression("utcParse", args) + return FunctionExpression("utcParse", (value, specifier)) @classmethod - def regexp(cls, *args) -> FunctionExpression: + def regexp( + cls, pattern: IntoExpression, flags: IntoExpression = None, / + ) -> Expression: """ - Creates a regular expression instance from an input *pattern* string and optional *flags*. + Creates a regular expression instance from an input ``pattern`` string and optional ``flags``. - Same as `JavaScript's `RegExp` `__. + Same as `JavaScript's RegExp`_. + + .. _JavaScript's RegExp: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp """ - return FunctionExpression("regexp", args) + return FunctionExpression("regexp", (pattern, flags)) @classmethod - def test(cls, *args) -> FunctionExpression: + def test( + cls, regexp: IntoExpression, string: IntoExpression = None, / + ) -> Expression: r""" - Evaluates a regular expression *regexp* against the input *string*, returning `true` if the string matches the pattern, `false` otherwise. + Evaluates a regular expression ``regexp`` against the input ``string``, returning ``true`` if the string matches the pattern, ``false`` otherwise. - For example: `test(\d{3}, "32-21-9483") -> true`. + For example: ``alt.expr.test(/\\d{3}/, "32-21-9483") -> true``. """ - return FunctionExpression("test", args) + return FunctionExpression("test", (regexp, string)) @classmethod - def rgb(cls, *args) -> FunctionExpression: + def rgb(cls, *args: Any) -> Expression: """ - Constructs a new `RGB `__ color. + Constructs a new `RGB`_ color. + + If ``r``, ``g`` and ``b`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the RGB color space. Uses + `d3-color's rgb function`_. - If *r*, *g* and *b* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. - If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the RGB color space. Uses `d3-color's rgb function `__. + .. _RGB: + https://en.wikipedia.org/wiki/RGB_color_model + .. _d3-color's rgb function: + https://github.com/d3/d3-color#rgb """ return FunctionExpression("rgb", args) @classmethod - def hsl(cls, *args) -> FunctionExpression: + def hsl(cls, *args: Any) -> Expression: """ - Constructs a new `HSL `__ color. + Constructs a new `HSL`_ color. - If *h*, *s* and *l* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. - If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the HSL color space. - Uses `d3-color's hsl function `__. + If ``h``, ``s`` and ``l`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the HSL color space. Uses + `d3-color's hsl function`_. + + .. _HSL: + https://en.wikipedia.org/wiki/HSL_and_HSV + .. _d3-color's hsl function: + https://github.com/d3/d3-color#hsl """ return FunctionExpression("hsl", args) @classmethod - def lab(cls, *args) -> FunctionExpression: - """ - Constructs a new `CIE LAB `__ color. - - If *l*, *a* and *b* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. - If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the LAB color space. - Uses `d3-color's lab function `__. + def lab(cls, *args: Any) -> Expression: """ - return FunctionExpression("lab", args) + Constructs a new `CIE LAB`_ color. - @classmethod - def hcl(cls, *args) -> FunctionExpression: - """ - Constructs a new `HCL `__ (hue, chroma, luminance) color. + If ``l``, ``a`` and ``b`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the LAB color space. Uses + `d3-color's lab function`_. - If *h*, *c* and *l* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. - If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the HCL color space. - Uses `d3-color's hcl function `__. + .. _CIE LAB: + https://en.wikipedia.org/wiki/Lab_color_space#CIELAB + .. _d3-color's lab function: + https://github.com/d3/d3-color#lab """ - return FunctionExpression("hcl", args) + return FunctionExpression("lab", args) @classmethod - def luminance(cls, *args) -> FunctionExpression: - """ - Returns the luminance for the given color *specifier* (compatible with `d3-color's rgb function `__. - - The luminance is calculated according to the `W3C Web Content Accessibility Guidelines `__. + def hcl(cls, *args: Any) -> Expression: """ - return FunctionExpression("luminance", args) + Constructs a new `HCL`_ (hue, chroma, luminance) color. - @classmethod - def contrast(cls, *args) -> FunctionExpression: - """ - Returns the contrast ratio between the input color specifiers as a float between 1 and 21. + If ``h``, ``c`` and ``l`` are specified, these represent the channel values of the returned + color; an ``opacity`` may also be specified. If a CSS Color Module Level 3 *specifier* + string is specified, it is parsed and then converted to the HCL color space. Uses + `d3-color's hcl function`_. - The contrast is calculated according to the `W3C Web Content Accessibility Guidelines `__. + .. _HCL: + https://en.wikipedia.org/wiki/Lab_color_space#CIELAB + .. _d3-color's hcl function: + https://github.com/d3/d3-color#hcl """ - return FunctionExpression("contrast", args) - - @classmethod - def item(cls, *args) -> FunctionExpression: - """Returns the current scenegraph item that is the target of the event.""" - return FunctionExpression("item", args) + return FunctionExpression("hcl", args) @classmethod - def group(cls, *args) -> FunctionExpression: + def group(cls, name: IntoExpression = None, /) -> Expression: """ Returns the scenegraph group mark item in which the current event has occurred. - If no arguments are provided, the immediate parent group is returned. - If a group name is provided, the matching ancestor group item is returned. + If no arguments are provided, the immediate parent group is returned. If a group name is + provided, the matching ancestor group item is returned. """ - return FunctionExpression("group", args) + return FunctionExpression("group", (name,)) @classmethod - def xy(cls, *args) -> FunctionExpression: + def xy(cls, item: IntoExpression = None, /) -> Expression: """ Returns the x- and y-coordinates for the current event as a two-element array. - If no arguments are provided, the top-level coordinate space of the view is used. - If a scenegraph *item* (or string group name) is provided, the coordinate space of the group item is used. + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. """ - return FunctionExpression("xy", args) + return FunctionExpression("xy", (item,)) @classmethod - def x(cls, *args) -> FunctionExpression: + def x(cls, item: IntoExpression = None, /) -> Expression: """ Returns the x coordinate for the current event. - If no arguments are provided, the top-level coordinate space of the view is used. - If a scenegraph *item* (or string group name) is provided, the coordinate space of the group item is used. + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. """ - return FunctionExpression("x", args) + return FunctionExpression("x", (item,)) @classmethod - def y(cls, *args) -> FunctionExpression: + def y(cls, item: IntoExpression = None, /) -> Expression: """ Returns the y coordinate for the current event. - If no arguments are provided, the top-level coordinate space of the view is used. - If a scenegraph *item* (or string group name) is provided, the coordinate space of the group item is used. + If no arguments are provided, the top-level coordinate space of the view is used. If a + scenegraph ``item`` (or string group name) is provided, the coordinate space of the group + item is used. """ - return FunctionExpression("y", args) + return FunctionExpression("y", (item,)) @classmethod - def pinchDistance(cls, *args) -> FunctionExpression: + def pinchDistance(cls, event: IntoExpression, /) -> Expression: """Returns the pixel distance between the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchDistance", args) + return FunctionExpression("pinchDistance", (event,)) @classmethod - def pinchAngle(cls, *args) -> FunctionExpression: + def pinchAngle(cls, event: IntoExpression, /) -> Expression: """Returns the angle of the line connecting the first two touch points of a multi-touch event.""" - return FunctionExpression("pinchAngle", args) + return FunctionExpression("pinchAngle", (event,)) @classmethod - def inScope(cls, *args) -> FunctionExpression: - """Returns true if the given scenegraph *item* is a descendant of the group mark in which the event handler was defined, false otherwise.""" - return FunctionExpression("inScope", args) + def inScope(cls, item: IntoExpression, /) -> Expression: + """Returns true if the given scenegraph ``item`` is a descendant of the group mark in which the event handler was defined, false otherwise.""" + return FunctionExpression("inScope", (item,)) @classmethod - def data(cls, *args) -> FunctionExpression: + def data(cls, name: IntoExpression, /) -> Expression: """ - Returns the array of data objects for the Vega data set with the given *name*. + Returns the array of data objects for the Vega data set with the given ``name``. If the data set is not found, returns an empty array. """ - return FunctionExpression("data", args) + return FunctionExpression("data", (name,)) @classmethod - def indata(cls, *args) -> FunctionExpression: + def indata( + cls, name: IntoExpression, field: IntoExpression, value: IntoExpression, / + ) -> Expression: """ - Tests if the data set with a given *name* contains a datum with a *field* value that matches the input *value*. + Tests if the data set with a given ``name`` contains a datum with a ``field`` value that matches the input ``value``. - For example: `indata('table', 'category', value)`. + For example: ``alt.expr.indata('table', 'category', value)``. """ - return FunctionExpression("indata", args) + return FunctionExpression("indata", (name, field, value)) @classmethod - def scale(cls, *args) -> FunctionExpression: + def scale( + cls, + name: IntoExpression, + value: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: """ - Applies the named scale transform (or projection) to the specified *value*. + Applies the named scale transform (or projection) to the specified ``value``. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. """ - return FunctionExpression("scale", args) + return FunctionExpression("scale", (name, value, group)) @classmethod - def invert(cls, *args) -> FunctionExpression: + def invert( + cls, + name: IntoExpression, + value: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: """ - Inverts the named scale transform (or projection) for the specified *value*. + Inverts the named scale transform (or projection) for the specified ``value``. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. """ - return FunctionExpression("invert", args) + return FunctionExpression("invert", (name, value, group)) @classmethod - def copy(cls, *args) -> FunctionExpression: # type: ignore[override] + def copy(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: # type: ignore[override] """ - Returns a copy (a new cloned instance) of the named scale transform of projection, or `undefined` if no scale or projection is found. + Returns a copy (a new cloned instance) of the named scale transform of projection, or ``undefined`` if no scale or projection is found. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale or projection. """ - # error: Signature of "copy" incompatible with supertype "SchemaBase" [override] - # note: def copy(self, deep: bool | Iterable[Any] = ..., ignore: list[str] | None = ...) -> expr - # NOTE: Not relevant as `expr() -> ExprRef` - # this method is only accesible via `expr.copy()` - return FunctionExpression("copy", args) + return FunctionExpression("copy", (name, group)) @classmethod - def domain(cls, *args) -> FunctionExpression: + def domain( + cls, name: IntoExpression, group: IntoExpression = None, / + ) -> Expression: """ Returns the scale domain array for the named scale transform, or an empty array if the scale is not found. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. """ - return FunctionExpression("domain", args) + return FunctionExpression("domain", (name, group)) @classmethod - def range(cls, *args) -> FunctionExpression: + def range(cls, name: IntoExpression, group: IntoExpression = None, /) -> Expression: """ Returns the scale range array for the named scale transform, or an empty array if the scale is not found. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. """ - return FunctionExpression("range", args) + return FunctionExpression("range", (name, group)) @classmethod - def bandwidth(cls, *args) -> FunctionExpression: + def bandwidth( + cls, name: IntoExpression, group: IntoExpression = None, / + ) -> Expression: """ Returns the current band width for the named band scale transform, or zero if the scale is not found or is not a band scale. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the scale. """ - return FunctionExpression("bandwidth", args) + return FunctionExpression("bandwidth", (name, group)) @classmethod - def bandspace(cls, *args) -> FunctionExpression: + def bandspace( + cls, + count: IntoExpression, + paddingInner: IntoExpression = None, + paddingOuter: IntoExpression = None, + /, + ) -> Expression: """ - Returns the number of steps needed within a band scale, based on the *count* of domain elements and the inner and outer padding values. + Returns the number of steps needed within a band scale, based on the ``count`` of domain elements and the inner and outer padding values. - While normally calculated within the scale itself, this function can be helpful for determining the size of a chart's layout. + While normally calculated within the scale itself, this function can be helpful for + determining the size of a chart's layout. """ - return FunctionExpression("bandspace", args) + return FunctionExpression("bandspace", (count, paddingInner, paddingOuter)) @classmethod - def gradient(cls, *args) -> FunctionExpression: + def gradient( + cls, + scale: IntoExpression, + p0: IntoExpression, + p1: IntoExpression, + count: IntoExpression = None, + /, + ) -> Expression: """ - Returns a linear color gradient for the *scale* (whose range must be a `continuous color scheme `__ and starting and ending points *p0* and *p1*, each an `[x, y]` array. + Returns a linear color gradient for the ``scale`` (whose range must be a `continuous color scheme`_) and starting and ending points ``p0`` and ``p1``, each an *[x, y]* array. - The points *p0* and *p1* should be expressed in normalized coordinates in the domain `[0, 1]`, relative to the bounds of the item being colored. + The points ``p0`` and ``p1`` should be expressed in normalized coordinates in the domain [0, + 1], relative to the bounds of the item being colored. If unspecified, ``p0`` defaults to + ``[0, 0]`` and ``p1`` defaults to ``[1, 0]``, for a horizontal gradient that spans the full + bounds of an item. The optional ``count`` argument indicates a desired target number of + sample points to take from the color scale. - If unspecified, *p0* defaults to `[0, 0]` and *p1* defaults to `[1, 0]`, for a horizontal gradient that spans the full bounds of an item. - The optional *count* argument indicates a desired target number of sample points to take from the color scale. + .. _continuous color scheme: + https://vega.github.io/vega/docs/schemes """ - return FunctionExpression("gradient", args) + return FunctionExpression("gradient", (scale, p0, p1, count)) @classmethod - def panLinear(cls, *args) -> FunctionExpression: + def panLinear(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: """ - Given a linear scale *domain* array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional *delta*. + Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - The *delta* value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range. + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. """ - return FunctionExpression("panLinear", args) + return FunctionExpression("panLinear", (domain, delta)) @classmethod - def panLog(cls, *args) -> FunctionExpression: + def panLog(cls, domain: IntoExpression, delta: IntoExpression, /) -> Expression: """ - Given a log scale *domain* array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional *delta*. + Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - The *delta* value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range. + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. """ - return FunctionExpression("panLog", args) + return FunctionExpression("panLog", (domain, delta)) @classmethod - def panPow(cls, *args) -> FunctionExpression: + def panPow( + cls, domain: IntoExpression, delta: IntoExpression, exponent: IntoExpression, / + ) -> Expression: """ - Given a power scale *domain* array with numeric or datetime values and the given *exponent*, returns a new two-element domain array that is the result of panning the domain by a fractional *delta*. + Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - The *delta* value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range. + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. """ - return FunctionExpression("panPow", args) + return FunctionExpression("panPow", (domain, delta, exponent)) @classmethod - def panSymlog(cls, *args) -> FunctionExpression: + def panSymlog( + cls, domain: IntoExpression, delta: IntoExpression, constant: IntoExpression, / + ) -> Expression: """ - Given a symmetric log scale *domain* array with numeric or datetime values parameterized by the given *constant*, returns a new two-element domain array that is the result of panning the domain by a fractional *delta*. + Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of panning the domain by a fractional ``delta``. - The *delta* value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range. + The ``delta`` value represents fractional units of the scale range; for example, ``0.5`` + indicates panning the scale domain to the right by half the scale range. """ - return FunctionExpression("panSymlog", args) + return FunctionExpression("panSymlog", (domain, delta, constant)) @classmethod - def zoomLinear(cls, *args) -> FunctionExpression: + def zoomLinear( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + /, + ) -> Expression: """ - Given a linear scale *domain* array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a *scaleFactor*, centered at the provided fractional *anchor*. + Given a linear scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - The *anchor* value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range. + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. """ - return FunctionExpression("zoomLinear", args) + return FunctionExpression("zoomLinear", (domain, anchor, scaleFactor)) @classmethod - def zoomLog(cls, *args) -> FunctionExpression: + def zoomLog( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + /, + ) -> Expression: """ - Given a log scale *domain* array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a *scaleFactor*, centered at the provided fractional *anchor*. + Given a log scale ``domain`` array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - The *anchor* value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range. + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. """ - return FunctionExpression("zoomLog", args) + return FunctionExpression("zoomLog", (domain, anchor, scaleFactor)) @classmethod - def zoomPow(cls, *args) -> FunctionExpression: + def zoomPow( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + exponent: IntoExpression, + /, + ) -> Expression: """ - Given a power scale *domain* array with numeric or datetime values and the given *exponent*, returns a new two-element domain array that is the result of zooming the domain by a *scaleFactor*, centered at the provided fractional *anchor*. + Given a power scale ``domain`` array with numeric or datetime values and the given ``exponent``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - The *anchor* value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range. + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. """ - return FunctionExpression("zoomPow", args) + return FunctionExpression("zoomPow", (domain, anchor, scaleFactor, exponent)) @classmethod - def zoomSymlog(cls, *args) -> FunctionExpression: + def zoomSymlog( + cls, + domain: IntoExpression, + anchor: IntoExpression, + scaleFactor: IntoExpression, + constant: IntoExpression, + /, + ) -> Expression: """ - Given a symmetric log scale *domain* array with numeric or datetime values parameterized by the given *constant*, returns a new two-element domain array that is the result of zooming the domain by a *scaleFactor*, centered at the provided fractional *anchor*. + Given a symmetric log scale ``domain`` array with numeric or datetime values parameterized by the given ``constant``, returns a new two-element domain array that is the result of zooming the domain by a ``scaleFactor``, centered at the provided fractional ``anchor``. - The *anchor* value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range. + The ``anchor`` value represents the zoom position in terms of fractional units of the scale + range; for example, ``0.5`` indicates a zoom centered on the mid-point of the scale range. """ - return FunctionExpression("zoomSymlog", args) + return FunctionExpression("zoomSymlog", (domain, anchor, scaleFactor, constant)) @classmethod - def geoArea(cls, *args) -> FunctionExpression: + def geoArea( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: """ - Returns the projected planar area (typically in square pixels) of a GeoJSON *feature* according to the named *projection*. + Returns the projected planar area (typically in square pixels) of a GeoJSON ``feature`` according to the named ``projection``. + + If the ``projection`` argument is ``null``, computes the spherical area in steradians using + unprojected longitude, latitude coordinates. The optional ``group`` argument takes a + scenegraph group mark item to indicate the specific scope in which to look up the + projection. Uses d3-geo's `geoArea`_ and `path.area`_ methods. - If the *projection* argument is `null`, computes the spherical area in steradians using unprojected longitude, latitude coordinates. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. - Uses d3-geo's `geoArea `__ and `path.area `__ methods. + .. _geoArea: + https://github.com/d3/d3-geo#geoArea + .. _path.area: + https://github.com/d3/d3-geo#path_area """ - return FunctionExpression("geoArea", args) + return FunctionExpression("geoArea", (projection, feature, group)) @classmethod - def geoBounds(cls, *args) -> FunctionExpression: + def geoBounds( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: """ - Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON *feature*, according to the named *projection*. + Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. - The bounding box is represented by a two-dimensional array: `[[x0, y0], [x1, y1]]`, - where *x0* is the minimum x-coordinate, *y0* is the minimum y-coordinate, - *x1* is the maximum x-coordinate, and *y1* is the maximum y-coordinate. + The bounding box is represented by a two-dimensional array: [[*x₀*, *y₀*], [*x₁*, *y₁*]], + where *x₀* is the minimum x-coordinate, *y₀* is the minimum y-coordinate, *x₁* is the + maximum x-coordinate, and *y₁* is the maximum y-coordinate. If the ``projection`` argument + is ``null``, computes the spherical bounding box using unprojected longitude, latitude + coordinates. The optional ``group`` argument takes a scenegraph group mark item to indicate + the specific scope in which to look up the projection. Uses d3-geo's `geoBounds`_ and + `path.bounds`_ methods. - If the *projection* argument is `null`, computes the spherical bounding box using unprojected longitude, latitude coordinates. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. - Uses d3-geo's `geoBounds `__ and `path.bounds `__ methods. + .. _geoBounds: + https://github.com/d3/d3-geo#geoBounds + .. _path.bounds: + https://github.com/d3/d3-geo#path_bounds """ - return FunctionExpression("geoBounds", args) + return FunctionExpression("geoBounds", (projection, feature, group)) @classmethod - def geoCentroid(cls, *args) -> FunctionExpression: + def geoCentroid( + cls, + projection: IntoExpression, + feature: IntoExpression, + group: IntoExpression = None, + /, + ) -> Expression: """ - Returns the projected planar centroid (typically in pixels) for the specified GeoJSON *feature*, according to the named *projection*. + Returns the projected planar centroid (typically in pixels) for the specified GeoJSON ``feature``, according to the named ``projection``. - If the *projection* argument is `null`, computes the spherical centroid using unprojected longitude, latitude coordinates. - The optional *group* argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. - Uses d3-geo's `geoCentroid `__ and `path.centroid `__ methods. + If the ``projection`` argument is ``null``, computes the spherical centroid using + unprojected longitude, latitude coordinates. The optional ``group`` argument takes a + scenegraph group mark item to indicate the specific scope in which to look up the + projection. Uses d3-geo's `geoCentroid`_ and `path.centroid`_ methods. + + .. _geoCentroid: + https://github.com/d3/d3-geo#geoCentroid + .. _path.centroid: + https://github.com/d3/d3-geo#path_centroid """ - return FunctionExpression("geoCentroid", args) + return FunctionExpression("geoCentroid", (projection, feature, group)) @classmethod - def treePath(cls, *args) -> FunctionExpression: + def geoScale( + cls, projection: IntoExpression, group: IntoExpression = None, / + ) -> Expression: """ - For the hierarchy data set with the given *name*, returns the shortest path through from the *source* node id to the *target* node id. + Returns the scale value for the named ``projection``. - The path starts at the *source* node, ascends to the least common ancestor of the *source* node and the *target* node, and then descends to the *target* node. + The optional ``group`` argument takes a scenegraph group mark item to indicate the specific + scope in which to look up the projection. """ - return FunctionExpression("treePath", args) + return FunctionExpression("geoScale", (projection, group)) @classmethod - def treeAncestors(cls, *args) -> FunctionExpression: - """For the hierarchy data set with the given *name*, returns the array of ancestors nodes, starting with the input *node*, then followed by each parent up to the root.""" - return FunctionExpression("treeAncestors", args) - - @classmethod - def containerSize(cls, *args) -> FunctionExpression: + def treePath( + cls, name: IntoExpression, source: IntoExpression, target: IntoExpression, / + ) -> Expression: """ - Returns the current CSS box size (`[el.clientWidth, el.clientHeight]`) of the parent DOM element that contains the Vega view. + For the hierarchy data set with the given ``name``, returns the shortest path through from the ``source`` node id to the ``target`` node id. - If there is no container element, returns `[undefined, undefined]`. + The path starts at the ``source`` node, ascends to the least common ancestor of the + ``source`` node and the ``target`` node, and then descends to the ``target`` node. """ - return FunctionExpression("containerSize", args) - - @classmethod - def screen(cls, *args) -> FunctionExpression: - """Returns the `window.screen `__ object, or `{}` if Vega is not running in a browser environment.""" - return FunctionExpression("screen", args) + return FunctionExpression("treePath", (name, source, target)) @classmethod - def windowSize(cls, *args) -> FunctionExpression: - """Returns the current window size (`[window.innerWidth, window.innerHeight]`) or `[undefined, undefined]` if Vega is not running in a browser environment.""" - return FunctionExpression("windowSize", args) + def treeAncestors(cls, name: IntoExpression, node: IntoExpression, /) -> Expression: + """For the hierarchy data set with the given ``name``, returns the array of ancestors nodes, starting with the input ``node``, then followed by each parent up to the root.""" + return FunctionExpression("treeAncestors", (name, node)) @classmethod - def warn(cls, *args) -> FunctionExpression: + def warn( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: """ Logs a warning message and returns the last argument. - For the message to appear in the console, the visualization view must have the appropriate logging level set. + For the message to appear in the console, the visualization view must have the appropriate + logging level set. """ - return FunctionExpression("warn", args) + return FunctionExpression("warn", (value1, value2, *args)) @classmethod - def info(cls, *args) -> FunctionExpression: + def info( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: """ Logs an informative message and returns the last argument. - For the message to appear in the console, the visualization view must have the appropriate logging level set. + For the message to appear in the console, the visualization view must have the appropriate + logging level set. """ - return FunctionExpression("info", args) + return FunctionExpression("info", (value1, value2, *args)) @classmethod - def debug(cls, *args) -> FunctionExpression: + def debug( + cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any + ) -> Expression: """ Logs a debugging message and returns the last argument. - For the message to appear in the console, the visualization view must have the appropriate logging level set. + For the message to appear in the console, the visualization view must have the appropriate + logging level set. """ - return FunctionExpression("debug", args) + return FunctionExpression("debug", (value1, value2, *args)) _ExprType = expr diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 6b2e24c51..3be003ae9 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -63,7 +63,7 @@ SCHEMA_FILE = "vega-lite-schema.json" THEMES_FILE = "vega-themes.json" EXPR_FILE: Path = ( - Path(__file__).parent / ".." / "altair" / "expr" / "dummy.py" + Path(__file__).parent / ".." / "altair" / "expr" / "__init__.py" ).resolve() CHANNEL_MYPY_IGNORE_STATEMENTS: Final = """\ From 21d13e722a4bb466c572710f9171fc15484d5f7a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:22:35 +0100 Subject: [PATCH 50/77] fix(typing): Resolve some revealed issues - `IntoExpression` change I'll add in a new fix PR - `OperatorMixin` todo needs an issue --- altair/expr/core.py | 2 +- tests/vegalite/v5/test_api.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/altair/expr/core.py b/altair/expr/core.py index e3bc65a52..e7872ada6 100644 --- a/altair/expr/core.py +++ b/altair/expr/core.py @@ -237,4 +237,4 @@ def __repr__(self) -> str: return f"{self.group}[{self.name!r}]" -IntoExpression: TypeAlias = Union[bool, None, str, OperatorMixin, Dict[str, Any]] +IntoExpression: TypeAlias = Union[bool, None, str, float, OperatorMixin, Dict[str, Any]] diff --git a/tests/vegalite/v5/test_api.py b/tests/vegalite/v5/test_api.py index a7d2f1c69..5ca60e1f8 100644 --- a/tests/vegalite/v5/test_api.py +++ b/tests/vegalite/v5/test_api.py @@ -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 = ( @@ -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( From 7e0db6842a0f1f158033f69d867a08ca43f28989 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:15:07 +0100 Subject: [PATCH 51/77] refactor: Move `.md`, `.rst` utils to `tools.markup.py` Also some rearranging in `vega_expr.py` --- tools/__init__.py | 9 ++- tools/generate_schema_wrapper.py | 2 +- tools/markup.py | 122 +++++++++++++++++++++++++++++++ tools/schemapi/utils.py | 58 +-------------- tools/schemapi/vega_expr.py | 113 +++++++++------------------- 5 files changed, 170 insertions(+), 134 deletions(-) create mode 100644 tools/markup.py diff --git a/tools/__init__.py b/tools/__init__.py index 052b8e9c0..46fc97553 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -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", ] diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 3be003ae9..09ce43770 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -20,6 +20,7 @@ sys.path.insert(0, str(Path.cwd())) +from tools.markup import rst_syntax_for_class from tools.schemapi import ( # noqa: F401 CodeSnippet, SchemaInfo, @@ -38,7 +39,6 @@ import_typing_extensions, indent_docstring, resolve_references, - rst_syntax_for_class, ruff_format_py, ruff_write_lint_format_str, spell_literal, diff --git a/tools/markup.py b/tools/markup.py new file mode 100644 index 000000000..fc1af9cfc --- /dev/null +++ b/tools/markup.py @@ -0,0 +1,122 @@ +"""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 +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, InlineParser + +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) + + +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(mistune.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() + return self.renderer(self._iter_render(tokens, state), state) + + +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() + + +def read_ast_tokens(source: Path, /) -> list[Token]: + """ + Read from ``source``, drop ``BlockState``. + + Factored out to provide accurate typing. + """ + return mistune.create_markdown(renderer="ast").read(source)[0] + + +def rst_syntax_for_class(class_name: str) -> str: + return f":class:`{class_name}`" diff --git a/tools/schemapi/utils.py b/tools/schemapi/utils.py index 5c4a84f9c..6bc7b1f4b 100644 --- a/tools/schemapi/utils.py +++ b/tools/schemapi/utils.py @@ -8,7 +8,6 @@ import sys import textwrap import urllib.parse -from html import unescape from itertools import chain from keyword import iskeyword from operator import itemgetter @@ -27,9 +26,7 @@ overload, ) -import mistune -from mistune.renderers.rst import RSTRenderer as _RSTRenderer - +from tools.markup import RSTParseVegaLite, rst_syntax_for_class from tools.schemapi.schemapi import _resolve_references as resolve_references if TYPE_CHECKING: @@ -37,7 +34,6 @@ from pathlib import Path from re import Pattern - from mistune import BlockState if sys.version_info >= (3, 12): from typing import TypeAliasType @@ -76,8 +72,7 @@ } _VALID_IDENT: Pattern[str] = re.compile(r"^[^\d\W]\w*\Z", re.ASCII) -_RE_LINK: Pattern[str] = re.compile(r"(?<=\[)([^\]]+)(?=\]\([^\)]+\))", re.MULTILINE) -_RE_SPECIAL: Pattern[str] = re.compile(r"[*_]{2,3}|`", re.MULTILINE) + _RE_LIST_MISSING_ASTERISK: Pattern[str] = re.compile(r"^-(?=[ `\"a-z])", re.MULTILINE) _RE_LIST_MISSING_WHITESPACE: Pattern[str] = re.compile(r"^\*(?=[`\"a-z])", re.MULTILINE) @@ -1083,30 +1078,6 @@ def import_typing_extensions( """ -class RSTRenderer(_RSTRenderer): - def __init__(self) -> None: - super().__init__() - - def inline_html(self, token: dict[str, Any], state: BlockState) -> str: - html = token["raw"] - return rf"\ :raw-html:`{html}`\ " - - -class RSTParse(mistune.Markdown): - def __init__( - self, - renderer: mistune.BaseRenderer, - block: mistune.BlockParser | None = None, - inline: mistune.InlineParser | None = None, - plugins=None, - ) -> 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 indent_docstring( # noqa: C901 lines: Iterable[str], indent_level: int, width: int = 100, lstrip=True ) -> str: @@ -1192,31 +1163,10 @@ def fix_docstring_issues(docstring: str) -> str: ) -def rst_syntax_for_class(class_name: str) -> str: - return f":class:`{class_name}`" - - -rst_parse: RSTParse = RSTParse(RSTRenderer()) +rst_parse: RSTParseVegaLite = RSTParseVegaLite() # TODO: Investigate `mistune.Markdown.(before|after)_render_hooks`. def process_description(description: str) -> str: """Parse a JSON encoded markdown description into an `RST` string.""" - # remove formatting from links - description = "".join( - _RE_SPECIAL.sub("", d) if i % 2 else d - for i, d in enumerate(_RE_LINK.split(description)) - ) - description = rst_parse(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() + return rst_parse(description) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index d77ba2433..677cc6547 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -27,9 +27,9 @@ import mistune import mistune.util +from tools.markup import RSTParse, Token, read_ast_tokens +from tools.markup import RSTRenderer as _RSTRenderer from tools.schemapi.schemapi import SchemaBase as _SchemaBase -from tools.schemapi.utils import RSTParse as _RSTParse -from tools.schemapi.utils import RSTRenderer as _RSTRenderer from tools.schemapi.utils import ( ruff_write_lint_format_str as _ruff_write_lint_format_str, ) @@ -38,7 +38,7 @@ import sys from re import Match, Pattern - from mistune import BlockParser, BlockState, InlineParser + from mistune import BlockState if sys.version_info >= (3, 11): from typing import LiteralString, Self, TypeAlias @@ -48,7 +48,7 @@ __all__ = ["render_expr_full", "test_parse", "write_expr_module"] -Token: TypeAlias = "dict[str, Any]" + WorkInProgress: TypeAlias = Any """Marker for a type that will not be final.""" @@ -100,6 +100,20 @@ class Source(str, enum.Enum): IGNORE_MISC = r"# type: ignore[misc]" +def _override_predicate(obj: Any, /) -> bool: + return ( + callable(obj) + and (name := obj.__name__) + and isinstance(name, str) + and not (name.startswith("_")) + ) + + +_SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( + nm for nm, _ in getmembers(_SchemaBase, _override_predicate) +) + + def download_expressions_md(url: str, /) -> Path: """Download to a temporary file, return that as a ``pathlib.Path``.""" tmp, _ = request.urlretrieve(url) @@ -114,15 +128,6 @@ def download_expressions_md(url: str, /) -> Path: return fp -def read_tokens(source: Path, /) -> list[Token]: - """ - Read from ``source``, drop ``BlockState``. - - Factored out to provide accurate typing. - """ - return mistune.create_markdown(renderer="ast").read(source)[0] - - def strip_include_tag(s: str, /) -> str: """ Removes `liquid`_ templating markup. @@ -133,18 +138,12 @@ def strip_include_tag(s: str, /) -> str: return LIQUID_INCLUDE.sub(r"", s) -def _override_predicate(obj: Any, /) -> bool: - return ( - callable(obj) - and (name := obj.__name__) - and isinstance(name, str) - and not (name.startswith("_")) - ) - - -_SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( - nm for nm, _ in getmembers(_SchemaBase, _override_predicate) -) +def expand_urls(url: str, /) -> str: + if url.startswith("#"): + url = f"{EXPRESSIONS_DOCS_URL}{url}" + else: + url = url.replace(r"../", VEGA_DOCS_URL) + return url class RSTRenderer(_RSTRenderer): @@ -160,11 +159,7 @@ def link(self, token: Token, state: BlockState) -> str: - Parameterize `"#"`, `"../"` expansion during init """ attrs = token["attrs"] - url: str = attrs["url"] - if url.startswith("#"): - url = f"{EXPRESSIONS_DOCS_URL}{url}" - else: - url = url.replace(r"../", VEGA_DOCS_URL) + url = expand_urls(attrs["url"]) text = self.render_children(token, state) text = text.replace("`", "") inline = f"`{text}`_" @@ -175,55 +170,17 @@ def text(self, token: Token, state: BlockState) -> str: text = super().text(token, state) return strip_include_tag(text) + def _with_links(self, s: str, links: dict[str, Any] | Any, /) -> str: + it = chain.from_iterable( + (f".. _{ref_name}:", f" {attrs['url']}") + for ref_name, attrs in links.items() + ) + return "\n".join(chain([s], it)) -def _iter_link_lines(ref_links: Any, /) -> Iterator[str]: - links: dict[str, Any] = ref_links - for ref_name, attrs in links.items(): - yield from (f".. _{ref_name}:", f" {attrs['url']}") - - -class RSTParse(_RSTParse): - """ - Minor extension to support partial `ast`_ conversion. - - Only need to convert the docstring tokens to `.rst`. - - NOTE - ---- - Once `PR`_ is merged, move this to the parent class and rename - - .. _ast: - https://mistune.lepture.com/en/latest/guide.html#abstract-syntax-tree - .. _PR: - https://github.com/vega/altair/pull/3536 - """ - - def __init__( - self, - renderer: RSTRenderer, - block: BlockParser | None = None, - inline: InlineParser | None = None, - plugins=None, - ) -> None: - super().__init__(renderer, block, inline, plugins) - if self.renderer is None: - msg = "Must provide a renderer, got `None`" - raise TypeError(msg) - self.renderer: RSTRenderer - - 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 - """ - state = self.block.state_cls() - result = self.renderer(self._iter_render(tokens, state), state) + def __call__(self, tokens: Iterable[Token], state: BlockState) -> str: + result = super().__call__(tokens, state) if links := state.env.get("ref_links", {}): - return "\n".join(chain([result], _iter_link_lines(links))) + return self._with_links(result, links) else: return result @@ -748,7 +705,7 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: def _parse_expressions(url: str, /) -> Iterator[VegaExprNode]: """Download, read markdown and iteratively parse into signature representations.""" - for tok in read_tokens(download_expressions_md(url)): + for tok in read_ast_tokens(download_expressions_md(url)): if ( (children := tok.get(CHILDREN)) is not None and (child := next(iter(children)).get(RAW)) is not None From 9aaf86284493afd197d8af6c10dfb475bc671874 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:00:10 +0100 Subject: [PATCH 52/77] chore: Remove debugging code https://github.com/vega/altair/pull/3600#discussion_r1775374246, https://github.com/vega/altair/pull/3600#discussion_r1775380576 --- tools/schemapi/vega_expr.py | 38 +------------------------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 677cc6547..1ed4b5c4c 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -46,7 +46,7 @@ from typing_extensions import LiteralString, Self, TypeAlias from _typeshed import SupportsKeysAndGetItem -__all__ = ["render_expr_full", "test_parse", "write_expr_module"] +__all__ = ["write_expr_module"] WorkInProgress: TypeAlias = Any @@ -798,7 +798,6 @@ def PI(cls) -> {return_ann}: return {const}("PI") ''' - EXPR_MODULE_POST = """\ _ExprType = expr # NOTE: Compatibility alias for previous type of `alt.expr`. @@ -869,15 +868,6 @@ def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} ''' -def render_expr_cls() -> WorkInProgress: - return EXPR_CLS_TEMPLATE.format( - base="_ExprRef", - metaclass=CONST_META, - doc=EXPR_CLS_DOC, - type_ignore=IGNORE_MISC, - ) - - def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: if node.is_overloaded(): body_params = STAR_ARGS[1:] @@ -893,32 +883,6 @@ def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: ) -def test_parse() -> dict[str, VegaExprNode]: - """Temporary introspection tool.""" - return {node.name: node for node in parse_expressions(Source.LIVE.value)} - - -def render_expr_full() -> str: - """Temporary sample of **pre-ruff** module.""" - it = (render_expr_method(node) for node in parse_expressions(Source.LIVE.value)) - return "\n".join( - chain( - ( - EXPR_MODULE_PRE.format( - metaclass=CONST_META, - const=CONST_WRAPPER, - return_ann=RETURN_ANNOTATION, - input_ann=INPUT_ANNOTATION, - func=RETURN_WRAPPER, - ), - render_expr_cls(), - ), - it, - [EXPR_MODULE_POST], - ) - ) - - def write_expr_module( source_url: Literal["live", "static"] | str, output: Path ) -> None: From 9215d7d31dfb82e906cf9a28eb0c27ccb7a13b0e Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:21:26 +0100 Subject: [PATCH 53/77] refactor: Replace `render_expr_method` with a method Also moved some static content to template and simplified https://github.com/vega/altair/pull/3600#discussion_r1775356969, https://github.com/vega/altair/pull/3600#discussion_r1775376732, https://github.com/vega/altair/pull/3600#discussion_r1775379947 --- tools/schemapi/vega_expr.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 1ed4b5c4c..efbf030ad 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -416,6 +416,24 @@ def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: ) raise TypeError(msg) + def render(self) -> str: + if self.is_overloaded(): + body_params = STAR_ARGS[1:] + else: + body_params = ( + f"({self.parameters[0].name},)" + if len(self.parameters) == 1 + else f"({','.join(self.parameter_names())})" + ) + return EXPR_METHOD_TEMPLATE.format( + decorator=DECORATOR, + signature=self.signature, + doc=self.doc, + return_wrapper=RETURN_WRAPPER, + name=f"{self.name!r}", + body_params=body_params, + ) + @property def title(self) -> str: """ @@ -864,25 +882,10 @@ def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} """ {doc} """ - {body} + return {return_wrapper}({name}, {body_params}) ''' -def render_expr_method(node: VegaExprNode, /) -> WorkInProgress: - if node.is_overloaded(): - body_params = STAR_ARGS[1:] - else: - body_params = ", ".join(node.parameter_names()) - if "," not in body_params: - body_params = f"({body_params}, )" - else: - body_params = f"({body_params})" - body = f"return {RETURN_WRAPPER}({node.name!r}, {body_params})" - return EXPR_METHOD_TEMPLATE.format( - decorator=DECORATOR, signature=node.signature, doc=node.doc, body=body - ) - - def write_expr_module( source_url: Literal["live", "static"] | str, output: Path ) -> None: @@ -921,7 +924,7 @@ def write_expr_module( ) contents = chain( content, - (render_expr_method(node) for node in parse_expressions(url)), + (node.render() for node in parse_expressions(url)), [EXPR_MODULE_POST], ) print(f"Generating\n {url!s}\n ->{output!s}") From 7a10e3da511c23f10800b2d143581dfa49f48c14 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:59:20 +0100 Subject: [PATCH 54/77] refactor: Add signature template, rename others The `EXPR_` prefix is meaningless when all use it --- tools/schemapi/vega_expr.py | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index efbf030ad..eaf81c12a 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -388,16 +388,20 @@ def with_signature(self) -> Self: Accessible via ``self.signature`` """ - pre_params = f"def {self.title}(cls, " - post_params = ")" if self.is_variadic() else ", /)" - post_params = f"{post_params} -> {RETURN_ANNOTATION}:" - if self.is_incompatible_override(): - post_params = f"{post_params} {IGNORE_OVERRIDE}" - if self.is_overloaded(): - param_list = VegaExprParam.star_args() - else: - param_list = ", ".join(p.to_str() for p in self.parameters) - self.signature = f"{pre_params}{param_list}{post_params}" + param_list = ( + VegaExprParam.star_args() + if self.is_overloaded() + else ", ".join(p.to_str() for p in self.parameters) + ) + self.signature = METHOD_SIGNATURE.format( + title=self.title, + param_list=param_list, + marker="" if self.is_variadic() else ", /", + return_ann=RETURN_ANNOTATION, + type_ignore=( + f" {IGNORE_OVERRIDE}" if self.is_incompatible_override() else "" + ), + ) return self def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: @@ -425,7 +429,7 @@ def render(self) -> str: if len(self.parameters) == 1 else f"({','.join(self.parameter_names())})" ) - return EXPR_METHOD_TEMPLATE.format( + return METHOD_TEMPLATE.format( decorator=DECORATOR, signature=self.signature, doc=self.doc, @@ -747,7 +751,7 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: yield node.with_doc() -EXPR_MODULE_PRE = '''\ +MODULE_PRE = '''\ """Tools for creating transform & filter expressions with a python syntax.""" from __future__ import annotations @@ -816,13 +820,13 @@ def PI(cls) -> {return_ann}: return {const}("PI") ''' -EXPR_MODULE_POST = """\ +MODULE_POST = """\ _ExprType = expr # NOTE: Compatibility alias for previous type of `alt.expr`. # `_ExprType` was not referenced in any internal imports/tests. """ -EXPR_CLS_DOC = """ +CLS_DOC = """ Utility providing *constants* and *classmethods* to construct expressions. `Expressions`_ can be used to write basic formulas that enable custom interactions. @@ -867,7 +871,7 @@ def PI(cls) -> {return_ann}: }) """ -EXPR_CLS_TEMPLATE = '''\ +CLS_TEMPLATE = '''\ class expr({base}, metaclass={metaclass}): """{doc}""" @@ -876,7 +880,11 @@ def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} return {base}(expr=expr) ''' -EXPR_METHOD_TEMPLATE = '''\ +METHOD_SIGNATURE = ( + """def {title}(cls, {param_list}{marker}) -> {return_ann}:{type_ignore}""" +) + +METHOD_TEMPLATE = '''\ {decorator} {signature} """ @@ -908,24 +916,24 @@ def write_expr_module( else: url = source_url content = ( - EXPR_MODULE_PRE.format( + MODULE_PRE.format( metaclass=CONST_META, const=CONST_WRAPPER, return_ann=RETURN_ANNOTATION, input_ann=INPUT_ANNOTATION, func=RETURN_WRAPPER, ), - EXPR_CLS_TEMPLATE.format( + CLS_TEMPLATE.format( base="_ExprRef", metaclass=CONST_META, - doc=EXPR_CLS_DOC, + doc=CLS_DOC, type_ignore=IGNORE_MISC, ), ) contents = chain( content, (node.render() for node in parse_expressions(url)), - [EXPR_MODULE_POST], + [MODULE_POST], ) print(f"Generating\n {url!s}\n ->{output!s}") _ruff_write_lint_format_str(output, contents) From e3273f6eb1e0df07cf3e892d24f5db24a1ee1454 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:02:37 +0100 Subject: [PATCH 55/77] docs: Update metaclass description https://github.com/vega/altair/pull/3600#discussion_r1775392083 --- altair/expr/__init__.py | 6 +++++- tools/schemapi/vega_expr.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index c61dddbfc..e89ef6a3f 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -18,7 +18,11 @@ class _ConstExpressionType(type): - """Metaclass providing read-only class properties for :class:`expr`.""" + """ + Metaclass for :class:`expr`. + + Currently providing read-only class properties, representing JavaScript constants. + """ @property def NaN(cls) -> Expression: diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index eaf81c12a..3b80f8948 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -89,7 +89,7 @@ class Source(str, enum.Enum): # NOTE: No benefit to annotating with the actual wrapper # - `Expression` is shorter, and has all the functionality/attributes CONST_WRAPPER = "ConstExpression" -CONST_META = "_ConstExpressionType" +CLS_META = "_ConstExpressionType" INPUT_ANNOTATION = "IntoExpression" # NOTE: `python`/`mypy` related literals @@ -772,7 +772,11 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: class {metaclass}(type): - """Metaclass providing read-only class properties for :class:`expr`.""" + """ + Metaclass for :class:`expr`. + + Currently providing read-only class properties, representing JavaScript constants. + """ @property def NaN(cls) -> {return_ann}: @@ -917,7 +921,7 @@ def write_expr_module( url = source_url content = ( MODULE_PRE.format( - metaclass=CONST_META, + metaclass=CLS_META, const=CONST_WRAPPER, return_ann=RETURN_ANNOTATION, input_ann=INPUT_ANNOTATION, @@ -925,7 +929,7 @@ def write_expr_module( ), CLS_TEMPLATE.format( base="_ExprRef", - metaclass=CONST_META, + metaclass=CLS_META, doc=CLS_DOC, type_ignore=IGNORE_MISC, ), From 1a7d2417d0a6a45c3f234c4226bbdf783105631a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:06:52 +0100 Subject: [PATCH 56/77] refactor: Rename `_ConstExpressionType` -> `_ExprMeta` --- altair/expr/__init__.py | 4 ++-- tests/expr/test_expr.py | 6 +++--- tools/schemapi/vega_expr.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index e89ef6a3f..bdaa54819 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -17,7 +17,7 @@ from altair.expr.core import Expression, IntoExpression -class _ConstExpressionType(type): +class _ExprMeta(type): """ Metaclass for :class:`expr`. @@ -70,7 +70,7 @@ def PI(cls) -> Expression: return ConstExpression("PI") -class expr(_ExprRef, metaclass=_ConstExpressionType): +class expr(_ExprRef, metaclass=_ExprMeta): """ Utility providing *constants* and *classmethods* to construct expressions. diff --git a/tests/expr/test_expr.py b/tests/expr/test_expr.py index d0523cc33..2170c1e36 100644 --- a/tests/expr/test_expr.py +++ b/tests/expr/test_expr.py @@ -9,7 +9,7 @@ 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: @@ -112,7 +112,7 @@ def test_expr_methods( 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) @@ -120,7 +120,7 @@ def test_expr_consts(constname: str): 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): diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 3b80f8948..c68b1d30e 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -89,7 +89,7 @@ class Source(str, enum.Enum): # NOTE: No benefit to annotating with the actual wrapper # - `Expression` is shorter, and has all the functionality/attributes CONST_WRAPPER = "ConstExpression" -CLS_META = "_ConstExpressionType" +CLS_META = "_ExprMeta" INPUT_ANNOTATION = "IntoExpression" # NOTE: `python`/`mypy` related literals From cfb676f8ee0b939d8e5f0e91160dbeaeae4a8e71 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:59:27 +0100 Subject: [PATCH 57/77] refactor: Align `VegaExpr(Node|Param)` apis - Both have a `.from_..()` iterator classmethod - Both output via `.render()` https://github.com/vega/altair/pull/3600#discussion_r1775371829 --- tools/schemapi/vega_expr.py | 66 +++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c68b1d30e..e5a8c9212 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -41,16 +41,12 @@ from mistune import BlockState if sys.version_info >= (3, 11): - from typing import LiteralString, Self, TypeAlias + from typing import LiteralString, Self else: - from typing_extensions import LiteralString, Self, TypeAlias + from typing_extensions import LiteralString, Self from _typeshed import SupportsKeysAndGetItem -__all__ = ["write_expr_module"] - - -WorkInProgress: TypeAlias = Any -"""Marker for a type that will not be final.""" +__all__ = ["parse_expressions", "write_expr_module"] # NOTE: Urls/fragments @@ -379,7 +375,7 @@ def with_parameters(self) -> Self: Accessible via ``self.parameters``. """ split: Iterator[str] = self._split_signature_tokens(exclude_name=True) - self.parameters = list(VegaExprParam.iter_params(split)) + self.parameters = list(VegaExprParam.from_texts(split)) return self def with_signature(self) -> Self: @@ -391,7 +387,7 @@ def with_signature(self) -> Self: param_list = ( VegaExprParam.star_args() if self.is_overloaded() - else ", ".join(p.to_str() for p in self.parameters) + else ", ".join(p.render() for p in self.parameters) ) self.signature = METHOD_SIGNATURE.format( title=self.title, @@ -685,6 +681,30 @@ def __repr__(self) -> str: ")" ) + @classmethod + def from_tokens(cls, tokens: Iterable[Token], /) -> Iterator[Self]: + """ + Lazy, filtered partial parser. + + Applies a series of filters before rendering everything but the docs. + + Parameters + ---------- + tokens + `ast tokens`_ produced by ``mistune`` + + .. _ast tokens: + https://mistune.lepture.com/en/latest/guide.html#abstract-syntax-tree + """ + for tok in tokens: + if ( + (children := tok.get(CHILDREN)) is not None + and (child := next(iter(children)).get(RAW)) is not None + and (match := FUNCTION_DEF_LINE.match(child)) + and (node := cls(match[1], children)).is_callable() + ): + yield node.with_parameters().with_signature() + @dataclasses.dataclass class VegaExprParam: @@ -696,7 +716,7 @@ class VegaExprParam: def star_args() -> LiteralString: return f"{STAR_ARGS}: Any" - def to_str(self) -> str: + def render(self) -> str: """Return as an annotated parameter, with a default if needed.""" if self.required: return f"{self.name}: {INPUT_ANNOTATION}" @@ -706,7 +726,7 @@ def to_str(self) -> str: return self.star_args() @classmethod - def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: + def from_texts(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: """Yields an ordered parameter list.""" is_required: bool = True for s in raw_texts: @@ -725,29 +745,17 @@ def iter_params(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: continue -def _parse_expressions(url: str, /) -> Iterator[VegaExprNode]: - """Download, read markdown and iteratively parse into signature representations.""" - for tok in read_ast_tokens(download_expressions_md(url)): - if ( - (children := tok.get(CHILDREN)) is not None - and (child := next(iter(children)).get(RAW)) is not None - and (match := FUNCTION_DEF_LINE.match(child)) - ): - node = VegaExprNode(match[1], children) - if node.is_callable(): - yield node.with_parameters().with_signature() - request.urlcleanup() - - def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: """ - Eagerly parse signatures of relevant definitions, then yield with docs. + Download, read markdown and eagerly parse signatures of relevant definitions. - Ensures each doc can use all remapped names, regardless of the order they appear. + Yields with docs to ensure each can use all remapped names, regardless of the order they appear. """ - eager = tuple(_parse_expressions(url)) + tokens = read_ast_tokens(download_expressions_md(url)) + expr_nodes = tuple(VegaExprNode.from_tokens(tokens)) + request.urlcleanup() VegaExprNode.remap_title.refresh() - for node in eager: + for node in expr_nodes: yield node.with_doc() From 6fd32e9fe6de2e43bd6ac7df2cfd337c2c869c66 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:26:14 +0100 Subject: [PATCH 58/77] refactor: Factor out to `italics_to_backticks` https://github.com/vega/altair/pull/3600#discussion_r1775367672 --- tools/schemapi/vega_expr.py | 40 ++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index e5a8c9212..8c831bc01 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -340,6 +340,34 @@ def __repr__(self) -> str: return f"{type(self).__name__}(\n {self._mapping!r}\n)" +def italics_to_backticks(s: str, names: Iterable[str], /) -> str: + """ + Perform a targeted replacement, considering links. + + Parameters + ---------- + s + String containing rendered `.rst`. + names + Group of names the replacement applies to. + + Notes + ----- + - Avoids adding backticks to parameter names that are also used in a link. + - All cases of these are for `unit|units`. + + Examples + -------- + >>> italics_to_backticks( + ... "some text and *name* and more text but also *other* text", + ... ("name", "other"), + ... ) + "some text and ``name`` and more text but also ``other`` text" + """ + pattern = rf"(?P[^`_])\*(?P{'|'.join(names)})\*(?P[^`])" + return re.sub(pattern, r"\g``\g``\g", s) + + class VegaExprNode: """ ``SchemaInfo``-like, but operates on `expressions.md`_. @@ -538,20 +566,14 @@ def _doc_tokens(self) -> Sequence[Token]: ) raise NotImplementedError(msg) - def _doc_post_process(self, rendered: str, /) -> str: + def _doc_post_process(self, s: str, /) -> str: """ Utilizing properties found during parsing to improve docs. Temporarily handling this here. """ - # NOTE: Avoids adding backticks to parameter names that are also used in a link - # - All cases of these are for `unit|units` - pre, post = "[^`_]", "[^`]" - pattern = ( - rf"({pre})\*({'|'.join(self.parameter_names(variadic=False))})\*({post})" - ) - highlight_params = re.sub(pattern, r"\g<1>``\g<2>``\g<3>", rendered) - with_alt_references = type(self).remap_title(highlight_params) + backtick_params = italics_to_backticks(s, self.parameter_names(variadic=False)) + with_alt_references = type(self).remap_title(backtick_params) unescaped = mistune.util.unescape(with_alt_references) numpydoc_style = _doc_fmt(unescaped) return numpydoc_style From b2aeecb10008a8d6e57cf479be3b7d0f20097a5b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:41:30 +0100 Subject: [PATCH 59/77] feat: Strip template markup earlier - Moves a replacement that previously occured in two places (rendering & signatuyre parsing) - Now the tokens returned by `read_ast_tokens` do not contain this at all https://github.com/vega/altair/pull/3600#discussion_r1777033906 --- tools/markup.py | 28 ++++++++++++++++++++++------ tools/schemapi/vega_expr.py | 17 +---------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tools/markup.py b/tools/markup.py index fc1af9cfc..c2a01c640 100644 --- a/tools/markup.py +++ b/tools/markup.py @@ -6,7 +6,8 @@ from html import unescape from typing import TYPE_CHECKING, Any, Iterable, Literal -import mistune +from mistune import InlineParser as _InlineParser +from mistune import Markdown as _Markdown from mistune.renderers.rst import RSTRenderer as _RSTRenderer if TYPE_CHECKING: @@ -19,12 +20,13 @@ from typing_extensions import TypeAlias from re import Pattern - from mistune import BaseRenderer, BlockParser, BlockState, InlineParser + 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): @@ -36,7 +38,7 @@ def inline_html(self, token: Token, state: BlockState) -> str: return rf"\ :raw-html:`{html}`\ " -class RSTParse(mistune.Markdown): +class RSTParse(_Markdown): """ Minor extension to support partial `ast`_ conversion. @@ -50,7 +52,7 @@ def __init__( self, renderer: BaseRenderer | Literal["ast"] | None, block: BlockParser | None = None, - inline: InlineParser | None = None, + inline: _InlineParser | None = None, plugins=None, ) -> None: if renderer == "ast": @@ -82,7 +84,7 @@ def __init__( self, renderer: RSTRenderer | None = None, block: BlockParser | None = None, - inline: InlineParser | None = None, + inline: _InlineParser | None = None, plugins=None, ) -> None: super().__init__(renderer or RSTRenderer(), block, inline, plugins) @@ -109,13 +111,27 @@ def __call__(self, s: str) -> str: 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 mistune.create_markdown(renderer="ast").read(source)[0] + return _Markdown(renderer=None, inline=InlineParser()).read(source)[0] def rst_syntax_for_class(class_name: str) -> str: diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 8c831bc01..c10c6c8cb 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -61,7 +61,6 @@ class Source(str, enum.Enum): # NOTE: Regex patterns FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") -LIQUID_INCLUDE: Pattern[str] = re.compile(r"( \{% include.+%\})") SENTENCE_BREAK: Pattern[str] = re.compile(r"(? Path: return fp -def strip_include_tag(s: str, /) -> str: - """ - Removes `liquid`_ templating markup. - - .. _liquid: - https://shopify.github.io/liquid/ - """ - return LIQUID_INCLUDE.sub(r"", s) - - def expand_urls(url: str, /) -> str: if url.startswith("#"): url = f"{EXPRESSIONS_DOCS_URL}{url}" @@ -162,10 +151,6 @@ def link(self, token: Token, state: BlockState) -> str: state.env["ref_links"][text] = {"url": url} return inline - def text(self, token: Token, state: BlockState) -> str: - text = super().text(token, state) - return strip_include_tag(text) - def _with_links(self, s: str, links: dict[str, Any] | Any, /) -> str: it = chain.from_iterable( (f".. _{ref_name}:", f" {attrs['url']}") @@ -518,7 +503,7 @@ def _split_signature_tokens(self, *, exclude_name: bool = False) -> Iterator[str """ EXCLUDE: set[str] = {", ", "", self.name} if exclude_name else {", ", ""} for tok in self._signature_tokens(): - clean = strip_include_tag(tok[RAW]).strip(", -") + clean = tok[RAW].strip(", -") if clean not in EXCLUDE: yield from VegaExprNode._split_markers(clean) From fde31e25835f3144515faa8bbb984dfbb7f350ea Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:35:32 +0100 Subject: [PATCH 60/77] refactor: Move `unescape` to `render_tokens` Step is unrelated to parsed attributes of a definition --- tools/markup.py | 4 +++- tools/schemapi/vega_expr.py | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tools/markup.py b/tools/markup.py index c2a01c640..d9e8230ec 100644 --- a/tools/markup.py +++ b/tools/markup.py @@ -6,6 +6,7 @@ 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 @@ -76,7 +77,8 @@ def render_tokens(self, tokens: Iterable[Token], /) -> str: msg = "Unable to render tokens without a renderer." raise TypeError(msg) state = self.block.state_cls() - return self.renderer(self._iter_render(tokens, state), state) + s = self.renderer(self._iter_render(tokens, state), state) + return mistune.util.unescape(s) class RSTParseVegaLite(RSTParse): diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c10c6c8cb..8525996a9 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -24,9 +24,6 @@ ) from urllib import request -import mistune -import mistune.util - from tools.markup import RSTParse, Token, read_ast_tokens from tools.markup import RSTRenderer as _RSTRenderer from tools.schemapi.schemapi import SchemaBase as _SchemaBase @@ -559,9 +556,7 @@ def _doc_post_process(self, s: str, /) -> str: """ backtick_params = italics_to_backticks(s, self.parameter_names(variadic=False)) with_alt_references = type(self).remap_title(backtick_params) - unescaped = mistune.util.unescape(with_alt_references) - numpydoc_style = _doc_fmt(unescaped) - return numpydoc_style + return _doc_fmt(with_alt_references) def is_callable(self) -> bool: """ From d4d9145eba5a25d4edfb4fa27cd05c993a6e0e7f Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:41:27 +0100 Subject: [PATCH 61/77] refactor: Move `_doc_post_process` -> `with_doc` This method was always intended to be temorary and is no longer helpful --- tools/schemapi/vega_expr.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 8525996a9..b0680dbca 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -375,7 +375,10 @@ def with_doc(self) -> Self: Accessible via ``self.doc`` """ - self.doc = self._doc_post_process(parser.render_tokens(self._doc_tokens())) + s: str = parser.render_tokens(self._doc_tokens()) + s = italics_to_backticks(s, self.parameter_names(variadic=False)) + s = type(self).remap_title(s) + self.doc = _doc_fmt(s) return self def with_parameters(self) -> Self: @@ -548,16 +551,6 @@ def _doc_tokens(self) -> Sequence[Token]: ) raise NotImplementedError(msg) - def _doc_post_process(self, s: str, /) -> str: - """ - Utilizing properties found during parsing to improve docs. - - Temporarily handling this here. - """ - backtick_params = italics_to_backticks(s, self.parameter_names(variadic=False)) - with_alt_references = type(self).remap_title(backtick_params) - return _doc_fmt(with_alt_references) - def is_callable(self) -> bool: """ Rough filter for excluding `constants`_. From 433611bb9016e14b06514a88d031853efc832131 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:53:33 +0100 Subject: [PATCH 62/77] refactor: Remove redundant branches in `_override_predicate` --- tools/schemapi/vega_expr.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index b0680dbca..c68cfe6b8 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -93,12 +93,7 @@ class Source(str, enum.Enum): def _override_predicate(obj: Any, /) -> bool: - return ( - callable(obj) - and (name := obj.__name__) - and isinstance(name, str) - and not (name.startswith("_")) - ) + return callable(obj) and not (name := obj.__name__).startswith("_") # noqa: F841 _SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( From c2af5f47bcf043e6734fd57c2e843bceef312f06 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:42:46 +0100 Subject: [PATCH 63/77] refactor: Assign names to literals in `_doc_fmt` Related to (but doesn't resolve) https://github.com/vega/altair/pull/3600#discussion_r1775348969 --- tools/schemapi/vega_expr.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c68cfe6b8..bc29dae62 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -75,6 +75,9 @@ class Source(str, enum.Enum): CLOSE_BRACKET: Literal["]"] = "]" INLINE_OVERLOAD: Literal[" |"] = " |" +METHOD_INDENT: LiteralString = 8 * " " +SECTION_BREAK: Literal["\n\n"] = "\n\n" + # NOTE: `altair` types (for annotations) RETURN_WRAPPER = "FunctionExpression" RETURN_ANNOTATION = "Expression" @@ -163,8 +166,8 @@ def __call__(self, tokens: Iterable[Token], state: BlockState) -> str: width=100, break_long_words=False, break_on_hyphens=False, - initial_indent=8 * " ", - subsequent_indent=8 * " ", + initial_indent=METHOD_INDENT, + subsequent_indent=METHOD_INDENT, ) @@ -180,13 +183,13 @@ def _doc_fmt(doc: str, /) -> str: summary = f"{sentences.popleft()}.\n" last_line = sentences.pop().strip() sentences = deque(f"{s}. " for s in sentences) - if "\n\n.. _" in last_line: - last_line, references = last_line.split("\n\n", maxsplit=1) + if SECTION_BREAK in last_line: + last_line, references = last_line.split(SECTION_BREAK, maxsplit=1) sentences.append(last_line) sentences = deque(text_wrap.wrap("".join(sentences))) sentences.appendleft(summary) if references: - sentences.extend(("", indent(references, 8 * " "))) + sentences.extend(("", indent(references, METHOD_INDENT))) return "\n".join(sentences) else: return sentences.pop().strip() From e7e79c967933ab9e9a7dd02527a5a34b046bcbec Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:43:48 +0100 Subject: [PATCH 64/77] docs(typing): Add missing annotations --- tools/schemapi/vega_expr.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index bc29dae62..7a4b52295 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -47,8 +47,8 @@ # NOTE: Urls/fragments -VEGA_DOCS_URL = "https://vega.github.io/vega/docs/" -EXPRESSIONS_DOCS_URL = f"{VEGA_DOCS_URL}expressions/" +VEGA_DOCS_URL: LiteralString = "https://vega.github.io/vega/docs/" +EXPRESSIONS_DOCS_URL: LiteralString = f"{VEGA_DOCS_URL}expressions/" class Source(str, enum.Enum): @@ -79,20 +79,24 @@ class Source(str, enum.Enum): SECTION_BREAK: Literal["\n\n"] = "\n\n" # NOTE: `altair` types (for annotations) -RETURN_WRAPPER = "FunctionExpression" -RETURN_ANNOTATION = "Expression" -# NOTE: No benefit to annotating with the actual wrapper -# - `Expression` is shorter, and has all the functionality/attributes -CONST_WRAPPER = "ConstExpression" -CLS_META = "_ExprMeta" -INPUT_ANNOTATION = "IntoExpression" +RETURN_WRAPPER: LiteralString = "FunctionExpression" +RETURN_ANNOTATION: LiteralString = "Expression" +""" +The annotation is intentionally *less* specific than the real type. + +``Expression`` is shorter, while preserving all the user-facing functionality +""" + +CONST_WRAPPER: LiteralString = "ConstExpression" +CLS_META: LiteralString = "_ExprMeta" +INPUT_ANNOTATION: LiteralString = "IntoExpression" # NOTE: `python`/`mypy` related literals NONE: Literal[r"None"] = r"None" STAR_ARGS: Literal["*args"] = "*args" -DECORATOR = r"@classmethod" -IGNORE_OVERRIDE = r"# type: ignore[override]" -IGNORE_MISC = r"# type: ignore[misc]" +DECORATOR: LiteralString = r"@classmethod" +IGNORE_OVERRIDE: LiteralString = r"# type: ignore[override]" +IGNORE_MISC: LiteralString = r"# type: ignore[misc]" def _override_predicate(obj: Any, /) -> bool: From e20490bf8ed83f34ecb9936aed736b3c71c8d743 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:47:44 +0100 Subject: [PATCH 65/77] refactor: Final tidy up, renaming --- tools/schemapi/vega_expr.py | 458 ++++++++++++++++++------------------ 1 file changed, 233 insertions(+), 225 deletions(-) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 7a4b52295..220243ce6 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -52,6 +52,8 @@ class Source(str, enum.Enum): + """Enumerations for ``expressions.md`` source files.""" + LIVE = "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" STATIC = "https://raw.githubusercontent.com/vega/vega/ff98519cce32b776a98d01dd982467d76fc9ee34/docs/docs/expressions.md" @@ -98,36 +100,160 @@ class Source(str, enum.Enum): IGNORE_OVERRIDE: LiteralString = r"# type: ignore[override]" IGNORE_MISC: LiteralString = r"# type: ignore[misc]" +MODULE_PRE = '''\ +"""Tools for creating transform & filter expressions with a python syntax.""" -def _override_predicate(obj: Any, /) -> bool: - return callable(obj) and not (name := obj.__name__).startswith("_") # noqa: F841 +from __future__ import annotations +import sys +from typing import Any, TYPE_CHECKING -_SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( - nm for nm, _ in getmembers(_SchemaBase, _override_predicate) +from altair.expr.core import {const}, {func} +from altair.vegalite.v5.schema.core import ExprRef as _ExprRef + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +if TYPE_CHECKING: + from altair.expr.core import {return_ann}, {input_ann} + + +class {metaclass}(type): + """ + Metaclass for :class:`expr`. + + Currently providing read-only class properties, representing JavaScript constants. + """ + + @property + def NaN(cls) -> {return_ann}: + """Not a number (same as JavaScript literal NaN).""" + return {const}("NaN") + + @property + def LN10(cls) -> {return_ann}: + """The natural log of 10 (alias to Math.LN10).""" + return {const}("LN10") + + @property + def E(cls) -> {return_ann}: + """The transcendental number e (alias to Math.E).""" + return {const}("E") + + @property + def LOG10E(cls) -> {return_ann}: + """The base 10 logarithm e (alias to Math.LOG10E).""" + return {const}("LOG10E") + + @property + def LOG2E(cls) -> {return_ann}: + """The base 2 logarithm of e (alias to Math.LOG2E).""" + return {const}("LOG2E") + + @property + def SQRT1_2(cls) -> {return_ann}: + """The square root of 0.5 (alias to Math.SQRT1_2).""" + return {const}("SQRT1_2") + + @property + def LN2(cls) -> {return_ann}: + """The natural log of 2 (alias to Math.LN2).""" + return {const}("LN2") + + @property + def SQRT2(cls) -> {return_ann}: + """The square root of 2 (alias to Math.SQRT1_2).""" + return {const}("SQRT2") + + @property + def PI(cls) -> {return_ann}: + """The transcendental number pi (alias to Math.PI).""" + return {const}("PI") +''' + +MODULE_POST = """\ +_ExprType = expr +# NOTE: Compatibility alias for previous type of `alt.expr`. +# `_ExprType` was not referenced in any internal imports/tests. +""" + +CLS_DOC = """ + Utility providing *constants* and *classmethods* to construct expressions. + + `Expressions`_ can be used to write basic formulas that enable custom interactions. + + Alternatively, an `inline expression`_ may be defined via :class:`expr()`. + + Parameters + ---------- + expr: str + A `vega expression`_ string. + + Returns + ------- + ``ExprRef`` + + .. _Expressions: + https://altair-viz.github.io/user_guide/interactions.html#expressions + .. _inline expression: + https://altair-viz.github.io/user_guide/interactions.html#inline-expressions + .. _vega expression: + https://vega.github.io/vega/docs/expressions/ + + Examples + -------- + >>> import altair as alt + + >>> bind_range = alt.binding_range(min=100, max=300, name="Slider value: ") + >>> param_width = alt.param(bind=bind_range, name="param_width") + >>> param_color = alt.param( + ... expr=alt.expr.if_(param_width < 200, "red", "black"), + ... name="param_color", + ... ) + >>> y = alt.Y("yval").axis(titleColor=param_color) + + >>> y + Y({ + axis: {'titleColor': Parameter('param_color', VariableParameter({ + expr: if((param_width < 200),'red','black'), + name: 'param_color' + }))}, + shorthand: 'yval' + }) + """ + +CLS_TEMPLATE = '''\ +class expr({base}, metaclass={metaclass}): + """{doc}""" + + @override + def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} + return {base}(expr=expr) +''' + +METHOD_SIGNATURE = ( + """def {title}(cls, {param_list}{marker}) -> {return_ann}:{type_ignore}""" ) +METHOD_TEMPLATE = '''\ + {decorator} + {signature} + """ + {doc} + """ + return {return_wrapper}({name}, {body_params}) +''' -def download_expressions_md(url: str, /) -> Path: - """Download to a temporary file, return that as a ``pathlib.Path``.""" - tmp, _ = request.urlretrieve(url) - fp = Path(tmp) - if not fp.exists(): - msg = ( - f"Expressions download failed: {fp!s}.\n\n" - f"Try manually accessing resource: {url!r}" - ) - raise FileNotFoundError(msg) - else: - return fp +def _override_predicate(obj: Any, /) -> bool: + return callable(obj) and not (name := obj.__name__).startswith("_") # noqa: F841 -def expand_urls(url: str, /) -> str: - if url.startswith("#"): - url = f"{EXPRESSIONS_DOCS_URL}{url}" - else: - url = url.replace(r"../", VEGA_DOCS_URL) - return url + +_SCHEMA_BASE_MEMBERS: frozenset[str] = frozenset( + nm for nm, _ in getmembers(_SchemaBase, _override_predicate) +) class RSTRenderer(_RSTRenderer): @@ -135,13 +261,7 @@ def __init__(self) -> None: super().__init__() def link(self, token: Token, state: BlockState) -> str: - """ - Store link url, for appending at the end of doc. - - TODO - ---- - - Parameterize `"#"`, `"../"` expansion during init - """ + """Store link url, for appending at the end of doc.""" attrs = token["attrs"] url = expand_urls(attrs["url"]) text = self.render_children(token, state) @@ -175,30 +295,6 @@ def __call__(self, tokens: Iterable[Token], state: BlockState) -> str: ) -def _doc_fmt(doc: str, /) -> str: - """ - FIXME: Currently doing too many things. - - Primarily using to exclude summary line + references from ``textwrap``. - """ - sentences: deque[str] = deque(SENTENCE_BREAK.split(doc)) - if len(sentences) > 1: - references: str = "" - summary = f"{sentences.popleft()}.\n" - last_line = sentences.pop().strip() - sentences = deque(f"{s}. " for s in sentences) - if SECTION_BREAK in last_line: - last_line, references = last_line.split(SECTION_BREAK, maxsplit=1) - sentences.append(last_line) - sentences = deque(text_wrap.wrap("".join(sentences))) - sentences.appendleft(summary) - if references: - sentences.extend(("", indent(references, METHOD_INDENT))) - return "\n".join(sentences) - else: - return sentences.pop().strip() - - class ReplaceMany: """ Perform many ``1:1`` replacements on a given text. @@ -324,35 +420,7 @@ def __repr__(self) -> str: return f"{type(self).__name__}(\n {self._mapping!r}\n)" -def italics_to_backticks(s: str, names: Iterable[str], /) -> str: - """ - Perform a targeted replacement, considering links. - - Parameters - ---------- - s - String containing rendered `.rst`. - names - Group of names the replacement applies to. - - Notes - ----- - - Avoids adding backticks to parameter names that are also used in a link. - - All cases of these are for `unit|units`. - - Examples - -------- - >>> italics_to_backticks( - ... "some text and *name* and more text but also *other* text", - ... ("name", "other"), - ... ) - "some text and ``name`` and more text but also ``other`` text" - """ - pattern = rf"(?P[^`_])\*(?P{'|'.join(names)})\*(?P[^`])" - return re.sub(pattern, r"\g``\g``\g", s) - - -class VegaExprNode: +class VegaExprDef: """ ``SchemaInfo``-like, but operates on `expressions.md`_. @@ -380,7 +448,7 @@ def with_doc(self) -> Self: s: str = parser.render_tokens(self._doc_tokens()) s = italics_to_backticks(s, self.parameter_names(variadic=False)) s = type(self).remap_title(s) - self.doc = _doc_fmt(s) + self.doc = format_doc(s) return self def with_parameters(self) -> Self: @@ -432,6 +500,7 @@ def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: raise TypeError(msg) def render(self) -> str: + """Return fully parsed method definition.""" if self.is_overloaded(): body_params = STAR_ARGS[1:] else: @@ -454,7 +523,7 @@ def title(self) -> str: """ Use for the method definition, but not when calling internally. - Updates ``VegaExprNode.remap_title`` for documentation example substitutions. + Updates ``remap_title`` class variable for documentation example substitutions. """ title = f"{self.name}_" if self.is_keyword() else self.name type(self).remap_title.update({self.name: f"alt.expr.{title}"}) @@ -507,7 +576,7 @@ def _split_signature_tokens(self, *, exclude_name: bool = False) -> Iterator[str for tok in self._signature_tokens(): clean = tok[RAW].strip(", -") if clean not in EXCLUDE: - yield from VegaExprNode._split_markers(clean) + yield from VegaExprDef._split_markers(clean) @staticmethod def _split_markers(s: str, /) -> Iterator[str]: @@ -537,7 +606,7 @@ def _split_markers(s: str, /) -> Iterator[str]: if len(s) == 1: yield s elif len(s) > 1: - yield from VegaExprNode._split_markers(s) + yield from VegaExprDef._split_markers(s) yield from end def _doc_tokens(self) -> Sequence[Token]: @@ -742,165 +811,104 @@ def from_texts(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: continue -def parse_expressions(url: str, /) -> Iterator[VegaExprNode]: - """ - Download, read markdown and eagerly parse signatures of relevant definitions. - - Yields with docs to ensure each can use all remapped names, regardless of the order they appear. - """ - tokens = read_ast_tokens(download_expressions_md(url)) - expr_nodes = tuple(VegaExprNode.from_tokens(tokens)) - request.urlcleanup() - VegaExprNode.remap_title.refresh() - for node in expr_nodes: - yield node.with_doc() - - -MODULE_PRE = '''\ -"""Tools for creating transform & filter expressions with a python syntax.""" - -from __future__ import annotations - -import sys -from typing import Any, TYPE_CHECKING - -from altair.expr.core import {const}, {func} -from altair.vegalite.v5.schema.core import ExprRef as _ExprRef - -if sys.version_info >= (3, 12): - from typing import override -else: - from typing_extensions import override +def download_expressions_md(url: str, /) -> Path: + """Download to a temporary file, return that as a ``pathlib.Path``.""" + tmp, _ = request.urlretrieve(url) + fp = Path(tmp) + if not fp.exists(): + msg = ( + f"Expressions download failed: {fp!s}.\n\n" + f"Try manually accessing resource: {url!r}" + ) + raise FileNotFoundError(msg) + else: + return fp -if TYPE_CHECKING: - from altair.expr.core import {return_ann}, {input_ann} +def expand_urls(url: str, /) -> str: + if url.startswith("#"): + url = f"{EXPRESSIONS_DOCS_URL}{url}" + else: + url = url.replace(r"../", VEGA_DOCS_URL) + return url -class {metaclass}(type): - """ - Metaclass for :class:`expr`. - Currently providing read-only class properties, representing JavaScript constants. +def format_doc(doc: str, /) -> str: """ + Format rendered docstring content. - @property - def NaN(cls) -> {return_ann}: - """Not a number (same as JavaScript literal NaN).""" - return {const}("NaN") - - @property - def LN10(cls) -> {return_ann}: - """The natural log of 10 (alias to Math.LN10).""" - return {const}("LN10") - - @property - def E(cls) -> {return_ann}: - """The transcendental number e (alias to Math.E).""" - return {const}("E") - - @property - def LOG10E(cls) -> {return_ann}: - """The base 10 logarithm e (alias to Math.LOG10E).""" - return {const}("LOG10E") - - @property - def LOG2E(cls) -> {return_ann}: - """The base 2 logarithm of e (alias to Math.LOG2E).""" - return {const}("LOG2E") - - @property - def SQRT1_2(cls) -> {return_ann}: - """The square root of 0.5 (alias to Math.SQRT1_2).""" - return {const}("SQRT1_2") - - @property - def LN2(cls) -> {return_ann}: - """The natural log of 2 (alias to Math.LN2).""" - return {const}("LN2") + Primarily used to prevent wrapping on `summary line`_ and references. - @property - def SQRT2(cls) -> {return_ann}: - """The square root of 2 (alias to Math.SQRT1_2).""" - return {const}("SQRT2") - - @property - def PI(cls) -> {return_ann}: - """The transcendental number pi (alias to Math.PI).""" - return {const}("PI") -''' - -MODULE_POST = """\ -_ExprType = expr -# NOTE: Compatibility alias for previous type of `alt.expr`. -# `_ExprType` was not referenced in any internal imports/tests. -""" - -CLS_DOC = """ - Utility providing *constants* and *classmethods* to construct expressions. + Notes + ----- + - Source is very different to `vega-lite` + - There are no real sections, so these are created here + - Single line docs are unchanged + - Multi-line have everything following the first line wrappped. + - With a double break inserted for a summary line + - Reference-like links section (if present) are also ommitted from wrapping + + .. _summary line: + https://numpydoc.readthedocs.io/en/latest/format.html#short-summary + """ + sentences: deque[str] = deque(SENTENCE_BREAK.split(doc)) + if len(sentences) > 1: + references: str = "" + summary = f"{sentences.popleft()}.\n" + last_line = sentences.pop().strip() + sentences = deque(f"{s}. " for s in sentences) + if SECTION_BREAK in last_line: + last_line, references = last_line.split(SECTION_BREAK, maxsplit=1) + sentences.append(last_line) + sentences = deque(text_wrap.wrap("".join(sentences))) + sentences.appendleft(summary) + if references: + sentences.extend(("", indent(references, METHOD_INDENT))) + return "\n".join(sentences) + else: + return sentences.pop().strip() - `Expressions`_ can be used to write basic formulas that enable custom interactions. - Alternatively, an `inline expression`_ may be defined via :class:`expr()`. +def italics_to_backticks(s: str, names: Iterable[str], /) -> str: + """ + Perform a targeted replacement, considering links. Parameters ---------- - expr: str - A `vega expression`_ string. - - Returns - ------- - ``ExprRef`` + s + String containing rendered `.rst`. + names + Group of names the replacement applies to. - .. _Expressions: - https://altair-viz.github.io/user_guide/interactions.html#expressions - .. _inline expression: - https://altair-viz.github.io/user_guide/interactions.html#inline-expressions - .. _vega expression: - https://vega.github.io/vega/docs/expressions/ + Notes + ----- + - Avoids adding backticks to parameter names that are also used in a link. + - All cases of these are for `unit|units`. Examples -------- - >>> import altair as alt - - >>> bind_range = alt.binding_range(min=100, max=300, name="Slider value: ") - >>> param_width = alt.param(bind=bind_range, name="param_width") - >>> param_color = alt.param( - ... expr=alt.expr.if_(param_width < 200, "red", "black"), - ... name="param_color", + >>> italics_to_backticks( + ... "some text and *name* and more text but also *other* text", + ... ("name", "other"), ... ) - >>> y = alt.Y("yval").axis(titleColor=param_color) - - >>> y - Y({ - axis: {'titleColor': Parameter('param_color', VariableParameter({ - expr: if((param_width < 200),'red','black'), - name: 'param_color' - }))}, - shorthand: 'yval' - }) + "some text and ``name`` and more text but also ``other`` text" """ + pattern = rf"(?P[^`_])\*(?P{'|'.join(names)})\*(?P[^`])" + return re.sub(pattern, r"\g``\g``\g", s) -CLS_TEMPLATE = '''\ -class expr({base}, metaclass={metaclass}): - """{doc}""" - - @override - def __new__(cls: type[{base}], expr: str) -> {base}: {type_ignore} - return {base}(expr=expr) -''' -METHOD_SIGNATURE = ( - """def {title}(cls, {param_list}{marker}) -> {return_ann}:{type_ignore}""" -) +def parse_expressions(url: str, /) -> Iterator[VegaExprDef]: + """ + Download, read markdown and eagerly parse signatures of relevant definitions. -METHOD_TEMPLATE = '''\ - {decorator} - {signature} - """ - {doc} - """ - return {return_wrapper}({name}, {body_params}) -''' + Yields with docs to ensure each can use all remapped names, regardless of the order they appear. + """ + tokens = read_ast_tokens(download_expressions_md(url)) + expr_defs = tuple(VegaExprDef.from_tokens(tokens)) + request.urlcleanup() + VegaExprDef.remap_title.refresh() + for expr_def in expr_defs: + yield expr_def.with_doc() def write_expr_module( @@ -941,7 +949,7 @@ def write_expr_module( ) contents = chain( content, - (node.render() for node in parse_expressions(url)), + (expr_def.render() for expr_def in parse_expressions(url)), [MODULE_POST], ) print(f"Generating\n {url!s}\n ->{output!s}") From 5929077be3a93570729dd13d1ef7b4a3d4d66676 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:18:49 +0100 Subject: [PATCH 66/77] fix: Resolve false negatives for `is_callable` There were 10+ cases of methods not being defined. These are now all present https://github.com/vega/altair/pull/3600#discussion_r1778416377 --- altair/expr/__init__.py | 242 ++++++++++++++++++++++++++++++++++++ tools/schemapi/vega_expr.py | 59 +++++++-- 2 files changed, 291 insertions(+), 10 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index bdaa54819..0dd0a8a37 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -435,6 +435,18 @@ def pow(cls, value: IntoExpression, exponent: IntoExpression, /) -> Expression: """ return FunctionExpression("pow", (value, exponent)) + @classmethod + def random(cls) -> Expression: + """ + Returns a pseudo-random number in the range [0,1). + + Same as JavaScript's `Math.random`_. + + .. _Math.random: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + """ + return FunctionExpression("random", ()) + @classmethod def round(cls, value: IntoExpression, /) -> Expression: """ @@ -497,6 +509,60 @@ def sampleNormal( """ return FunctionExpression("sampleNormal", (mean, stdev)) + @classmethod + def cumulativeNormal( + cls, + value: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `cumulative distribution function`_ at the given input domain ``value`` for a normal distribution with specified ``mean`` and standard deviation ``stdev``. + + If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("cumulativeNormal", (value, mean, stdev)) + + @classmethod + def densityNormal( + cls, + value: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `probability density function`_ at the given input domain ``value``, for a normal distribution with specified ``mean`` and standard deviation ``stdev``. + + If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. + + .. _probability density function: + https://en.wikipedia.org/wiki/Probability_density_function + """ + return FunctionExpression("densityNormal", (value, mean, stdev)) + + @classmethod + def quantileNormal( + cls, + probability: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the quantile value (the inverse of the `cumulative distribution function`_) for the given input ``probability``, for a normal distribution with specified ``mean`` and standard deviation ``stdev``. + + If unspecified, the mean defaults to ``0`` and the standard deviation defaults to ``1``. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("quantileNormal", (probability, mean, stdev)) + @classmethod def sampleLogNormal( cls, mean: IntoExpression = None, stdev: IntoExpression = None, / @@ -512,6 +578,63 @@ def sampleLogNormal( """ return FunctionExpression("sampleLogNormal", (mean, stdev)) + @classmethod + def cumulativeLogNormal( + cls, + value: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `cumulative distribution function`_ at the given input domain ``value`` for a log-normal distribution with specified log ``mean`` and log standard deviation ``stdev``. + + If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to + ``1``. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("cumulativeLogNormal", (value, mean, stdev)) + + @classmethod + def densityLogNormal( + cls, + value: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `probability density function`_ at the given input domain ``value``, for a log-normal distribution with specified log ``mean`` and log standard deviation ``stdev``. + + If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to + ``1``. + + .. _probability density function: + https://en.wikipedia.org/wiki/Probability_density_function + """ + return FunctionExpression("densityLogNormal", (value, mean, stdev)) + + @classmethod + def quantileLogNormal( + cls, + probability: IntoExpression, + mean: IntoExpression = None, + stdev: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the quantile value (the inverse of the `cumulative distribution function`_) for the given input ``probability``, for a log-normal distribution with specified log ``mean`` and log standard deviation ``stdev``. + + If unspecified, the log mean defaults to ``0`` and the log standard deviation defaults to + ``1``. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("quantileLogNormal", (probability, mean, stdev)) + @classmethod def sampleUniform( cls, min: IntoExpression = None, max: IntoExpression = None, / @@ -527,6 +650,68 @@ def sampleUniform( """ return FunctionExpression("sampleUniform", (min, max)) + @classmethod + def cumulativeUniform( + cls, + value: IntoExpression, + min: IntoExpression = None, + max: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `cumulative distribution function`_ at the given input domain ``value`` for a uniform distribution over the interval [``min``, ``max``). + + If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one + argument is provided, it is interpreted as the ``max`` value. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("cumulativeUniform", (value, min, max)) + + @classmethod + def densityUniform( + cls, + value: IntoExpression, + min: IntoExpression = None, + max: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the value of the `probability density function`_ at the given input domain ``value``, for a uniform distribution over the interval [``min``, ``max``). + + If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one + argument is provided, it is interpreted as the ``max`` value. + + .. _probability density function: + https://en.wikipedia.org/wiki/Probability_density_function + """ + return FunctionExpression("densityUniform", (value, min, max)) + + @classmethod + def quantileUniform( + cls, + probability: IntoExpression, + min: IntoExpression = None, + max: IntoExpression = None, + /, + ) -> Expression: + """ + Returns the quantile value (the inverse of the `cumulative distribution function`_) for the given input ``probability``, for a uniform distribution over the interval [``min``, ``max``). + + If unspecified, ``min`` defaults to ``0`` and ``max`` defaults to ``1``. If only one + argument is provided, it is interpreted as the ``max`` value. + + .. _cumulative distribution function: + https://en.wikipedia.org/wiki/Cumulative_distribution_function + """ + return FunctionExpression("quantileUniform", (probability, min, max)) + + @classmethod + def now(cls) -> Expression: + """Returns the timestamp for the current time.""" + return FunctionExpression("now", ()) + @classmethod def datetime( cls, @@ -1234,6 +1419,39 @@ def hcl(cls, *args: Any) -> Expression: """ return FunctionExpression("hcl", args) + @classmethod + def luminance(cls, specifier: IntoExpression, /) -> Expression: + """ + Returns the luminance for the given color ``specifier`` (compatible with `d3-color's rgb function`_). + + The luminance is calculated according to the `W3C Web Content Accessibility Guidelines`_. + + .. _d3-color's rgb function: + https://github.com/d3/d3-color#rgb + .. _W3C Web Content Accessibility Guidelines: + https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + """ + return FunctionExpression("luminance", (specifier,)) + + @classmethod + def contrast( + cls, specifier1: IntoExpression, specifier2: IntoExpression, / + ) -> Expression: + """ + Returns the contrast ratio between the input color specifiers as a float between 1 and 21. + + The contrast is calculated according to the `W3C Web Content Accessibility Guidelines`_. + + .. _W3C Web Content Accessibility Guidelines: + https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + """ + return FunctionExpression("contrast", (specifier1, specifier2)) + + @classmethod + def item(cls) -> Expression: + """Returns the current scenegraph item that is the target of the event.""" + return FunctionExpression("item", ()) + @classmethod def group(cls, name: IntoExpression = None, /) -> Expression: """ @@ -1638,6 +1856,30 @@ def treeAncestors(cls, name: IntoExpression, node: IntoExpression, /) -> Express """For the hierarchy data set with the given ``name``, returns the array of ancestors nodes, starting with the input ``node``, then followed by each parent up to the root.""" return FunctionExpression("treeAncestors", (name, node)) + @classmethod + def containerSize(cls) -> Expression: + """ + Returns the current CSS box size (``[el.clientWidth, el.clientHeight]``) of the parent DOM element that contains the Vega view. + + If there is no container element, returns ``[undefined, undefined]``. + """ + return FunctionExpression("containerSize", ()) + + @classmethod + def screen(cls) -> Expression: + """ + Returns the `window.screen`_ object, or ``{}`` if Vega is not running in a browser environment. + + .. _window.screen: + https://developer.mozilla.org/en-US/docs/Web/API/Window/screen + """ + return FunctionExpression("screen", ()) + + @classmethod + def windowSize(cls) -> Expression: + """Returns the current window size (``[window.innerWidth, window.innerHeight]``) or ``[undefined, undefined]`` if Vega is not running in a browser environment.""" + return FunctionExpression("windowSize", ()) + @classmethod def warn( cls, value1: IntoExpression, value2: IntoExpression = None, *args: Any diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 220243ce6..c3bd99c35 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -59,7 +59,9 @@ class Source(str, enum.Enum): # NOTE: Regex patterns -FUNCTION_DEF_LINE: Pattern[str] = re.compile(r"") +FUNCTION_DEF_LINE: Pattern[str] = re.compile( + r".+)\" href=\"#(.+)\">" +) SENTENCE_BREAK: Pattern[str] = re.compile(r"(? {base}: {type_ignore} ''' METHOD_SIGNATURE = ( - """def {title}(cls, {param_list}{marker}) -> {return_ann}:{type_ignore}""" + """def {title}(cls{sep}{param_list}{marker}) -> {return_ann}:{type_ignore}""" ) METHOD_TEMPLATE = '''\ @@ -420,6 +422,16 @@ def __repr__(self) -> str: return f"{type(self).__name__}(\n {self._mapping!r}\n)" +class Special(enum.Enum): + """ + Special-case identifiers. + + Representing ``VegaExprDef`` states that may be otherwise ambiguous. + """ + + NO_PARAMETERS = enum.auto() + + class VegaExprDef: """ ``SchemaInfo``-like, but operates on `expressions.md`_. @@ -438,6 +450,7 @@ def __init__(self, name: str, children: Sequence[Token], /) -> None: self.parameters: list[VegaExprParam] = [] self.doc: str = "" self.signature: str = "" + self._special: set[Special] = set() def with_doc(self) -> Self: """ @@ -459,6 +472,8 @@ def with_parameters(self) -> Self: """ split: Iterator[str] = self._split_signature_tokens(exclude_name=True) self.parameters = list(VegaExprParam.from_texts(split)) + if not self.parameters: + self._special.add(Special.NO_PARAMETERS) return self def with_signature(self) -> Self: @@ -474,8 +489,9 @@ def with_signature(self) -> Self: ) self.signature = METHOD_SIGNATURE.format( title=self.title, + sep="" if self.is_no_parameters() else ",", param_list=param_list, - marker="" if self.is_variadic() else ", /", + marker="" if (self.is_variadic() or self.is_no_parameters()) else ", /", return_ann=RETURN_ANNOTATION, type_ignore=( f" {IGNORE_OVERRIDE}" if self.is_incompatible_override() else "" @@ -492,10 +508,13 @@ def parameter_names(self, *, variadic: bool = True) -> Iterator[str]: else (p.name for p in self.parameters if not p.variadic) ) yield from it + elif self.is_no_parameters(): + yield from () else: msg = ( f"Cannot provide `parameter_names` until they have been initialized via:\n" - f"{type(self).__name__}.with_parameters()" + f"{type(self).__name__}.with_parameters()\n\n" + f"{self!r}" ) raise TypeError(msg) @@ -572,10 +591,18 @@ def _split_signature_tokens(self, *, exclude_name: bool = False) -> Iterator[str ['(', '[', 'start', ']', 'stop', '[', 'step', ']', ')'] """ - EXCLUDE: set[str] = {", ", "", self.name} if exclude_name else {", ", ""} - for tok in self._signature_tokens(): - clean = tok[RAW].strip(", -") - if clean not in EXCLUDE: + EXCLUDE_INNER: set[str] = {self.name} if exclude_name else set() + EXCLUDE: set[str] = {", "} | EXCLUDE_INNER + for token in self._signature_tokens(): + raw: str = token[RAW] + if raw == OPEN_PAREN: + yield raw + elif raw.startswith(OPEN_PAREN): + yield raw[0] + for s in raw[1:].split(","): + if (clean := s.strip(" -")) not in EXCLUDE_INNER: + yield from VegaExprDef._split_markers(clean) + elif (clean := raw.strip(", -")) not in EXCLUDE: yield from VegaExprDef._split_markers(clean) @staticmethod @@ -650,7 +677,7 @@ def is_callable(self) -> bool: if current != self.name: self.name = current next(it) - return next(it).get(RAW, "") == OPEN_PAREN + return next(it).get(RAW, "").startswith(OPEN_PAREN) def is_bound_variable_name(self) -> bool: """ @@ -728,6 +755,18 @@ def is_variadic(self) -> bool: """Position-only parameter separator `"/"` not allowed after `"*"` parameter.""" return self.is_overloaded() or any(p.variadic for p in self.parameters) + def is_no_parameters(self) -> bool: + """ + Signature has been parsed for parameters, but none were present. + + For example the definition for `now`_ would **only** return ``True`` + after calling ``self.with_parameters()``. + + .. _now: + https://vega.github.io/vega/docs/expressions/#now + """ + return bool(self._special) and Special.NO_PARAMETERS in self._special + def __iter__(self) -> Iterator[Token]: yield from self._children @@ -767,7 +806,7 @@ def from_tokens(cls, tokens: Iterable[Token], /) -> Iterator[Self]: (children := tok.get(CHILDREN)) is not None and (child := next(iter(children)).get(RAW)) is not None and (match := FUNCTION_DEF_LINE.match(child)) - and (node := cls(match[1], children)).is_callable() + and (node := cls(match["name"], children)).is_callable() ): yield node.with_parameters().with_signature() From 2f1d1994486e23de4e345042de7e96c8543dc384 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:37:45 +0100 Subject: [PATCH 67/77] fix: Correct indent for `expr.screen` reference https://github.com/vega/altair/pull/3600#discussion_r1778727868 --- altair/expr/__init__.py | 2 +- tools/schemapi/vega_expr.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index 0dd0a8a37..b8185e49e 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -1871,7 +1871,7 @@ def screen(cls) -> Expression: Returns the `window.screen`_ object, or ``{}`` if Vega is not running in a browser environment. .. _window.screen: - https://developer.mozilla.org/en-US/docs/Web/API/Window/screen + https://developer.mozilla.org/en-US/docs/Web/API/Window/screen """ return FunctionExpression("screen", ()) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index c3bd99c35..a7a230e77 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -904,6 +904,10 @@ def format_doc(doc: str, /) -> str: if references: sentences.extend(("", indent(references, METHOD_INDENT))) return "\n".join(sentences) + elif SECTION_BREAK in doc: + # NOTE: 2 cases have a single line with a reference + summary, references = doc.split(SECTION_BREAK, maxsplit=1) + return "\n".join((summary, "", indent(references, METHOD_INDENT))) else: return sentences.pop().strip() From 01e61e33a909417aacc89311f748e8ac3ed5e15b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:40:36 +0100 Subject: [PATCH 68/77] feat: Bump source to add `alt.expr.sort` See https://github.com/vega/vega/pull/3973 --- altair/expr/__init__.py | 10 ++++++++++ tools/schemapi/vega_expr.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index b8185e49e..c3589be42 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -1063,6 +1063,16 @@ def slice( """ return FunctionExpression("slice", (array, start, end)) + @classmethod + def sort(cls, array: IntoExpression, /) -> Expression: + """ + Sorts the array in natural order using `ascending from Vega Utils`_. + + .. _ascending from Vega Utils: + https://vega.github.io/vega/docs/api/util/#ascending + """ + return FunctionExpression("sort", (array,)) + @classmethod def span(cls, array: IntoExpression, /) -> Expression: """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index a7a230e77..e125a0086 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -55,7 +55,8 @@ class Source(str, enum.Enum): """Enumerations for ``expressions.md`` source files.""" LIVE = "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" - STATIC = "https://raw.githubusercontent.com/vega/vega/ff98519cce32b776a98d01dd982467d76fc9ee34/docs/docs/expressions.md" + STATIC = "https://raw.githubusercontent.com/vega/vega/fb2e60274071033b4c427410ef43375b6f314cf2/docs/docs/expressions.md" + OLD = "https://raw.githubusercontent.com/vega/vega/ff98519cce32b776a98d01dd982467d76fc9ee34/docs/docs/expressions.md" # NOTE: Regex patterns From d75792b15a029f1a1628728ef3403aadc30ab065 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:58:06 +0100 Subject: [PATCH 69/77] fix: Pin to `vega` release instead of commit hash https://github.com/vega/altair/pull/3600#issuecomment-2380711841, https://github.com/vega/altair/pull/3600#discussion_r1779673096 --- altair/expr/__init__.py | 10 ---------- tools/generate_schema_wrapper.py | 3 ++- tools/schemapi/vega_expr.py | 26 +++++--------------------- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index c3589be42..b8185e49e 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -1063,16 +1063,6 @@ def slice( """ return FunctionExpression("slice", (array, start, end)) - @classmethod - def sort(cls, array: IntoExpression, /) -> Expression: - """ - Sorts the array in natural order using `ascending from Vega Utils`_. - - .. _ascending from Vega Utils: - https://vega.github.io/vega/docs/api/util/#ascending - """ - return FunctionExpression("sort", (array,)) - @classmethod def span(cls, array: IntoExpression, /) -> Expression: """Returns the span of ``array``: the difference between the last and first elements, or *array[array.length-1] - array[0]*.""" diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 09ce43770..623d3cd82 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -48,6 +48,7 @@ from tools.schemapi.codegen import ArgInfo, AttrGetter SCHEMA_VERSION: Final = "v5.20.1" +VEGA_VERSION: Final = "v5.30.0" HEADER_COMMENT = """\ @@ -1210,7 +1211,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) + write_expr_module(VEGA_VERSION, 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 diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index e125a0086..71a8a2da5 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -49,14 +49,7 @@ # NOTE: Urls/fragments VEGA_DOCS_URL: LiteralString = "https://vega.github.io/vega/docs/" EXPRESSIONS_DOCS_URL: LiteralString = f"{VEGA_DOCS_URL}expressions/" - - -class Source(str, enum.Enum): - """Enumerations for ``expressions.md`` source files.""" - - LIVE = "https://raw.githubusercontent.com/vega/vega/main/docs/docs/expressions.md" - STATIC = "https://raw.githubusercontent.com/vega/vega/fb2e60274071033b4c427410ef43375b6f314cf2/docs/docs/expressions.md" - OLD = "https://raw.githubusercontent.com/vega/vega/ff98519cce32b776a98d01dd982467d76fc9ee34/docs/docs/expressions.md" +EXPRESSIONS_URL_TEMPLATE = "https://raw.githubusercontent.com/vega/vega/refs/tags/{version}/docs/docs/expressions.md" # NOTE: Regex patterns @@ -955,27 +948,18 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprDef]: yield expr_def.with_doc() -def write_expr_module( - source_url: Literal["live", "static"] | str, output: Path -) -> None: +def write_expr_module(version: str, output: Path) -> None: """ Parse an ``expressions.md`` into a ``.py`` module. Parameters ---------- - source_url - - ``"live"``: current version - - ``"static"``: most recent version available during testing - - Or provide an alternative as a ``str`` + version + Vega release version, e.g. ``"v5.30.0"``. output Target path to write to. """ - if source_url == "live": - url = Source.LIVE.value - elif source_url == "static": - url = Source.STATIC.value - else: - url = source_url + url = EXPRESSIONS_URL_TEMPLATE.format(version=version) content = ( MODULE_PRE.format( metaclass=CLS_META, From 968114e5133ac4553727d6f39021b06c48fbe0fb Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:36:46 +0100 Subject: [PATCH 70/77] docs: Include header comment --- altair/expr/__init__.py | 3 +++ tools/generate_schema_wrapper.py | 6 +++++- tools/schemapi/vega_expr.py | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py index b8185e49e..38d87f4c5 100644 --- a/altair/expr/__init__.py +++ b/altair/expr/__init__.py @@ -1,3 +1,6 @@ +# The contents of this file are automatically written by +# tools/generate_schema_wrapper.py. Do not modify directly. + """Tools for creating transform & filter expressions with a python syntax.""" from __future__ import annotations diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 623d3cd82..5185dd174 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -1211,7 +1211,11 @@ def main() -> None: args = parser.parse_args() copy_schemapi_util() vegalite_main(args.skip_download) - write_expr_module(VEGA_VERSION, output=EXPR_FILE) + write_expr_module( + 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 diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 71a8a2da5..714dd886b 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -97,6 +97,7 @@ IGNORE_MISC: LiteralString = r"# type: ignore[misc]" MODULE_PRE = '''\ +{header} """Tools for creating transform & filter expressions with a python syntax.""" from __future__ import annotations @@ -948,7 +949,7 @@ def parse_expressions(url: str, /) -> Iterator[VegaExprDef]: yield expr_def.with_doc() -def write_expr_module(version: str, output: Path) -> None: +def write_expr_module(version: str, output: Path, *, header: str) -> None: """ Parse an ``expressions.md`` into a ``.py`` module. @@ -962,6 +963,7 @@ def write_expr_module(version: str, output: Path) -> None: url = EXPRESSIONS_URL_TEMPLATE.format(version=version) content = ( MODULE_PRE.format( + header=header, metaclass=CLS_META, const=CONST_WRAPPER, return_ann=RETURN_ANNOTATION, From 061b066e6a652d53e8cd94301439294c56338463 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:45:13 +0100 Subject: [PATCH 71/77] build(DRAFT): Add `vegalite_to_vega_version` Not fully convinced this is a reliable alterntive to manually maintaining https://github.com/vega/altair/pull/3600#discussion_r1779718475 --- tools/generate_schema_wrapper.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 5185dd174..e490d5a19 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -61,6 +61,9 @@ """ 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 = ( @@ -471,6 +474,15 @@ def schema_url(version: str = SCHEMA_VERSION) -> str: return SCHEMA_URL_TEMPLATE.format(library="vega-lite", version=version) +def vegalite_to_vega_version(vl_version: str, /) -> str: + """Return the minimum supported ``vega`` release for a ``vega-lite`` version.""" + with request.urlopen(VL_PACKAGE_TEMPLATE.format(version=vl_version)) as response: + package_json = json.load(response) + + version_spec = package_json["peerDependencies"]["vega"] + return f"v{version_spec.lstrip('^~')}" + + def download_schemafile( version: str, schemapath: Path, skip_download: bool = False ) -> Path: From d95a5735c0ae784361529d7d50d558eee5dca7c3 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:45:46 +0100 Subject: [PATCH 72/77] feat: Adds `vl_convert_to_vega_version`, remove `vegalite_to_vega_version` - Found this to be a much more reliable source - Won't require manual syncing - Can easily be replaced in the future with a public function/attribute accessible in `vl_convert` itself https://github.com/vega/altair/pull/3600#discussion_r1779748538 --- tools/generate_schema_wrapper.py | 79 +++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index e490d5a19..4ee1aac89 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -5,6 +5,7 @@ import argparse import copy import json +import re import sys import textwrap from collections import deque @@ -12,9 +13,11 @@ from itertools import chain from operator import attrgetter from pathlib import Path -from typing import TYPE_CHECKING, Any, Final, Iterable, Iterator, Literal +from typing import TYPE_CHECKING, Any, Final, Iterable, Iterator, Literal, cast from urllib import request +from packaging.version import Version + import vl_convert as vlc sys.path.insert(0, str(Path.cwd())) @@ -45,10 +48,73 @@ ) if TYPE_CHECKING: + from http.client import HTTPResponse + from tools.schemapi.codegen import ArgInfo, AttrGetter + +def vl_convert_to_vega_version( + *, + version: str | None = None, + url_fmt: str = "https://raw.githubusercontent.com/vega/vl-convert/refs/tags/vl-convert-vendor%40{0}/vl-convert-vendor/src/main.rs", + prefix: bytes = b"const VEGA_PATH", + pattern: str = r".+\/pin\/vega@(?Pv\d\.\d{1,3}\.\d{1,3})", +) -> str: + """ + Return the minimum supported ``vega`` release for a ``vl_convert`` version. + + Parameters + ---------- + version + A target `vl_convert` release. + Defaults to currently installed version. + url_fmt + Format string for source file. + prefix + Byte string prefix of target line. + pattern + Matches and extracts vega version, e.g. `"v5.30.0"`. + + Examples + -------- + >>> vl_convert_to_vega_version(version="0.2.0") + 'v5.22.1' + >>> vl_convert_to_vega_version(version="1.0.0") + 'v5.25.0' + >>> vl_convert_to_vega_version(version="1.3.0") + 'v5.28.0' + >>> vl_convert_to_vega_version(version="1.6.1") + 'v5.30.0' + """ + MIN_VL_CONVERT = "0.2.0" + vlc_version: str = version or vlc.__version__ # pyright: ignore[reportAttributeAccessIssue] + if Version(vlc_version) < Version(MIN_VL_CONVERT): + msg = ( + f"Operation requires `vl_convert>={MIN_VL_CONVERT!r}`\n" + f"but got: {version!r}" + ) + raise NotImplementedError(msg) + with request.urlopen(url_fmt.format(vlc_version)) as response: + response = cast("HTTPResponse", response) + line = response.readline() + while not line.startswith(prefix): + line = response.readline() + if line.startswith(b"fn main"): + msg = f"Failed to find {prefix!r} in {url_fmt.format(vlc_version)}." + raise ValueError(msg) + src_line = line.decode() + if match := re.match(pattern, src_line): + return match.group("vega_version") + else: + msg = ( + f"Failed to match a vega version specifier.\n" + f"{src_line=}\n{pattern=}\n{match=}\n" + ) + raise NotImplementedError(msg) + + SCHEMA_VERSION: Final = "v5.20.1" -VEGA_VERSION: Final = "v5.30.0" +VEGA_VERSION: Final[str] = vl_convert_to_vega_version() HEADER_COMMENT = """\ @@ -474,15 +540,6 @@ def schema_url(version: str = SCHEMA_VERSION) -> str: return SCHEMA_URL_TEMPLATE.format(library="vega-lite", version=version) -def vegalite_to_vega_version(vl_version: str, /) -> str: - """Return the minimum supported ``vega`` release for a ``vega-lite`` version.""" - with request.urlopen(VL_PACKAGE_TEMPLATE.format(version=vl_version)) as response: - package_json = json.load(response) - - version_spec = package_json["peerDependencies"]["vega"] - return f"v{version_spec.lstrip('^~')}" - - def download_schemafile( version: str, schemapath: Path, skip_download: bool = False ) -> Path: From b0f1164f985d33fe8889aa1a4f96a813606d7a7d Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:57:36 +0100 Subject: [PATCH 73/77] refactor: Use `vlc.get_vega_version()` https://github.com/vega/altair/pull/3633, https://github.com/vega/vl-convert/issues/191 --- tools/generate_schema_wrapper.py | 3 +-- tools/schemapi/vega_expr.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index b25e51b34..5c5f25b07 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -115,7 +115,6 @@ def vl_convert_to_vega_version( SCHEMA_VERSION: Final = "v5.20.1" -VEGA_VERSION: Final[str] = vl_convert_to_vega_version() HEADER_COMMENT = """\ @@ -1282,7 +1281,7 @@ def main() -> None: copy_schemapi_util() vegalite_main(args.skip_download) write_expr_module( - VEGA_VERSION, + vlc.get_vega_version(), output=EXPR_FILE, header=HEADER_COMMENT, ) diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index 714dd886b..ce12660f1 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -960,6 +960,7 @@ def write_expr_module(version: str, output: Path, *, header: str) -> None: output Target path to write to. """ + version = version if version.startswith("v") else f"v{version}" url = EXPRESSIONS_URL_TEMPLATE.format(version=version) content = ( MODULE_PRE.format( From 5a081891822114e0b7c48e1503f44b2183b49255 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:59:59 +0100 Subject: [PATCH 74/77] revert: Remove `vl_convert_to_vega_version` https://github.com/vega/altair/pull/3600/commits/d95a5735c0ae784361529d7d50d558eee5dca7c3 --- tools/generate_schema_wrapper.py | 67 +------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 5c5f25b07..22dbede8d 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -5,7 +5,6 @@ import argparse import copy import json -import re import sys import textwrap from collections import deque @@ -13,11 +12,9 @@ from itertools import chain from operator import attrgetter from pathlib import Path -from typing import TYPE_CHECKING, Any, Final, Iterable, Iterator, Literal, cast +from typing import TYPE_CHECKING, Any, Final, Iterable, Iterator, Literal from urllib import request -from packaging.version import Version - import vl_convert as vlc sys.path.insert(0, str(Path.cwd())) @@ -48,72 +45,10 @@ ) if TYPE_CHECKING: - from http.client import HTTPResponse - from tools.schemapi.codegen import ArgInfo, AttrGetter from vl_convert import VegaThemes -def vl_convert_to_vega_version( - *, - version: str | None = None, - url_fmt: str = "https://raw.githubusercontent.com/vega/vl-convert/refs/tags/vl-convert-vendor%40{0}/vl-convert-vendor/src/main.rs", - prefix: bytes = b"const VEGA_PATH", - pattern: str = r".+\/pin\/vega@(?Pv\d\.\d{1,3}\.\d{1,3})", -) -> str: - """ - Return the minimum supported ``vega`` release for a ``vl_convert`` version. - - Parameters - ---------- - version - A target `vl_convert` release. - Defaults to currently installed version. - url_fmt - Format string for source file. - prefix - Byte string prefix of target line. - pattern - Matches and extracts vega version, e.g. `"v5.30.0"`. - - Examples - -------- - >>> vl_convert_to_vega_version(version="0.2.0") - 'v5.22.1' - >>> vl_convert_to_vega_version(version="1.0.0") - 'v5.25.0' - >>> vl_convert_to_vega_version(version="1.3.0") - 'v5.28.0' - >>> vl_convert_to_vega_version(version="1.6.1") - 'v5.30.0' - """ - MIN_VL_CONVERT = "0.2.0" - vlc_version: str = version or vlc.__version__ # pyright: ignore[reportAttributeAccessIssue] - if Version(vlc_version) < Version(MIN_VL_CONVERT): - msg = ( - f"Operation requires `vl_convert>={MIN_VL_CONVERT!r}`\n" - f"but got: {version!r}" - ) - raise NotImplementedError(msg) - with request.urlopen(url_fmt.format(vlc_version)) as response: - response = cast("HTTPResponse", response) - line = response.readline() - while not line.startswith(prefix): - line = response.readline() - if line.startswith(b"fn main"): - msg = f"Failed to find {prefix!r} in {url_fmt.format(vlc_version)}." - raise ValueError(msg) - src_line = line.decode() - if match := re.match(pattern, src_line): - return match.group("vega_version") - else: - msg = ( - f"Failed to match a vega version specifier.\n" - f"{src_line=}\n{pattern=}\n{match=}\n" - ) - raise NotImplementedError(msg) - - SCHEMA_VERSION: Final = "v5.20.1" From 0969b30adc4fa67dd667e851f146efa61eade12d Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:00:19 +0100 Subject: [PATCH 75/77] refactor: Factor out `download_expressions_md` Also moves away from using the legacy `python2` interace https://docs.python.org/3/library/urllib.request.html#legacy-interface --- tools/markup.py | 16 +++++++++++++--- tools/schemapi/vega_expr.py | 26 ++++++-------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/tools/markup.py b/tools/markup.py index d9e8230ec..b17e7ad24 100644 --- a/tools/markup.py +++ b/tools/markup.py @@ -4,7 +4,9 @@ import re from html import unescape +from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Literal +from urllib import request import mistune.util from mistune import InlineParser as _InlineParser @@ -13,7 +15,6 @@ if TYPE_CHECKING: import sys - from pathlib import Path if sys.version_info >= (3, 11): from typing import TypeAlias @@ -23,6 +24,8 @@ from mistune import BaseRenderer, BlockParser, BlockState, InlineState + Url: TypeAlias = str + Token: TypeAlias = "dict[str, Any]" _RE_LINK: Pattern[str] = re.compile(r"(?<=\[)([^\]]+)(?=\]\([^\)]+\))", re.MULTILINE) @@ -127,13 +130,20 @@ def process_text(self, text: str, state: InlineState) -> None: state.append_token({"type": "text", "raw": _RE_LIQUID_INCLUDE.sub(r"", text)}) -def read_ast_tokens(source: Path, /) -> list[Token]: +def read_ast_tokens(source: Url | Path, /) -> list[Token]: """ Read from ``source``, drop ``BlockState``. Factored out to provide accurate typing. """ - return _Markdown(renderer=None, inline=InlineParser()).read(source)[0] + markdown = _Markdown(renderer=None, inline=InlineParser()) + if isinstance(source, Path): + tokens = markdown.read(source) + else: + with request.urlopen(source) as response: + s = response.read().decode("utf-8") + tokens = markdown.parse(s, markdown.block.state_cls()) + return tokens[0] def rst_syntax_for_class(class_name: str) -> str: diff --git a/tools/schemapi/vega_expr.py b/tools/schemapi/vega_expr.py index ce12660f1..90ed3ad0c 100644 --- a/tools/schemapi/vega_expr.py +++ b/tools/schemapi/vega_expr.py @@ -7,7 +7,6 @@ from collections import deque from inspect import getmembers from itertools import chain -from pathlib import Path from textwrap import TextWrapper as _TextWrapper from textwrap import indent from typing import ( @@ -22,7 +21,6 @@ Sequence, overload, ) -from urllib import request from tools.markup import RSTParse, Token, read_ast_tokens from tools.markup import RSTRenderer as _RSTRenderer @@ -33,6 +31,7 @@ if TYPE_CHECKING: import sys + from pathlib import Path from re import Match, Pattern from mistune import BlockState @@ -43,6 +42,8 @@ from typing_extensions import LiteralString, Self from _typeshed import SupportsKeysAndGetItem + from tools.markup import Url + __all__ = ["parse_expressions", "write_expr_module"] @@ -845,20 +846,6 @@ def from_texts(cls, raw_texts: Iterable[str], /) -> Iterator[Self]: continue -def download_expressions_md(url: str, /) -> Path: - """Download to a temporary file, return that as a ``pathlib.Path``.""" - tmp, _ = request.urlretrieve(url) - fp = Path(tmp) - if not fp.exists(): - msg = ( - f"Expressions download failed: {fp!s}.\n\n" - f"Try manually accessing resource: {url!r}" - ) - raise FileNotFoundError(msg) - else: - return fp - - def expand_urls(url: str, /) -> str: if url.startswith("#"): url = f"{EXPRESSIONS_DOCS_URL}{url}" @@ -935,15 +922,14 @@ def italics_to_backticks(s: str, names: Iterable[str], /) -> str: return re.sub(pattern, r"\g``\g``\g", s) -def parse_expressions(url: str, /) -> Iterator[VegaExprDef]: +def parse_expressions(source: Url | Path, /) -> Iterator[VegaExprDef]: """ - Download, read markdown and eagerly parse signatures of relevant definitions. + Download remote or read local `.md` resource and eagerly parse signatures of relevant definitions. Yields with docs to ensure each can use all remapped names, regardless of the order they appear. """ - tokens = read_ast_tokens(download_expressions_md(url)) + tokens = read_ast_tokens(source) expr_defs = tuple(VegaExprDef.from_tokens(tokens)) - request.urlcleanup() VegaExprDef.remap_title.refresh() for expr_def in expr_defs: yield expr_def.with_doc() From ad1ac8f9bdcec0a0c09c26d3a5c4588b21fa9bd9 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:51:04 +0100 Subject: [PATCH 76/77] refactor: `tools.schemapi.vega_expr.py` -> `tools.vega_expr.py` https://github.com/vega/altair/pull/3600#issuecomment-2408521893 --- tools/generate_schema_wrapper.py | 11 ++--------- tools/schemapi/__init__.py | 2 +- tools/{schemapi => }/vega_expr.py | 0 3 files changed, 3 insertions(+), 10 deletions(-) rename tools/{schemapi => }/vega_expr.py (100%) diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py index 22dbede8d..58a395fb3 100644 --- a/tools/generate_schema_wrapper.py +++ b/tools/generate_schema_wrapper.py @@ -21,15 +21,7 @@ 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 import CodeSnippet, SchemaInfo, arg_kwds, arg_required_kwds, codegen from tools.schemapi.utils import ( SchemaProperties, TypeAliasTracer, @@ -43,6 +35,7 @@ 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 diff --git a/tools/schemapi/__init__.py b/tools/schemapi/__init__.py index a08b8f44e..b3ea70704 100644 --- a/tools/schemapi/__init__.py +++ b/tools/schemapi/__init__.py @@ -9,7 +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 +from tools.vega_expr import write_expr_module __all__ = [ "CodeSnippet", diff --git a/tools/schemapi/vega_expr.py b/tools/vega_expr.py similarity index 100% rename from tools/schemapi/vega_expr.py rename to tools/vega_expr.py From 91180d111a958b8eaae13028196f120398de829b Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:59:00 +0100 Subject: [PATCH 77/77] docs: Add module docstring for `vega_expr.py` --- tools/vega_expr.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/vega_expr.py b/tools/vega_expr.py index 90ed3ad0c..ce87cb2fb 100644 --- a/tools/vega_expr.py +++ b/tools/vega_expr.py @@ -1,3 +1,10 @@ +""" +Parsing `Vega Expressions`_ docs to write the ``alt.expr`` module. + +.. _Vega Expressions: + https://vega.github.io/vega/docs/expressions/ +""" + from __future__ import annotations import dataclasses