vgplot 0.29.1__tar.gz → 0.31.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vgplot-0.29.1 → vgplot-0.31.0}/.gitignore +1 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/PKG-INFO +21 -11
- {vgplot-0.29.1 → vgplot-0.31.0}/README.md +19 -9
- {vgplot-0.29.1 → vgplot-0.31.0}/package.json +1 -1
- {vgplot-0.29.1 → vgplot-0.31.0}/pyproject.toml +0 -4
- {vgplot-0.29.1/test → vgplot-0.31.0/tests}/test_full_round_trip.py +15 -19
- {vgplot-0.29.1/test → vgplot-0.31.0/tests}/test_python_api.py +34 -24
- vgplot-0.31.0/tests/test_schema_coverage.py +65 -0
- vgplot-0.31.0/tests/test_unset_sentinel.py +70 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/__init__.py +31 -29
- vgplot-0.31.0/vgplot/_compat.py +101 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/_generated/__init__.py +5 -3
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/_generated/attributes.py +12 -6
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/_generated/encodings.py +9 -7
- vgplot-0.31.0/vgplot/_generated/marks.py +4126 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/_types.py +8 -9
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/params.py +6 -6
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/plot.py +22 -18
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/spec.py +24 -13
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/util.py +2 -0
- vgplot-0.29.1/test/test_schema_coverage.py +0 -61
- vgplot-0.29.1/vgplot/_generated/marks.py +0 -4124
- {vgplot-0.29.1 → vgplot-0.31.0}/CONTRIBUTING.md +0 -0
- {vgplot-0.29.1/test → vgplot-0.31.0/tests}/__init__.py +0 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/data.py +0 -0
- {vgplot-0.29.1 → vgplot-0.31.0}/vgplot/encodings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vgplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
4
4
|
Summary: A Python API for authoring Mosaic vgplot visualizations
|
|
5
5
|
Project-URL: homepage, https://github.com/uwdata/mosaic
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -59,18 +59,30 @@ brush = vg.selection.crossfilter()
|
|
|
59
59
|
|
|
60
60
|
view = vg.vconcat(
|
|
61
61
|
vg.plot(
|
|
62
|
-
vg.rect_y(
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
vg.rect_y(
|
|
63
|
+
flights,
|
|
64
|
+
x=vg.bin("delay"),
|
|
65
|
+
y=vg.count(),
|
|
66
|
+
filter_by=brush,
|
|
67
|
+
fill="steelblue",
|
|
68
|
+
inset_left=0.5,
|
|
69
|
+
inset_right=0.5,
|
|
70
|
+
),
|
|
65
71
|
vg.interval_x(bind=brush),
|
|
66
72
|
vg.x_domain("Fixed"),
|
|
67
73
|
vg.x_label("Arrival Delay (min)"),
|
|
68
74
|
vg.height(200),
|
|
69
75
|
),
|
|
70
76
|
vg.plot(
|
|
71
|
-
vg.rect_y(
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
vg.rect_y(
|
|
78
|
+
flights,
|
|
79
|
+
x=vg.bin("time"),
|
|
80
|
+
y=vg.count(),
|
|
81
|
+
filter_by=brush,
|
|
82
|
+
fill="steelblue",
|
|
83
|
+
inset_left=0.5,
|
|
84
|
+
inset_right=0.5,
|
|
85
|
+
),
|
|
74
86
|
vg.interval_x(bind=brush),
|
|
75
87
|
vg.x_domain("Fixed"),
|
|
76
88
|
vg.x_label("Departure Time (hour)"),
|
|
@@ -94,9 +106,7 @@ bias = vg.param(0)
|
|
|
94
106
|
view = vg.vconcat(
|
|
95
107
|
vg.slider(label="Bias", bind=bias, min=0, max=1000, step=1),
|
|
96
108
|
vg.plot(
|
|
97
|
-
vg.area_y(walk, x="t", y=vg.sql("v + $bias")),
|
|
98
|
-
vg.width(680),
|
|
99
|
-
vg.height(200),
|
|
109
|
+
vg.area_y(walk, x="t", y=vg.sql("v + $bias")), vg.width(680), vg.height(200)
|
|
100
110
|
),
|
|
101
111
|
)
|
|
102
112
|
|
|
@@ -50,18 +50,30 @@ brush = vg.selection.crossfilter()
|
|
|
50
50
|
|
|
51
51
|
view = vg.vconcat(
|
|
52
52
|
vg.plot(
|
|
53
|
-
vg.rect_y(
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
vg.rect_y(
|
|
54
|
+
flights,
|
|
55
|
+
x=vg.bin("delay"),
|
|
56
|
+
y=vg.count(),
|
|
57
|
+
filter_by=brush,
|
|
58
|
+
fill="steelblue",
|
|
59
|
+
inset_left=0.5,
|
|
60
|
+
inset_right=0.5,
|
|
61
|
+
),
|
|
56
62
|
vg.interval_x(bind=brush),
|
|
57
63
|
vg.x_domain("Fixed"),
|
|
58
64
|
vg.x_label("Arrival Delay (min)"),
|
|
59
65
|
vg.height(200),
|
|
60
66
|
),
|
|
61
67
|
vg.plot(
|
|
62
|
-
vg.rect_y(
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
vg.rect_y(
|
|
69
|
+
flights,
|
|
70
|
+
x=vg.bin("time"),
|
|
71
|
+
y=vg.count(),
|
|
72
|
+
filter_by=brush,
|
|
73
|
+
fill="steelblue",
|
|
74
|
+
inset_left=0.5,
|
|
75
|
+
inset_right=0.5,
|
|
76
|
+
),
|
|
65
77
|
vg.interval_x(bind=brush),
|
|
66
78
|
vg.x_domain("Fixed"),
|
|
67
79
|
vg.x_label("Departure Time (hour)"),
|
|
@@ -85,9 +97,7 @@ bias = vg.param(0)
|
|
|
85
97
|
view = vg.vconcat(
|
|
86
98
|
vg.slider(label="Bias", bind=bias, min=0, max=1000, step=1),
|
|
87
99
|
vg.plot(
|
|
88
|
-
vg.area_y(walk, x="t", y=vg.sql("v + $bias")),
|
|
89
|
-
vg.width(680),
|
|
90
|
-
vg.height(200),
|
|
100
|
+
vg.area_y(walk, x="t", y=vg.sql("v + $bias")), vg.width(680), vg.height(200)
|
|
91
101
|
),
|
|
92
102
|
)
|
|
93
103
|
|
|
@@ -3,48 +3,44 @@
|
|
|
3
3
|
# in specs/json/. Also checks that Python, JSON, and ESM example sets stay
|
|
4
4
|
# in sync (same filenames).
|
|
5
5
|
#
|
|
6
|
-
# Run: pytest packages/vgplot/vgplot-python/
|
|
6
|
+
# Run: pytest packages/vgplot/vgplot-python/tests/test_full_round_trip.py
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
import json
|
|
8
|
-
import sys
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
SPEC_DIR = ROOT / "specs"
|
|
15
|
+
SPEC_DIR = Path(__file__).resolve().parents[4] / "specs"
|
|
16
16
|
JSON_DIR = SPEC_DIR / "json"
|
|
17
17
|
ESM_DIR = SPEC_DIR / "esm"
|
|
18
18
|
PYTHON_DIR = SPEC_DIR / "python"
|
|
19
19
|
|
|
20
|
-
VGPLOT_PKG = ROOT / "packages" / "vgplot" / "vgplot-python"
|
|
21
|
-
|
|
22
|
-
for p in (VGPLOT_PKG, ROOT):
|
|
23
|
-
if str(p) not in sys.path:
|
|
24
|
-
sys.path.insert(0, str(p))
|
|
25
|
-
|
|
26
20
|
|
|
27
|
-
def load_json_fixture(name: str) -> dict:
|
|
21
|
+
def load_json_fixture(name: str) -> dict[str, Any]:
|
|
28
22
|
return json.loads((JSON_DIR / f"{name}.json").read_text(encoding="utf-8"))
|
|
29
23
|
|
|
30
24
|
|
|
31
|
-
def run_spec_file_with_exec(path: Path) -> dict:
|
|
32
|
-
namespace
|
|
25
|
+
def run_spec_file_with_exec(path: Path) -> dict[str, Any]:
|
|
26
|
+
namespace = {
|
|
33
27
|
"__name__": "__spec_exec__",
|
|
34
28
|
"__file__": str(path),
|
|
35
29
|
"__builtins__": __builtins__,
|
|
36
30
|
}
|
|
37
31
|
code = compile(path.read_text(encoding="utf-8"), str(path), "exec")
|
|
38
|
-
exec(code, namespace)
|
|
32
|
+
exec(code, namespace) # ruff: ignore[exec-builtin]
|
|
39
33
|
|
|
40
34
|
view = namespace.get("view")
|
|
41
35
|
if view is None:
|
|
42
|
-
|
|
36
|
+
msg = f"`{path.name}` must define a top-level `view` variable"
|
|
37
|
+
raise AssertionError(msg)
|
|
43
38
|
if not hasattr(view, "to_dict"):
|
|
44
|
-
|
|
39
|
+
msg = f"`view` in `{path.name}` does not expose to_dict()"
|
|
40
|
+
raise AssertionError(msg)
|
|
45
41
|
# Execute to_dict() within the spec namespace so frame inspection finds the right variables
|
|
46
42
|
namespace["__view__"] = view
|
|
47
|
-
exec("__result__ = __view__.to_dict()", namespace)
|
|
43
|
+
exec("__result__ = __view__.to_dict()", namespace) # ruff: ignore[exec-builtin]
|
|
48
44
|
return namespace["__result__"]
|
|
49
45
|
|
|
50
46
|
|
|
@@ -53,13 +49,13 @@ ESM_EXAMPLES = sorted(path.stem for path in ESM_DIR.glob("*.js"))
|
|
|
53
49
|
PYTHON_EXAMPLES = sorted(path.stem for path in PYTHON_DIR.glob("*.py"))
|
|
54
50
|
|
|
55
51
|
|
|
56
|
-
def test_generated_examples_stay_in_sync():
|
|
52
|
+
def test_generated_examples_stay_in_sync() -> None:
|
|
57
53
|
assert PYTHON_EXAMPLES == JSON_EXAMPLES
|
|
58
54
|
assert ESM_EXAMPLES == JSON_EXAMPLES
|
|
59
55
|
|
|
60
56
|
|
|
61
57
|
@pytest.mark.parametrize("example_name", PYTHON_EXAMPLES)
|
|
62
|
-
def test_python_round_trip(example_name: str):
|
|
58
|
+
def test_python_round_trip(example_name: str) -> None:
|
|
63
59
|
generated = run_spec_file_with_exec(PYTHON_DIR / f"{example_name}.py")
|
|
64
60
|
original = load_json_fixture(example_name)
|
|
65
61
|
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
# Unit tests for the vgplot Python API, covering behaviors that the
|
|
2
2
|
# generated-spec round-trip suite does not exercise directly.
|
|
3
|
-
import
|
|
4
|
-
from pathlib import Path
|
|
3
|
+
from __future__ import annotations
|
|
5
4
|
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
ROOT = Path(__file__).resolve().parents[4]
|
|
9
|
-
VGPLOT_PKG = ROOT / "packages" / "vgplot" / "vgplot-python"
|
|
10
|
-
for p in (VGPLOT_PKG, ROOT):
|
|
11
|
-
if str(p) not in sys.path:
|
|
12
|
-
sys.path.insert(0, str(p))
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
13
6
|
|
|
14
|
-
import
|
|
15
|
-
|
|
7
|
+
import pytest
|
|
8
|
+
import vgplot as vg
|
|
9
|
+
from vgplot.plot import Mark
|
|
16
10
|
|
|
17
11
|
|
|
18
12
|
class TestGeneratedMarks:
|
|
19
13
|
"""Marks are generated from the schema; there is no dynamic fallback, so
|
|
20
14
|
unknown names fail loudly rather than being silently fabricated."""
|
|
21
15
|
|
|
22
|
-
def test_positional_data_with_encodings_is_kept(self):
|
|
16
|
+
def test_positional_data_with_encodings_is_kept(self) -> None:
|
|
23
17
|
# A leading positional arg is the data source even when the call also
|
|
24
18
|
# has keyword encodings.
|
|
25
19
|
mark = vg.waffle_y("athletes", x="a", y={"count": ""})
|
|
@@ -29,45 +23,45 @@ class TestGeneratedMarks:
|
|
|
29
23
|
assert d["data"] == {"from": "athletes"}
|
|
30
24
|
assert d["x"] == "a"
|
|
31
25
|
|
|
32
|
-
def test_explicit_none_channel_is_preserved(self):
|
|
26
|
+
def test_explicit_none_channel_is_preserved(self) -> None:
|
|
33
27
|
# Passing a channel explicitly as None keeps it in the output (distinct
|
|
34
28
|
# from not passing it at all).
|
|
35
29
|
assert vg.line_y("t", x="a", z=None).to_dict()["z"] is None
|
|
36
30
|
assert "z" not in vg.line_y("t", x="a").to_dict()
|
|
37
31
|
|
|
38
|
-
def test_unknown_name_raises(self):
|
|
32
|
+
def test_unknown_name_raises(self) -> None:
|
|
39
33
|
# No __getattr__ fallback: a missing or mis-typed API name is an error.
|
|
40
34
|
with pytest.raises(AttributeError):
|
|
41
|
-
vg.some_custom_mark # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
35
|
+
_ = vg.some_custom_mark # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
42
36
|
with pytest.raises(AttributeError):
|
|
43
|
-
vg.definitely_not_a_real_directive # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
37
|
+
_ = vg.definitely_not_a_real_directive # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
|
44
38
|
|
|
45
39
|
|
|
46
40
|
class TestDataHelpers:
|
|
47
|
-
def test_json_inline_data(self):
|
|
41
|
+
def test_json_inline_data(self) -> None:
|
|
48
42
|
d = vg.json([{"a": 1}, {"a": 2}])
|
|
49
43
|
assert d.to_dict() == {"type": "json", "data": [{"a": 1}, {"a": 2}]}
|
|
50
44
|
|
|
51
|
-
def test_json_file(self):
|
|
45
|
+
def test_json_file(self) -> None:
|
|
52
46
|
assert vg.json(file="x.json").to_dict() == {"type": "json", "file": "x.json"}
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
class TestParams:
|
|
56
|
-
def test_date_param(self):
|
|
50
|
+
def test_date_param(self) -> None:
|
|
57
51
|
from datetime import date
|
|
58
52
|
|
|
59
53
|
assert vg.param(date(2013, 5, 13)).param_def() == {"date": "2013-05-13"}
|
|
60
54
|
|
|
61
|
-
def test_datetime_param(self):
|
|
55
|
+
def test_datetime_param(self) -> None:
|
|
62
56
|
from datetime import datetime
|
|
63
57
|
|
|
64
|
-
assert vg.param(datetime(2013, 5, 13, 10, 30)).param_def() == {
|
|
58
|
+
assert vg.param(datetime(2013, 5, 13, 10, 30)).param_def() == { # ruff: ignore[call-datetime-without-tzinfo]
|
|
65
59
|
"date": "2013-05-13T10:30:00"
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
|
|
69
63
|
class TestAutoNaming:
|
|
70
|
-
def test_auto_param_name_skips_explicit_name(self):
|
|
64
|
+
def test_auto_param_name_skips_explicit_name(self) -> None:
|
|
71
65
|
from vgplot.spec import Spec
|
|
72
66
|
|
|
73
67
|
named = vg.param(1)
|
|
@@ -86,7 +80,8 @@ class TestDataFrames:
|
|
|
86
80
|
referenced as a table, and carried in the spec's data section for the host
|
|
87
81
|
(e.g. the widget) to register."""
|
|
88
82
|
|
|
89
|
-
def test_frame_is_referenced_and_named_after_variable(self):
|
|
83
|
+
def test_frame_is_referenced_and_named_after_variable(self) -> None:
|
|
84
|
+
pytest.importorskip("pyarrow")
|
|
90
85
|
import pyarrow as pa
|
|
91
86
|
|
|
92
87
|
weather = pa.table({"a": [1, 2, 3]})
|
|
@@ -96,7 +91,8 @@ class TestDataFrames:
|
|
|
96
91
|
assert d["plot"][0]["data"] == {"from": "weather"}
|
|
97
92
|
assert d["data"]["weather"] is weather
|
|
98
93
|
|
|
99
|
-
def test_frame_and_datadef_share_the_data_section(self):
|
|
94
|
+
def test_frame_and_datadef_share_the_data_section(self) -> None:
|
|
95
|
+
pytest.importorskip("pyarrow")
|
|
100
96
|
import pyarrow as pa
|
|
101
97
|
|
|
102
98
|
weather = pa.table({"a": [1]})
|
|
@@ -108,3 +104,17 @@ class TestDataFrames:
|
|
|
108
104
|
assert d["plot"][1]["data"] == {"from": "athletes"}
|
|
109
105
|
assert d["data"]["weather"] is weather
|
|
110
106
|
assert d["data"]["athletes"] == {"type": "csv", "file": "athletes.csv"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if TYPE_CHECKING:
|
|
110
|
+
|
|
111
|
+
def typing_dunder_all() -> None:
|
|
112
|
+
# Ok
|
|
113
|
+
_ = vg.arrow
|
|
114
|
+
_ = vg.errorbar_x
|
|
115
|
+
_ = vg.mark
|
|
116
|
+
|
|
117
|
+
# TODO @dangotbanned: Get pyright to error on these
|
|
118
|
+
# Error (`_generated` modules)
|
|
119
|
+
_ = vg.attributes # ty: ignore[unresolved-attribute]
|
|
120
|
+
_ = vg.marks # ty: ignore[unresolved-attribute]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Ensures the exported vgplot API covers every mark, attribute, interactor, and
|
|
2
|
+
# input in the Mosaic JSON schema (the source of truth). Marks and attributes
|
|
3
|
+
# are generated; interactors and inputs are hand-written. This guards against a
|
|
4
|
+
# schema addition that was never generated or hand-added.
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import vgplot as vg
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from typing_extensions import TypedDict
|
|
18
|
+
|
|
19
|
+
class Schema(TypedDict, closed=True, total=False):
|
|
20
|
+
properties: dict[str, dict[str, Any]]
|
|
21
|
+
anyOf: Sequence[Schema]
|
|
22
|
+
|
|
23
|
+
class Definitions(TypedDict, extra_items=Schema):
|
|
24
|
+
PlotAttributes: Schema
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parents[4]
|
|
28
|
+
SCHEMA = ROOT / "docs" / "public" / "schema" / "latest.json"
|
|
29
|
+
|
|
30
|
+
# Schema names whose Python export is renamed or intentionally absent.
|
|
31
|
+
RENAME = {"table": "table_input"} # `table` input -> vg.table_input (vg.table is data)
|
|
32
|
+
IGNORE = {"value"} # internal selection const, not a user-facing interactor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _snake(name: str) -> str:
|
|
36
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_discriminator_values(defs: Definitions, key: str) -> set[str]:
|
|
40
|
+
out: set[str] = set()
|
|
41
|
+
for d in defs.values():
|
|
42
|
+
if (p := d.get("properties")) and (c := p.get(key, {}).get("const")):
|
|
43
|
+
out.add(c)
|
|
44
|
+
elif u := d.get("anyOf"):
|
|
45
|
+
out.update(
|
|
46
|
+
c
|
|
47
|
+
for member in u
|
|
48
|
+
if (p := member.get("properties"))
|
|
49
|
+
and (c := p.get(key, {}).get("const"))
|
|
50
|
+
)
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_schema_surface_is_exported() -> None:
|
|
55
|
+
defs: Definitions = json.loads(SCHEMA.read_text("utf-8"))["definitions"]
|
|
56
|
+
names = (
|
|
57
|
+
get_discriminator_values(defs, "mark")
|
|
58
|
+
| get_discriminator_values(defs, "select")
|
|
59
|
+
| get_discriminator_values(defs, "input")
|
|
60
|
+
| set(defs["PlotAttributes"].get("properties", ()))
|
|
61
|
+
) - IGNORE
|
|
62
|
+
|
|
63
|
+
exported = set(vg.__all__)
|
|
64
|
+
missing = sorted(n for n in names if RENAME.get(n, _snake(n)) not in exported)
|
|
65
|
+
assert not missing, f"schema names with no exported vgplot function: {missing}"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Union, get_type_hints
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
# NOTE: Don't move this into a runtime import (yet)
|
|
8
|
+
from vgplot._types import UNSET
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _import_unset() -> UNSET:
|
|
12
|
+
# NOTE: A regular import would add:
|
|
13
|
+
# - `vgplot._types` to `sys.modules`
|
|
14
|
+
# - `UNSET` to `globals`
|
|
15
|
+
# And we can't use `importlib.reload`, since that would create a new object
|
|
16
|
+
from vgplot._types import UNSET
|
|
17
|
+
|
|
18
|
+
return UNSET
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_unset_identity() -> None:
|
|
22
|
+
unset_1 = _import_unset()
|
|
23
|
+
unset_2 = _import_unset()
|
|
24
|
+
assert unset_1 is unset_2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_unset_repr() -> None:
|
|
28
|
+
assert repr(_import_unset()) == "UNSET"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_unset_pickle() -> None:
|
|
32
|
+
import pickle
|
|
33
|
+
|
|
34
|
+
unset = _import_unset()
|
|
35
|
+
assert pickle.loads(pickle.dumps(unset)) is unset
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_unset_type_expression_union() -> None:
|
|
39
|
+
# Adapted from https://github.com/python/typing_extensions/blob/83400e979b8e3b0b647f9a6a57f0275230e5f19f/src/test_typing_extensions.py#L9694-L9701
|
|
40
|
+
from vgplot._types import UNSET
|
|
41
|
+
|
|
42
|
+
def func1(a: int | UNSET = UNSET) -> None: ...
|
|
43
|
+
def func2(a: UNSET | int = UNSET) -> None: ...
|
|
44
|
+
|
|
45
|
+
assert get_type_hints(func1, localns=locals())["a"] == Union[int, UNSET] # noqa: UP007
|
|
46
|
+
assert get_type_hints(func2, localns=locals())["a"] == Union[UNSET, int] # noqa: UP007
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_unset_copy_identity() -> None:
|
|
50
|
+
# Adapted from https://github.com/python/typing_extensions/blob/83400e979b8e3b0b647f9a6a57f0275230e5f19f/src/test_typing_extensions.py#L9711-L9713
|
|
51
|
+
unset = _import_unset()
|
|
52
|
+
assert unset is copy.copy(unset)
|
|
53
|
+
assert unset is copy.deepcopy(unset)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_unset_union_identity() -> None:
|
|
57
|
+
unset = _import_unset()
|
|
58
|
+
assert (unset | unset) is unset
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from typing_extensions import assert_type
|
|
63
|
+
|
|
64
|
+
def typing_unset(
|
|
65
|
+
a: UNSET, b: str | UNSET, c: Any | UNSET, d: int | None | UNSET = UNSET
|
|
66
|
+
) -> None:
|
|
67
|
+
assert_type(a, UNSET)
|
|
68
|
+
assert_type(b, str | UNSET)
|
|
69
|
+
assert_type(c, Any | UNSET)
|
|
70
|
+
assert_type(d, int | None | UNSET)
|
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._generated import * # noqa: F403
|
|
4
|
+
from ._generated import __all__ as _generated_all
|
|
5
|
+
from .data import csv, data, json, parquet, spatial, table
|
|
6
|
+
from .encodings import channels, sql
|
|
3
7
|
from .params import param, selection
|
|
4
|
-
from .encodings import sql, channels
|
|
5
8
|
from .plot import (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
brush,
|
|
10
|
+
checkbox,
|
|
11
|
+
color_legend,
|
|
9
12
|
directive,
|
|
10
|
-
margins,
|
|
11
|
-
vconcat,
|
|
12
13
|
hconcat,
|
|
13
|
-
|
|
14
|
+
highlight,
|
|
14
15
|
hspace,
|
|
15
|
-
slider,
|
|
16
|
-
select,
|
|
17
|
-
checkbox,
|
|
18
|
-
menu,
|
|
19
|
-
search,
|
|
20
|
-
table_input,
|
|
21
16
|
input,
|
|
22
|
-
option,
|
|
23
|
-
brush,
|
|
24
|
-
sort,
|
|
25
17
|
interval_x,
|
|
26
|
-
interval_y,
|
|
27
18
|
interval_xy,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
interval_y,
|
|
20
|
+
margins,
|
|
21
|
+
mark,
|
|
22
|
+
menu,
|
|
32
23
|
nearest_x,
|
|
33
24
|
nearest_y,
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
opacity_legend,
|
|
26
|
+
option,
|
|
36
27
|
pan,
|
|
37
28
|
pan_x,
|
|
38
29
|
pan_y,
|
|
39
30
|
pan_zoom,
|
|
40
31
|
pan_zoom_x,
|
|
41
32
|
pan_zoom_y,
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
plot,
|
|
34
|
+
region,
|
|
35
|
+
search,
|
|
36
|
+
select,
|
|
37
|
+
slider,
|
|
38
|
+
sort,
|
|
39
|
+
source,
|
|
44
40
|
symbol_legend,
|
|
41
|
+
table_input,
|
|
42
|
+
toggle,
|
|
43
|
+
toggle_color,
|
|
44
|
+
toggle_x,
|
|
45
|
+
toggle_y,
|
|
46
|
+
vconcat,
|
|
47
|
+
vspace,
|
|
45
48
|
)
|
|
46
|
-
from .
|
|
47
|
-
from ._generated import __all__ as _generated_all
|
|
49
|
+
from .spec import Spec, View, spec
|
|
48
50
|
|
|
49
51
|
__all__ = [
|
|
50
52
|
"Spec",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Backwards compatibility for features not available at our [requires-python][1].
|
|
2
|
+
|
|
3
|
+
Import from here to avoid introducing runtime dependency on [typing_extensions][2].
|
|
4
|
+
|
|
5
|
+
[1]: https://packaging.python.org/en/latest/specifications/pyproject-toml/#requires-python
|
|
6
|
+
[2]: https://github.com/python/typing_extensions
|
|
7
|
+
|
|
8
|
+
## sentinel
|
|
9
|
+
[`sentinel`][3] was introduced in `3.15` (see [PEP 661][4]).
|
|
10
|
+
|
|
11
|
+
We can remove the backport after [3.15 end-of-life][5].
|
|
12
|
+
|
|
13
|
+
[3]: https://docs.python.org/3.15/library/functions.html#sentinel
|
|
14
|
+
[4]: https://peps.python.org/pep-0661/
|
|
15
|
+
[5]: https://peps.python.org/pep-0790/#lifespan
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import typing
|
|
22
|
+
from contextlib import suppress
|
|
23
|
+
from importlib.util import find_spec
|
|
24
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sentinel_backport_pre_typing_extensions_4_16() -> Any:
|
|
28
|
+
class _sentinel_backport:
|
|
29
|
+
"""Create a unique sentinel object.
|
|
30
|
+
|
|
31
|
+
*name* should be the name of the variable to which the return value shall be assigned.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, name: str, /, *, repr: str | None = None) -> None:
|
|
35
|
+
self.__name__: str = name
|
|
36
|
+
self._repr: str = repr if repr is not None else name
|
|
37
|
+
# TODO @dangotbanned: Figure out why they didn't use the `"__main__"` default here?
|
|
38
|
+
module: str | None = None
|
|
39
|
+
if hasattr(sys, "_getframemodulename"):
|
|
40
|
+
module = sys._getframemodulename(1) or "__main__"
|
|
41
|
+
elif hasattr(sys, "_getframe"):
|
|
42
|
+
with suppress(ValueError):
|
|
43
|
+
module = sys._getframe(1).f_globals.get("__name__", "__main__")
|
|
44
|
+
|
|
45
|
+
# For pickling as a singleton
|
|
46
|
+
self.__module__ = module # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment]
|
|
47
|
+
|
|
48
|
+
__init_subclass__: ClassVar[None] = None
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return self._repr
|
|
52
|
+
|
|
53
|
+
if sys.version_info < (3, 11):
|
|
54
|
+
# The presence of this method convinces typing._type_check that Sentinels are types.
|
|
55
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
56
|
+
msg = f"{type(self).__name__!r} object is not callable"
|
|
57
|
+
raise TypeError(msg)
|
|
58
|
+
|
|
59
|
+
def __or__(self, other: Any) -> Any:
|
|
60
|
+
return typing.Union[self, other] # noqa: UP007
|
|
61
|
+
|
|
62
|
+
def __ror__(self, other: Any) -> Any:
|
|
63
|
+
return typing.Union[other, self] # noqa: UP007
|
|
64
|
+
|
|
65
|
+
def __reduce__(self) -> str:
|
|
66
|
+
return self.__name__
|
|
67
|
+
|
|
68
|
+
return _sentinel_backport
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _sentinel_backport_pre_py_3_15() -> Any:
|
|
72
|
+
"""Return a [PEP 661]-compatible [`sentinel`](https://docs.python.org/3.15/library/functions.html#sentinel) factory.
|
|
73
|
+
|
|
74
|
+
[PEP 661]: https://peps.python.org/pep-0661/
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
- Does not depend on `typing_extensions`, but will use it if a suitable version is available
|
|
78
|
+
- Fallback is adapted from [`typing_extensions==4.16.0`](https://github.com/python/typing_extensions/blob/f29cd28d8ed7642cafb1d18daf5aa41be6a5c0aa/src/typing_extensions.py#L176-L271)
|
|
79
|
+
"""
|
|
80
|
+
if find_spec("typing_extensions"):
|
|
81
|
+
import typing_extensions
|
|
82
|
+
|
|
83
|
+
# NOTE: In the same release the name changed, this guy landed https://github.com/python/typing_extensions/pull/617
|
|
84
|
+
# `4.14-4.15` is fine for typing, but the runtime changes are too big to rely on
|
|
85
|
+
if hasattr(typing_extensions, "sentinel"):
|
|
86
|
+
return getattr(typing_extensions, "sentinel", typing_extensions.Sentinel)
|
|
87
|
+
|
|
88
|
+
return _sentinel_backport_pre_typing_extensions_4_16()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if TYPE_CHECKING:
|
|
92
|
+
# Was renamed in https://github.com/python/typing_extensions/releases/tag/4.16.0
|
|
93
|
+
from typing_extensions import Sentinel as sentinel
|
|
94
|
+
else: # ruff: ignore[collapsible-else-if]
|
|
95
|
+
if sys.version_info >= (3, 15):
|
|
96
|
+
from builtins import sentinel
|
|
97
|
+
else:
|
|
98
|
+
sentinel = _sentinel_backport_pre_py_3_15()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ("sentinel",)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.
|
|
2
2
|
# Regenerate with: pnpm run generate:python-api
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from .attributes import * # noqa: F403
|
|
4
|
-
from .encodings import * # noqa: F403
|
|
5
|
-
from .marks import * # noqa: F403
|
|
6
6
|
from .attributes import __all__ as _attr_all
|
|
7
|
+
from .encodings import * # noqa: F403
|
|
7
8
|
from .encodings import __all__ as _enc_all
|
|
9
|
+
from .marks import * # noqa: F403
|
|
8
10
|
from .marks import __all__ as _mark_all
|
|
9
11
|
|
|
10
|
-
__all__ = [*_mark_all, *_attr_all, *_enc_all]
|
|
12
|
+
__all__ = [*_mark_all, *_attr_all, *_enc_all] # pyright: ignore[reportUnsupportedDunderAll] # ruff: ignore[invalid-all-object]
|