perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_0_wasm32.whl
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.
- perspective/__init__.py +403 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +69 -0
- perspective/handlers/starlette.py +63 -0
- perspective/handlers/tornado.py +192 -0
- perspective/perspective.abi3.so +0 -0
- perspective/templates/exported_widget.html.template +39 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/async/test_async_client.py +83 -0
- perspective/tests/async/test_websocket_client.py +124 -0
- perspective/tests/column_paths.py +33 -0
- perspective/tests/conftest.py +276 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +351 -0
- perspective/tests/multi_threaded/__init__.py +11 -0
- perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
- perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1016 -0
- perspective/tests/server/test_session.py +110 -0
- perspective/tests/table/__init__.py +11 -0
- perspective/tests/table/arrow/date32.arrow +0 -0
- perspective/tests/table/arrow/date64.arrow +0 -0
- perspective/tests/table/arrow/dict.arrow +0 -0
- perspective/tests/table/arrow/dict_update.arrow +0 -0
- perspective/tests/table/arrow/int_float_str.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
- perspective/tests/table/object_sequence.py +402 -0
- perspective/tests/table/test_column_paths.py +88 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_derived.py +145 -0
- perspective/tests/table/test_exception.py +73 -0
- perspective/tests/table/test_join.py +115 -0
- perspective/tests/table/test_leaks.py +105 -0
- perspective/tests/table/test_on_remove.py +140 -0
- perspective/tests/table/test_ports.py +178 -0
- perspective/tests/table/test_remove.py +102 -0
- perspective/tests/table/test_table.py +641 -0
- perspective/tests/table/test_table_arrow.py +500 -0
- perspective/tests/table/test_table_arrow_nested.py +535 -0
- perspective/tests/table/test_table_datetime.py +2544 -0
- perspective/tests/table/test_table_infer.py +201 -0
- perspective/tests/table/test_table_limit.py +45 -0
- perspective/tests/table/test_table_numpy.py +1022 -0
- perspective/tests/table/test_table_page_to_disk.py +304 -0
- perspective/tests/table/test_table_pandas.py +1018 -0
- perspective/tests/table/test_table_polars.py +251 -0
- perspective/tests/table/test_table_view_table.py +130 -0
- perspective/tests/table/test_to_arrow.py +416 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_arrow_zstd.py +41 -0
- perspective/tests/table/test_to_format.py +1030 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +561 -0
- perspective/tests/table/test_update_arrow.py +980 -0
- perspective/tests/table/test_update_pandas.py +211 -0
- perspective/tests/table/test_view.py +2503 -0
- perspective/tests/table/test_view_expression.py +2256 -0
- perspective/tests/test_dependencies.py +53 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_viewer.py +246 -0
- perspective/tests/virtual_servers/__init__.py +12 -0
- perspective/tests/virtual_servers/test_coerce_types.py +464 -0
- perspective/tests/virtual_servers/test_duckdb.py +1280 -0
- perspective/tests/virtual_servers/test_polars.py +1075 -0
- perspective/tests/virtual_servers/test_postgres.py +964 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_anywidget_smoke.py +109 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_html_export.py +113 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +206 -0
- perspective/virtual_servers/clickhouse.py +296 -0
- perspective/virtual_servers/duckdb.py +367 -0
- perspective/virtual_servers/polars.py +750 -0
- perspective/virtual_servers/postgres.py +448 -0
- perspective/widget/__init__.py +351 -0
- perspective/widget/static/perspective-anywidget.css +1 -0
- perspective/widget/static/perspective-anywidget.js +31 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/viewer.py +355 -0
- perspective/widget/viewer/viewer_traitlets.py +98 -0
- perspective_python-5.6.0.dist-info/METADATA +127 -0
- perspective_python-5.6.0.dist-info/RECORD +90 -0
- perspective_python-5.6.0.dist-info/WHEEL +4 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
- perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import pathlib
|
|
14
|
+
|
|
15
|
+
import anywidget
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import perspective.widget as widget_module
|
|
19
|
+
from perspective.widget import PerspectiveWidget
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
|
22
|
+
|
|
23
|
+
_STATIC = pathlib.Path(widget_module.__file__).parent / "static"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestAnyWidgetSmoke:
|
|
27
|
+
def test_instantiates_outside_jupyterlab(self):
|
|
28
|
+
widget = PerspectiveWidget({"a": [1, 2, 3, 4, 5]}, plugin="X Bar")
|
|
29
|
+
assert isinstance(widget, anywidget.AnyWidget)
|
|
30
|
+
assert widget.plugin == "X Bar"
|
|
31
|
+
|
|
32
|
+
def test_traits_round_trip(self):
|
|
33
|
+
widget = PerspectiveWidget(
|
|
34
|
+
{"a": [1, 2, 3]}, group_by=["a"], sort=[["a", "desc"]]
|
|
35
|
+
)
|
|
36
|
+
assert widget.group_by == ["a"]
|
|
37
|
+
assert widget.sort == [["a", "desc"]]
|
|
38
|
+
widget.split_by = ["a"]
|
|
39
|
+
assert widget.split_by == ["a"]
|
|
40
|
+
|
|
41
|
+
def test_columns_config_and_group_rollup_mode(self):
|
|
42
|
+
widget = PerspectiveWidget({"a": [1, 2, 3]})
|
|
43
|
+
assert widget.columns_config == {}
|
|
44
|
+
assert widget.group_rollup_mode == "rollup"
|
|
45
|
+
assert PerspectiveWidget.columns_config.metadata.get("sync") is True
|
|
46
|
+
assert PerspectiveWidget.group_rollup_mode.metadata.get("sync") is True
|
|
47
|
+
|
|
48
|
+
widget = PerspectiveWidget(
|
|
49
|
+
{"a": [1, 2, 3]},
|
|
50
|
+
columns_config={"a": {"fg_mode": "bar"}},
|
|
51
|
+
group_rollup_mode="flat",
|
|
52
|
+
)
|
|
53
|
+
assert widget.columns_config == {"a": {"fg_mode": "bar"}}
|
|
54
|
+
assert widget.group_rollup_mode == "flat"
|
|
55
|
+
|
|
56
|
+
def test_frontend_state_update_applies_with_version(self):
|
|
57
|
+
widget = PerspectiveWidget({"a": [1, 2, 3]})
|
|
58
|
+
widget.set_state(
|
|
59
|
+
{
|
|
60
|
+
"plugin": "X Bar",
|
|
61
|
+
"columns": ["a"],
|
|
62
|
+
"group_by": ["a"],
|
|
63
|
+
"settings": False,
|
|
64
|
+
"theme": "Pro Dark",
|
|
65
|
+
"version": "0.0.0",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert widget.plugin == "X Bar"
|
|
70
|
+
assert widget.columns == ["a"]
|
|
71
|
+
assert widget.group_by == ["a"]
|
|
72
|
+
assert widget.settings is False
|
|
73
|
+
assert widget.theme == "Pro Dark"
|
|
74
|
+
assert widget.version == "0.0.0"
|
|
75
|
+
|
|
76
|
+
def test_not_a_labextension_widget(self):
|
|
77
|
+
widget = PerspectiveWidget({"a": [1, 2, 3]})
|
|
78
|
+
assert widget._model_module == "anywidget"
|
|
79
|
+
assert widget._view_module == "anywidget"
|
|
80
|
+
for attr in (
|
|
81
|
+
"_model_module",
|
|
82
|
+
"_view_module",
|
|
83
|
+
"_model_name",
|
|
84
|
+
"_view_name",
|
|
85
|
+
):
|
|
86
|
+
assert getattr(widget, attr, "") != "@perspective-dev/jupyterlab"
|
|
87
|
+
|
|
88
|
+
def test_esm_css_configured(self):
|
|
89
|
+
assert PerspectiveWidget._esm is not None
|
|
90
|
+
assert PerspectiveWidget._css is not None
|
|
91
|
+
|
|
92
|
+
def test_bundle_is_packaged(self):
|
|
93
|
+
esm = _STATIC / "perspective-anywidget.js"
|
|
94
|
+
css = _STATIC / "perspective-anywidget.css"
|
|
95
|
+
if not esm.exists():
|
|
96
|
+
pytest.skip("anywidget bundle not built in this environment")
|
|
97
|
+
assert esm.stat().st_size > 0
|
|
98
|
+
assert css.exists() and css.stat().st_size > 0
|
|
99
|
+
|
|
100
|
+
def test_proxy_session_lifecycle(self):
|
|
101
|
+
widget = PerspectiveWidget({"a": [1, 2, 3]})
|
|
102
|
+
widget.handle_message(widget, {"type": "connect", "client_id": "c1"}, [])
|
|
103
|
+
assert "c1" in widget._sessions
|
|
104
|
+
widget.handle_message(widget, {"type": "hangup", "client_id": "c1"}, [])
|
|
105
|
+
assert "c1" not in widget._sessions
|
|
106
|
+
|
|
107
|
+
def test_deferred_load_without_data(self):
|
|
108
|
+
widget = PerspectiveWidget(None, group_by=["a"])
|
|
109
|
+
assert widget.group_by == ["a"]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
from functools import partial
|
|
14
|
+
from types import MethodType
|
|
15
|
+
import numpy as np
|
|
16
|
+
from perspective.widget import PerspectiveWidget
|
|
17
|
+
from pytest import raises
|
|
18
|
+
import pytest
|
|
19
|
+
import perspective as psp
|
|
20
|
+
|
|
21
|
+
client = psp.Server().new_local_client()
|
|
22
|
+
Table = client.table
|
|
23
|
+
|
|
24
|
+
pytest.skip(allow_module_level=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def mock_post(self, msg, msg_id=None, assert_msg=None):
|
|
28
|
+
"""Mock the widget's `post()` method so we can introspect the contents."""
|
|
29
|
+
assert msg == assert_msg
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestWidget:
|
|
33
|
+
def test_widget(self):
|
|
34
|
+
data = {"a": np.arange(0, 50)}
|
|
35
|
+
widget = PerspectiveWidget(data, plugin="X Bar")
|
|
36
|
+
assert widget.plugin == "X Bar"
|
|
37
|
+
load_msg = widget._make_load_message()
|
|
38
|
+
assert load_msg.to_columns() == {
|
|
39
|
+
"id": -2,
|
|
40
|
+
"type": "table",
|
|
41
|
+
"data": {"table_name": widget.table_name, "options": {}},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def test_widget_indexed(self):
|
|
45
|
+
data = {"a": np.arange(0, 50)}
|
|
46
|
+
widget = PerspectiveWidget(data, plugin="X Bar", index="a")
|
|
47
|
+
assert widget.plugin == "X Bar"
|
|
48
|
+
load_msg = widget._make_load_message()
|
|
49
|
+
assert load_msg.to_columns() == {
|
|
50
|
+
"id": -2,
|
|
51
|
+
"type": "table",
|
|
52
|
+
"data": {"table_name": widget.table_name, "options": {"index": "a"}},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def test_widget_no_data(self):
|
|
56
|
+
widget = PerspectiveWidget(None, plugin="X Bar", group_by=["a"])
|
|
57
|
+
assert widget.plugin == "X Bar"
|
|
58
|
+
assert widget.group_by == ["a"]
|
|
59
|
+
|
|
60
|
+
def test_widget_schema(self):
|
|
61
|
+
schema = {
|
|
62
|
+
"a": "integer",
|
|
63
|
+
"b": "float",
|
|
64
|
+
"c": "boolean",
|
|
65
|
+
"d": "date",
|
|
66
|
+
"e": "datetime",
|
|
67
|
+
"f": "string",
|
|
68
|
+
}
|
|
69
|
+
widget = PerspectiveWidget(schema)
|
|
70
|
+
assert widget.table.schema() == schema
|
|
71
|
+
|
|
72
|
+
def test_widget_schema_with_index(self):
|
|
73
|
+
widget = PerspectiveWidget({"a": "integer"}, index="a")
|
|
74
|
+
assert widget.table.get_index() == "a"
|
|
75
|
+
|
|
76
|
+
def test_widget_schema_with_limit(self):
|
|
77
|
+
widget = PerspectiveWidget({"a": "integer"}, limit=5)
|
|
78
|
+
assert widget.table.get_limit() == 5
|
|
79
|
+
|
|
80
|
+
def test_widget_no_data_with_index(self):
|
|
81
|
+
# should fail
|
|
82
|
+
with raises(TypeError):
|
|
83
|
+
PerspectiveWidget(None, index="a")
|
|
84
|
+
|
|
85
|
+
def test_widget_no_data_with_limit(self):
|
|
86
|
+
# should fail
|
|
87
|
+
with raises(TypeError):
|
|
88
|
+
PerspectiveWidget(None, limit=5)
|
|
89
|
+
|
|
90
|
+
def test_widget_eventual_data(self):
|
|
91
|
+
table = Table({"a": np.arange(0, 50)})
|
|
92
|
+
widget = PerspectiveWidget(None, plugin="X Bar")
|
|
93
|
+
assert widget.plugin == "X Bar"
|
|
94
|
+
|
|
95
|
+
with raises(TypeError):
|
|
96
|
+
widget._make_load_message()
|
|
97
|
+
|
|
98
|
+
widget.load(table)
|
|
99
|
+
|
|
100
|
+
load_msg = widget._make_load_message()
|
|
101
|
+
assert load_msg.to_columns() == {
|
|
102
|
+
"id": -2,
|
|
103
|
+
"type": "table",
|
|
104
|
+
"data": {"table_name": widget.table_name, "options": {}},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def test_widget_eventual_data_server(self):
|
|
108
|
+
widget = PerspectiveWidget(None, plugin="X Bar", server=True)
|
|
109
|
+
assert widget.plugin == "X Bar"
|
|
110
|
+
widget.load({"a": np.arange(0, 50)}, index="a")
|
|
111
|
+
load_msg = widget._make_load_message()
|
|
112
|
+
assert load_msg.to_columns() == {
|
|
113
|
+
"id": -2,
|
|
114
|
+
"type": "table",
|
|
115
|
+
"data": {
|
|
116
|
+
"table_name": widget.table_name,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def test_widget_eventual_data_indexed(self):
|
|
121
|
+
widget = PerspectiveWidget(None, plugin="X Bar")
|
|
122
|
+
assert widget.plugin == "X Bar"
|
|
123
|
+
widget.load({"a": np.arange(0, 50)}, index="a")
|
|
124
|
+
load_msg = widget._make_load_message()
|
|
125
|
+
assert load_msg.to_columns() == {
|
|
126
|
+
"id": -2,
|
|
127
|
+
"type": "table",
|
|
128
|
+
"data": {"table_name": widget.table_name, "options": {"index": "a"}},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def test_widget_eventual_table_indexed(self):
|
|
132
|
+
table = Table({"a": np.arange(0, 50)}, index="a")
|
|
133
|
+
widget = PerspectiveWidget(None, plugin="X Bar")
|
|
134
|
+
assert widget.plugin == "X Bar"
|
|
135
|
+
widget.load(table)
|
|
136
|
+
load_msg = widget._make_load_message()
|
|
137
|
+
assert load_msg.to_columns() == {
|
|
138
|
+
"id": -2,
|
|
139
|
+
"type": "table",
|
|
140
|
+
"data": {"table_name": widget.table_name, "options": {"index": "a"}},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def test_widget_load_table(self):
|
|
144
|
+
table = Table({"a": np.arange(0, 50)})
|
|
145
|
+
widget = PerspectiveWidget(table, plugin="X Bar")
|
|
146
|
+
assert widget.plugin == "X Bar"
|
|
147
|
+
load_msg = widget._make_load_message()
|
|
148
|
+
assert load_msg.to_columns() == {
|
|
149
|
+
"id": -2,
|
|
150
|
+
"type": "table",
|
|
151
|
+
"data": {"table_name": widget.table_name, "options": {}},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def test_widget_load_table_indexed(self):
|
|
155
|
+
table = Table({"a": np.arange(0, 50)}, index="a")
|
|
156
|
+
widget = PerspectiveWidget(table, plugin="X Bar")
|
|
157
|
+
assert widget.plugin == "X Bar"
|
|
158
|
+
load_msg = widget._make_load_message()
|
|
159
|
+
assert load_msg.to_columns() == {
|
|
160
|
+
"id": -2,
|
|
161
|
+
"type": "table",
|
|
162
|
+
"data": {"table_name": widget.table_name, "options": {"index": "a"}},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def test_widget_load_table_ignore_limit(self):
|
|
166
|
+
table = Table({"a": np.arange(0, 50)})
|
|
167
|
+
widget = PerspectiveWidget(table, limit=1)
|
|
168
|
+
assert widget.table.size() == 50
|
|
169
|
+
|
|
170
|
+
def test_widget_pass_index(self):
|
|
171
|
+
data = {"a": [1, 2, 3, 1]}
|
|
172
|
+
widget = PerspectiveWidget(data, index="a")
|
|
173
|
+
assert widget.table.size() == 3
|
|
174
|
+
|
|
175
|
+
def test_widget_pass_limit(self):
|
|
176
|
+
data = {"a": np.arange(0, 50)}
|
|
177
|
+
widget = PerspectiveWidget(data, limit=1)
|
|
178
|
+
assert widget.table.size() == 1
|
|
179
|
+
|
|
180
|
+
def test_widget_pass_options_invalid(self):
|
|
181
|
+
data = {"a": np.arange(0, 50)}
|
|
182
|
+
with raises(TypeError):
|
|
183
|
+
PerspectiveWidget(data, index="index", limit=1)
|
|
184
|
+
|
|
185
|
+
# server mode
|
|
186
|
+
def test_widget_load_table_server(self):
|
|
187
|
+
table = Table({"a": np.arange(0, 50)})
|
|
188
|
+
widget = PerspectiveWidget(table, server=True)
|
|
189
|
+
load_msg = widget._make_load_message()
|
|
190
|
+
assert load_msg.to_columns() == {
|
|
191
|
+
"id": -2,
|
|
192
|
+
"type": "table",
|
|
193
|
+
"data": {"table_name": widget.table_name},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
def test_widget_no_data_with_server(self):
|
|
197
|
+
# should fail
|
|
198
|
+
widget = PerspectiveWidget(None, server=True)
|
|
199
|
+
with raises(TypeError):
|
|
200
|
+
widget._make_load_message()
|
|
201
|
+
|
|
202
|
+
def test_widget_eventual_data_with_server(self):
|
|
203
|
+
# should fail
|
|
204
|
+
widget = PerspectiveWidget(None, server=True)
|
|
205
|
+
|
|
206
|
+
with raises(TypeError):
|
|
207
|
+
widget._make_load_message()
|
|
208
|
+
|
|
209
|
+
# then succeed
|
|
210
|
+
widget.load(Table({"a": np.arange(0, 50)}))
|
|
211
|
+
load_msg = widget._make_load_message()
|
|
212
|
+
assert load_msg.to_columns() == {
|
|
213
|
+
"id": -2,
|
|
214
|
+
"type": "table",
|
|
215
|
+
"data": {"table_name": widget.table_name},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# clear
|
|
219
|
+
|
|
220
|
+
def test_widget_clear(self):
|
|
221
|
+
data = {"a": np.arange(0, 50)}
|
|
222
|
+
widget = PerspectiveWidget(data)
|
|
223
|
+
widget.clear()
|
|
224
|
+
assert widget.table.size() == 0
|
|
225
|
+
|
|
226
|
+
def test_widget_clear_server(self):
|
|
227
|
+
data = {"a": np.arange(0, 50)}
|
|
228
|
+
widget = PerspectiveWidget(data, server=True)
|
|
229
|
+
widget.clear()
|
|
230
|
+
assert widget.table.size() == 0
|
|
231
|
+
|
|
232
|
+
# replace
|
|
233
|
+
|
|
234
|
+
def test_widget_replace(self):
|
|
235
|
+
data = {"a": np.arange(0, 50)}
|
|
236
|
+
widget = PerspectiveWidget(data)
|
|
237
|
+
widget.replace({"a": [1]})
|
|
238
|
+
assert widget.table.size() == 1
|
|
239
|
+
|
|
240
|
+
def test_widget_replace_server(self):
|
|
241
|
+
data = {"a": np.arange(0, 50)}
|
|
242
|
+
widget = PerspectiveWidget(data, server=True)
|
|
243
|
+
widget.replace({"a": [1]})
|
|
244
|
+
assert widget.table.size() == 1
|
|
245
|
+
|
|
246
|
+
# delete
|
|
247
|
+
|
|
248
|
+
def test_widget_delete(self):
|
|
249
|
+
data = {"a": np.arange(0, 50)}
|
|
250
|
+
widget = PerspectiveWidget(data)
|
|
251
|
+
mocked_post = partial(mock_post, assert_msg={"cmd": "delete"})
|
|
252
|
+
widget.post = MethodType(mocked_post, widget)
|
|
253
|
+
widget.delete()
|
|
254
|
+
assert widget.table is None
|
|
255
|
+
|
|
256
|
+
def test_widget_delete_with_view(self):
|
|
257
|
+
data = {"a": np.arange(0, 50)}
|
|
258
|
+
widget = PerspectiveWidget(data)
|
|
259
|
+
|
|
260
|
+
# create a view on the manager
|
|
261
|
+
table_name, table = list(widget.manager._tables.items())[0]
|
|
262
|
+
make_view_message = {
|
|
263
|
+
"id": 1,
|
|
264
|
+
"table_name": table_name,
|
|
265
|
+
"view_name": "view1",
|
|
266
|
+
"cmd": "view",
|
|
267
|
+
"config": {"group_by": ["a"]},
|
|
268
|
+
}
|
|
269
|
+
widget.manager._process(make_view_message, lambda x: True)
|
|
270
|
+
|
|
271
|
+
assert len(widget.manager._views) == 1
|
|
272
|
+
|
|
273
|
+
mocked_post = partial(mock_post, assert_msg={"cmd": "delete"})
|
|
274
|
+
|
|
275
|
+
widget.post = MethodType(mocked_post, widget)
|
|
276
|
+
widget.delete()
|
|
277
|
+
|
|
278
|
+
assert widget.table is None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
# Regression tests for static HTML export, based on the repro in
|
|
14
|
+
# https://github.com/perspective-dev/perspective/issues/2846
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from perspective.widget import PerspectiveWidget, __version__
|
|
24
|
+
|
|
25
|
+
pytestmark = pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_widget():
|
|
29
|
+
df = pd.DataFrame(dict(x=[1.0, 2.0, 3.0], y=[1.0, 3.0, 2.0]))
|
|
30
|
+
return PerspectiveWidget(df)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def export_html(widget):
|
|
34
|
+
bundle = widget._repr_mimebundle_()
|
|
35
|
+
data = bundle[0] if isinstance(bundle, tuple) else bundle
|
|
36
|
+
return data["text/html"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestWidgetHtmlExport:
|
|
40
|
+
@pytest.fixture(autouse=True)
|
|
41
|
+
def _enable_export(self, monkeypatch):
|
|
42
|
+
monkeypatch.setenv("PSP_JUPYTER_HTML_EXPORT", "1")
|
|
43
|
+
|
|
44
|
+
def test_disabled_without_env(self, monkeypatch):
|
|
45
|
+
monkeypatch.delenv("PSP_JUPYTER_HTML_EXPORT")
|
|
46
|
+
bundle = make_widget()._repr_mimebundle_()
|
|
47
|
+
data = bundle[0] if isinstance(bundle, tuple) else bundle
|
|
48
|
+
assert data is None or "text/html" not in data
|
|
49
|
+
|
|
50
|
+
def test_bundle_preserves_anywidget_view(self):
|
|
51
|
+
bundle = make_widget()._repr_mimebundle_()
|
|
52
|
+
assert isinstance(bundle, tuple)
|
|
53
|
+
data, _metadata = bundle
|
|
54
|
+
assert "text/html" in data
|
|
55
|
+
assert "application/vnd.jupyter.widget-view+json" in data
|
|
56
|
+
|
|
57
|
+
def test_viewer_id_is_quoted(self):
|
|
58
|
+
widget = make_widget()
|
|
59
|
+
html = export_html(widget)
|
|
60
|
+
assert f'const viewerId = "{widget.model_id}";' in html
|
|
61
|
+
assert f'id="perspective-envelope-{widget.model_id}"' in html
|
|
62
|
+
|
|
63
|
+
def test_viewer_attrs_is_json(self):
|
|
64
|
+
widget = make_widget()
|
|
65
|
+
html = export_html(widget)
|
|
66
|
+
match = re.search(r"const viewerAttrs = (\{.*?\});", html, re.S)
|
|
67
|
+
assert match is not None
|
|
68
|
+
attrs = json.loads(match.group(1))
|
|
69
|
+
assert attrs["columns"] == ["index", "x", "y"]
|
|
70
|
+
assert attrs == json.loads(json.dumps(widget.save()))
|
|
71
|
+
|
|
72
|
+
def test_worker_is_awaited_before_table(self):
|
|
73
|
+
html = export_html(make_widget())
|
|
74
|
+
assert 'await customElements.whenDefined("perspective-viewer");' in html
|
|
75
|
+
assert "const client = await perspective.worker();" in html
|
|
76
|
+
assert "const table = await client.table(data.buffer);" in html
|
|
77
|
+
assert "perspective.worker().table" not in html
|
|
78
|
+
|
|
79
|
+
def test_cdn_urls_use_5x_package_names(self):
|
|
80
|
+
html = export_html(make_widget())
|
|
81
|
+
urls = re.findall(r'(?:src|href)="([^"]+)"', html)
|
|
82
|
+
prefix = f"https://cdn.jsdelivr.net/npm/@perspective-dev"
|
|
83
|
+
assert urls == [
|
|
84
|
+
f"{prefix}/client@{__version__}/dist/cdn/perspective.js",
|
|
85
|
+
f"{prefix}/viewer@{__version__}/dist/cdn/perspective-viewer.js",
|
|
86
|
+
f"{prefix}/viewer-datagrid@{__version__}/dist/cdn/perspective-viewer-datagrid.js",
|
|
87
|
+
f"{prefix}/viewer-charts@{__version__}/dist/cdn/perspective-viewer-charts.js",
|
|
88
|
+
f"{prefix}/viewer@{__version__}/dist/css/themes.css",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def test_arrow_payload_round_trips(self):
|
|
92
|
+
widget = make_widget()
|
|
93
|
+
html = export_html(widget)
|
|
94
|
+
match = re.search(
|
|
95
|
+
r'<script type="application/vnd.apache.arrow.file">(.*?)</script>',
|
|
96
|
+
html,
|
|
97
|
+
re.S,
|
|
98
|
+
)
|
|
99
|
+
assert match is not None
|
|
100
|
+
data = base64.b64decode("".join(match.group(1).split()))
|
|
101
|
+
table = widget.table.get_client().table(data)
|
|
102
|
+
try:
|
|
103
|
+
view = table.view()
|
|
104
|
+
try:
|
|
105
|
+
assert view.to_columns() == {
|
|
106
|
+
"index": [0, 1, 2],
|
|
107
|
+
"x": [1.0, 2.0, 3.0],
|
|
108
|
+
"y": [1.0, 3.0, 2.0],
|
|
109
|
+
}
|
|
110
|
+
finally:
|
|
111
|
+
view.delete()
|
|
112
|
+
finally:
|
|
113
|
+
table.delete()
|