perspective-python 4.5.2__cp311-abi3-pyemscripten_2025_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 +35 -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/conftest.py +263 -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_multi_threaded.py +201 -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 +89 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_exception.py +65 -0
- perspective/tests/table/test_join.py +115 -0
- perspective/tests/table/test_leaks.py +105 -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_datetime.py +2409 -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 +417 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_format.py +1024 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +536 -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 +2444 -0
- perspective/tests/table/test_view_expression.py +1940 -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 +209 -0
- perspective/tests/virtual_servers/test_duckdb.py +908 -0
- perspective/tests/virtual_servers/test_polars.py +1032 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +142 -0
- perspective/virtual_servers/clickhouse.py +240 -0
- perspective/virtual_servers/duckdb.py +236 -0
- perspective/virtual_servers/polars.py +710 -0
- perspective/widget/__init__.py +349 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/validate.py +22 -0
- perspective/widget/viewer/viewer.py +343 -0
- perspective/widget/viewer/viewer_traitlets.py +101 -0
- perspective_python-4.5.2.dist-info/METADATA +29 -0
- perspective_python-4.5.2.dist-info/RECORD +79 -0
- perspective_python-4.5.2.dist-info/WHEEL +4 -0
- perspective_python-4.5.2.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-4.5.2.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +19255 -0
- perspective_python-4.5.2.dist-info/sboms/perspective-python.cyclonedx.json +5546 -0
|
@@ -0,0 +1,124 @@
|
|
|
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 perspective as psp
|
|
14
|
+
|
|
15
|
+
client = psp.Server().new_local_client()
|
|
16
|
+
Table = client.table
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestDelete(object):
|
|
20
|
+
# delete
|
|
21
|
+
|
|
22
|
+
def test_table_delete(self):
|
|
23
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
24
|
+
tbl = Table(data)
|
|
25
|
+
tbl.delete()
|
|
26
|
+
# don't segfault
|
|
27
|
+
|
|
28
|
+
def test_table_delete_callback(self, sentinel):
|
|
29
|
+
s = sentinel(False)
|
|
30
|
+
|
|
31
|
+
def callback():
|
|
32
|
+
s.set(True)
|
|
33
|
+
|
|
34
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
35
|
+
tbl = Table(data)
|
|
36
|
+
tbl.on_delete(callback)
|
|
37
|
+
tbl.delete()
|
|
38
|
+
assert s.get() is True
|
|
39
|
+
|
|
40
|
+
def test_table_delete_with_view(self, sentinel):
|
|
41
|
+
s = sentinel(False)
|
|
42
|
+
|
|
43
|
+
def callback():
|
|
44
|
+
s.set(True)
|
|
45
|
+
|
|
46
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
47
|
+
tbl = Table(data)
|
|
48
|
+
tbl.on_delete(callback)
|
|
49
|
+
view = tbl.view()
|
|
50
|
+
view.delete()
|
|
51
|
+
tbl.delete()
|
|
52
|
+
assert s.get() is True
|
|
53
|
+
|
|
54
|
+
def test_table_delete_multiple_callback(self, sentinel):
|
|
55
|
+
s1 = sentinel(False)
|
|
56
|
+
s2 = sentinel(False)
|
|
57
|
+
|
|
58
|
+
def callback1():
|
|
59
|
+
s1.set(True)
|
|
60
|
+
|
|
61
|
+
def callback2():
|
|
62
|
+
s2.set(True)
|
|
63
|
+
|
|
64
|
+
tbl = Table([{"a": 1}])
|
|
65
|
+
tbl.on_delete(callback1)
|
|
66
|
+
tbl.on_delete(callback2)
|
|
67
|
+
|
|
68
|
+
tbl.delete()
|
|
69
|
+
|
|
70
|
+
assert s1.get() is True
|
|
71
|
+
assert s2.get() is True
|
|
72
|
+
|
|
73
|
+
def test_table_remove_delete_callback(self, sentinel):
|
|
74
|
+
s = sentinel(False)
|
|
75
|
+
|
|
76
|
+
def callback():
|
|
77
|
+
s.set(True)
|
|
78
|
+
|
|
79
|
+
tbl = Table([{"a": 1}])
|
|
80
|
+
callback_id = tbl.on_delete(callback)
|
|
81
|
+
tbl.remove_delete(callback_id)
|
|
82
|
+
|
|
83
|
+
tbl.delete()
|
|
84
|
+
|
|
85
|
+
assert s.get() is False
|
|
86
|
+
|
|
87
|
+
def test_view_delete_multiple_callback(self, sentinel):
|
|
88
|
+
s1 = sentinel(False)
|
|
89
|
+
s2 = sentinel(False)
|
|
90
|
+
|
|
91
|
+
def callback1():
|
|
92
|
+
s1.set(True)
|
|
93
|
+
|
|
94
|
+
def callback2():
|
|
95
|
+
s2.set(True)
|
|
96
|
+
|
|
97
|
+
tbl = Table([{"a": 1}])
|
|
98
|
+
view = tbl.view()
|
|
99
|
+
|
|
100
|
+
view.on_delete(callback1)
|
|
101
|
+
view.on_delete(callback2)
|
|
102
|
+
|
|
103
|
+
view.delete()
|
|
104
|
+
tbl.delete()
|
|
105
|
+
|
|
106
|
+
assert s1.get() is True
|
|
107
|
+
assert s2.get() is True
|
|
108
|
+
|
|
109
|
+
def test_view_remove_delete_callback(self, sentinel):
|
|
110
|
+
s = sentinel(False)
|
|
111
|
+
|
|
112
|
+
def callback():
|
|
113
|
+
s.set(True)
|
|
114
|
+
|
|
115
|
+
tbl = Table([{"a": 1}])
|
|
116
|
+
view = tbl.view()
|
|
117
|
+
|
|
118
|
+
callback_id = view.on_delete(callback)
|
|
119
|
+
view.remove_delete(callback_id)
|
|
120
|
+
|
|
121
|
+
view.delete()
|
|
122
|
+
tbl.delete()
|
|
123
|
+
|
|
124
|
+
assert s.get() is False
|
|
@@ -0,0 +1,65 @@
|
|
|
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 pytest import raises
|
|
14
|
+
from perspective import PerspectiveError
|
|
15
|
+
|
|
16
|
+
import perspective as psp
|
|
17
|
+
|
|
18
|
+
client = psp.Server().new_local_client()
|
|
19
|
+
Table = client.table
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestException(object):
|
|
23
|
+
def test_instantiation_exception_table(self):
|
|
24
|
+
from perspective import Table
|
|
25
|
+
with raises(TypeError) as ex:
|
|
26
|
+
Table()
|
|
27
|
+
assert "Do not call Table's constructor directly" in str(ex.value)
|
|
28
|
+
|
|
29
|
+
def test_instantiation_exception_view(self):
|
|
30
|
+
from perspective import View
|
|
31
|
+
with raises(TypeError) as ex:
|
|
32
|
+
View()
|
|
33
|
+
assert "Do not call View's constructor directly" in str(ex.value)
|
|
34
|
+
|
|
35
|
+
def test_exception_from_core(self):
|
|
36
|
+
tbl = Table({"a": [1, 2, 3]})
|
|
37
|
+
|
|
38
|
+
with raises(PerspectiveError) as ex:
|
|
39
|
+
# creating view with unknown column should throw
|
|
40
|
+
tbl.view(group_by=["b"])
|
|
41
|
+
|
|
42
|
+
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
|
|
43
|
+
|
|
44
|
+
def test_exception_from_core_catch_generic(self):
|
|
45
|
+
tbl = Table({"a": [1, 2, 3]})
|
|
46
|
+
# `PerspectiveCppError` should inherit from `Exception`
|
|
47
|
+
with raises(Exception) as ex:
|
|
48
|
+
tbl.view(group_by=["b"])
|
|
49
|
+
|
|
50
|
+
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
|
|
51
|
+
|
|
52
|
+
def test_exception_from_core_correct_types(self):
|
|
53
|
+
tbl = Table({"a": [1, 2, 3]})
|
|
54
|
+
|
|
55
|
+
# `PerspectiveError` should be raised from the Python layer
|
|
56
|
+
with raises(PerspectiveError) as ex:
|
|
57
|
+
tbl.view()
|
|
58
|
+
tbl.delete()
|
|
59
|
+
|
|
60
|
+
assert str(ex.value) == "Abort(): Cannot delete table with views"
|
|
61
|
+
|
|
62
|
+
with raises(PerspectiveError) as ex:
|
|
63
|
+
tbl.view(group_by=["b"])
|
|
64
|
+
|
|
65
|
+
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
|
|
@@ -0,0 +1,115 @@
|
|
|
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 perspective as psp
|
|
14
|
+
|
|
15
|
+
client = psp.Server().new_local_client()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestJoin:
|
|
19
|
+
def test_inner_join_two_tables(self):
|
|
20
|
+
left = client.table(
|
|
21
|
+
[{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
|
|
22
|
+
)
|
|
23
|
+
right = client.table(
|
|
24
|
+
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}, {"id": 4, "y": "d"}]
|
|
25
|
+
)
|
|
26
|
+
joined = client.join(left, right, "id")
|
|
27
|
+
view = joined.view()
|
|
28
|
+
json = view.to_json()
|
|
29
|
+
assert len(json) == 2
|
|
30
|
+
view.delete()
|
|
31
|
+
joined.delete()
|
|
32
|
+
right.delete()
|
|
33
|
+
left.delete()
|
|
34
|
+
|
|
35
|
+
def test_join_has_correct_schema(self):
|
|
36
|
+
left = client.table({"id": "integer", "x": "float"})
|
|
37
|
+
right = client.table({"id": "integer", "y": "string"})
|
|
38
|
+
joined = client.join(left, right, "id")
|
|
39
|
+
schema = joined.schema()
|
|
40
|
+
assert schema == {"id": "integer", "x": "float", "y": "string"}
|
|
41
|
+
joined.delete()
|
|
42
|
+
right.delete()
|
|
43
|
+
left.delete()
|
|
44
|
+
|
|
45
|
+
def test_join_reacts_to_left_updates(self):
|
|
46
|
+
left = client.table(
|
|
47
|
+
[{"id": 1, "x": 10}, {"id": 2, "x": 20}]
|
|
48
|
+
)
|
|
49
|
+
right = client.table(
|
|
50
|
+
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
|
|
51
|
+
)
|
|
52
|
+
joined = client.join(left, right, "id")
|
|
53
|
+
view = joined.view()
|
|
54
|
+
left.update([{"id": 1, "x": 99}])
|
|
55
|
+
json = view.to_json()
|
|
56
|
+
assert json == [
|
|
57
|
+
{"id": 1, "x": 10, "y": "a"},
|
|
58
|
+
{"id": 2, "x": 20, "y": "b"},
|
|
59
|
+
{"id": 1, "x": 99, "y": "a"},
|
|
60
|
+
]
|
|
61
|
+
view.delete()
|
|
62
|
+
joined.delete()
|
|
63
|
+
right.delete()
|
|
64
|
+
left.delete()
|
|
65
|
+
|
|
66
|
+
def test_left_join(self):
|
|
67
|
+
left = client.table(
|
|
68
|
+
[{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
|
|
69
|
+
)
|
|
70
|
+
right = client.table(
|
|
71
|
+
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
|
|
72
|
+
)
|
|
73
|
+
joined = client.join(left, right, "id", "left")
|
|
74
|
+
view = joined.view()
|
|
75
|
+
json = view.to_json()
|
|
76
|
+
assert len(json) == 3
|
|
77
|
+
view.delete()
|
|
78
|
+
joined.delete()
|
|
79
|
+
right.delete()
|
|
80
|
+
left.delete()
|
|
81
|
+
|
|
82
|
+
def test_join_by_table_names(self):
|
|
83
|
+
left = client.table(
|
|
84
|
+
[{"id": 1, "x": 10}, {"id": 2, "x": 20}],
|
|
85
|
+
name="left_py",
|
|
86
|
+
)
|
|
87
|
+
right = client.table(
|
|
88
|
+
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
|
|
89
|
+
name="right_py",
|
|
90
|
+
)
|
|
91
|
+
joined = client.join("left_py", "right_py", "id")
|
|
92
|
+
view = joined.view()
|
|
93
|
+
json = view.to_json()
|
|
94
|
+
assert len(json) == 2
|
|
95
|
+
view.delete()
|
|
96
|
+
joined.delete()
|
|
97
|
+
right.delete()
|
|
98
|
+
left.delete()
|
|
99
|
+
|
|
100
|
+
def test_join_mixed_table_and_string(self):
|
|
101
|
+
left = client.table(
|
|
102
|
+
[{"id": 1, "x": 10}, {"id": 2, "x": 20}]
|
|
103
|
+
)
|
|
104
|
+
right = client.table(
|
|
105
|
+
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
|
|
106
|
+
name="right_py_mixed",
|
|
107
|
+
)
|
|
108
|
+
joined = client.join(left, "right_py_mixed", "id")
|
|
109
|
+
view = joined.view()
|
|
110
|
+
json = view.to_json()
|
|
111
|
+
assert len(json) == 2
|
|
112
|
+
view.delete()
|
|
113
|
+
joined.delete()
|
|
114
|
+
right.delete()
|
|
115
|
+
left.delete()
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
|
|
14
|
+
import psutil
|
|
15
|
+
import os
|
|
16
|
+
import perspective as psp
|
|
17
|
+
|
|
18
|
+
client = psp.Server().new_local_client()
|
|
19
|
+
Table = client.table
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestDelete(object):
|
|
23
|
+
# delete
|
|
24
|
+
|
|
25
|
+
def test_table_delete(self):
|
|
26
|
+
process = psutil.Process(os.getpid())
|
|
27
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
28
|
+
tbl = Table(data)
|
|
29
|
+
tbl.delete()
|
|
30
|
+
mem = process.memory_info().rss
|
|
31
|
+
|
|
32
|
+
for x in range(10000):
|
|
33
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
34
|
+
tbl = Table(data)
|
|
35
|
+
tbl.delete()
|
|
36
|
+
|
|
37
|
+
mem2 = process.memory_info().rss
|
|
38
|
+
|
|
39
|
+
# assert 1 < (max2 / max) < 1.01
|
|
40
|
+
assert (mem2 - mem) < 2000000
|
|
41
|
+
|
|
42
|
+
def test_table_delete_with_view(self, sentinel):
|
|
43
|
+
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
|
|
44
|
+
tbl = Table(data)
|
|
45
|
+
|
|
46
|
+
process = psutil.Process(os.getpid())
|
|
47
|
+
mem = process.memory_info().rss
|
|
48
|
+
for x in range(10000):
|
|
49
|
+
view = tbl.view()
|
|
50
|
+
view.delete()
|
|
51
|
+
|
|
52
|
+
tbl.delete()
|
|
53
|
+
mem2 = process.memory_info().rss
|
|
54
|
+
assert (mem2 - mem) < 2000000
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestExpressionVocab(object):
|
|
58
|
+
# Regression tests for the `t_expression_vocab` leak fixed in PR #3175.
|
|
59
|
+
# Pre-fix, updates against a view with a string-producing expression
|
|
60
|
+
# interned each result into a new 4KB vocab page once the prior page
|
|
61
|
+
# filled, and never deduplicated against older pages. RSS grew without
|
|
62
|
+
# bound. The fix adds cross-page deduplication; these tests assert the
|
|
63
|
+
# steady state is flat.
|
|
64
|
+
|
|
65
|
+
def test_string_expression_update_no_leak(self):
|
|
66
|
+
long_literal = "X" * 256
|
|
67
|
+
tbl = Table({"x": "integer", "c": "string"}, index="x")
|
|
68
|
+
view = tbl.view(expressions={"e": 'concat("c", \'' + long_literal + "')"})
|
|
69
|
+
|
|
70
|
+
for _ in range(100):
|
|
71
|
+
tbl.update([{"x": 1, "c": "value"}])
|
|
72
|
+
|
|
73
|
+
process = psutil.Process(os.getpid())
|
|
74
|
+
mem = process.memory_info().rss
|
|
75
|
+
for _ in range(5000):
|
|
76
|
+
tbl.update([{"x": 1, "c": "value"}])
|
|
77
|
+
mem2 = process.memory_info().rss
|
|
78
|
+
|
|
79
|
+
view.delete()
|
|
80
|
+
tbl.delete()
|
|
81
|
+
|
|
82
|
+
assert (mem2 - mem) < 2000000
|
|
83
|
+
|
|
84
|
+
def test_string_expression_update_bounded_vocab_no_leak(self):
|
|
85
|
+
# Cycles through a small fixed set of distinct values. Exercises the
|
|
86
|
+
# cross-page `string_exists` lookup the fix relies on: once the
|
|
87
|
+
# vocabulary is fully populated, subsequent interns must resolve to
|
|
88
|
+
# existing pointers rather than allocating new ones.
|
|
89
|
+
values = ["alpha", "bravo", "charlie", "delta", "echo"]
|
|
90
|
+
tbl = Table({"x": "integer", "c": "string"}, index="x")
|
|
91
|
+
view = tbl.view(expressions={"e": 'upper("c")'})
|
|
92
|
+
|
|
93
|
+
for i in range(100):
|
|
94
|
+
tbl.update([{"x": 1, "c": values[i % len(values)]}])
|
|
95
|
+
|
|
96
|
+
process = psutil.Process(os.getpid())
|
|
97
|
+
mem = process.memory_info().rss
|
|
98
|
+
for i in range(5000):
|
|
99
|
+
tbl.update([{"x": 1, "c": values[i % len(values)]}])
|
|
100
|
+
mem2 = process.memory_info().rss
|
|
101
|
+
|
|
102
|
+
view.delete()
|
|
103
|
+
tbl.delete()
|
|
104
|
+
|
|
105
|
+
assert (mem2 - mem) < 2000000
|
|
@@ -0,0 +1,178 @@
|
|
|
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 random
|
|
14
|
+
import perspective as psp
|
|
15
|
+
|
|
16
|
+
client = psp.Server().new_local_client()
|
|
17
|
+
Table = client.table
|
|
18
|
+
|
|
19
|
+
data = {"a": [1, 2, 3, 4], "b": ["a", "b", "c", "d"], "c": [True, False, True, False]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestPorts(object):
|
|
23
|
+
def test_make_port_sequential(self):
|
|
24
|
+
table = Table(data)
|
|
25
|
+
port_ids = []
|
|
26
|
+
|
|
27
|
+
for i in range(10):
|
|
28
|
+
port_ids.append(table.make_port())
|
|
29
|
+
|
|
30
|
+
assert port_ids == list(range(1, 11))
|
|
31
|
+
|
|
32
|
+
def test_make_port_sequential_and_update(self):
|
|
33
|
+
table = Table(data)
|
|
34
|
+
port_ids = []
|
|
35
|
+
|
|
36
|
+
for i in range(10):
|
|
37
|
+
port_ids.append(table.make_port())
|
|
38
|
+
|
|
39
|
+
assert port_ids == list(range(1, 11))
|
|
40
|
+
|
|
41
|
+
for i in range(1, 11):
|
|
42
|
+
table.update({"a": [i], "b": ["a"], "c": [True]}, port_id=i)
|
|
43
|
+
|
|
44
|
+
view = table.view()
|
|
45
|
+
result = view.to_columns()
|
|
46
|
+
|
|
47
|
+
assert result == {
|
|
48
|
+
"a": [1, 2, 3, 4] + [i for i in range(1, 11)],
|
|
49
|
+
"b": ["a", "b", "c", "d"] + ["a" for i in range(10)],
|
|
50
|
+
"c": [True, False, True, False] + [True for i in range(10)],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def test_arbitary_port_updates(self):
|
|
54
|
+
table = Table(data)
|
|
55
|
+
port_ids = []
|
|
56
|
+
|
|
57
|
+
for i in range(10):
|
|
58
|
+
port_ids.append(table.make_port())
|
|
59
|
+
|
|
60
|
+
assert port_ids == list(range(1, 11))
|
|
61
|
+
|
|
62
|
+
port = random.randint(0, 10)
|
|
63
|
+
|
|
64
|
+
table.update(data, port_id=port)
|
|
65
|
+
|
|
66
|
+
assert table.size() == 8
|
|
67
|
+
|
|
68
|
+
assert table.view().to_columns() == {
|
|
69
|
+
"a": [1, 2, 3, 4] * 2,
|
|
70
|
+
"b": ["a", "b", "c", "d"] * 2,
|
|
71
|
+
"c": [True, False, True, False] * 2,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def test_ports_should_only_notify_if_they_have_a_queued_update(self):
|
|
75
|
+
table = Table(data)
|
|
76
|
+
port_ids = []
|
|
77
|
+
|
|
78
|
+
for i in range(10):
|
|
79
|
+
port_ids.append(table.make_port())
|
|
80
|
+
|
|
81
|
+
assert port_ids == list(range(1, 11))
|
|
82
|
+
|
|
83
|
+
view = table.view()
|
|
84
|
+
ports_to_update = [random.randint(0, 10) for i in range(5)]
|
|
85
|
+
|
|
86
|
+
def callback(port_id):
|
|
87
|
+
assert port_id in ports_to_update
|
|
88
|
+
|
|
89
|
+
view.on_update(callback)
|
|
90
|
+
|
|
91
|
+
for port in ports_to_update:
|
|
92
|
+
table.update(data, port_id=port)
|
|
93
|
+
|
|
94
|
+
def test_ports_should_have_unique_deltas(self):
|
|
95
|
+
table = Table(data)
|
|
96
|
+
port_ids = []
|
|
97
|
+
|
|
98
|
+
for i in range(10):
|
|
99
|
+
port_ids.append(table.make_port())
|
|
100
|
+
|
|
101
|
+
assert port_ids == list(range(1, 11))
|
|
102
|
+
|
|
103
|
+
view = table.view()
|
|
104
|
+
ports_to_update = [random.randint(0, 10) for i in range(5)]
|
|
105
|
+
unique_data = {
|
|
106
|
+
port: [{"a": port, "b": str(port), "c": True}] for port in ports_to_update
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def callback(port_id, delta):
|
|
110
|
+
assert port_id in ports_to_update
|
|
111
|
+
_t = Table(delta)
|
|
112
|
+
_v = _t.view()
|
|
113
|
+
assert _v.to_records() == unique_data[port]
|
|
114
|
+
_v.delete()
|
|
115
|
+
_t.delete()
|
|
116
|
+
|
|
117
|
+
view.on_update(callback, mode="row")
|
|
118
|
+
|
|
119
|
+
for port in ports_to_update:
|
|
120
|
+
table.update(unique_data[port], port_id=port)
|
|
121
|
+
|
|
122
|
+
def test_ports_should_queue_updates_properly(self):
|
|
123
|
+
table = Table(data)
|
|
124
|
+
port_ids = []
|
|
125
|
+
|
|
126
|
+
for i in range(10):
|
|
127
|
+
port_ids.append(table.make_port())
|
|
128
|
+
|
|
129
|
+
assert port_ids == list(range(1, 11))
|
|
130
|
+
|
|
131
|
+
view = table.view()
|
|
132
|
+
ports_to_update = [random.randint(0, 10) for i in range(5)]
|
|
133
|
+
|
|
134
|
+
def callback(port_id):
|
|
135
|
+
assert port_id in ports_to_update
|
|
136
|
+
|
|
137
|
+
view.on_update(callback)
|
|
138
|
+
|
|
139
|
+
for port in ports_to_update:
|
|
140
|
+
table.update(data, port_id=port)
|
|
141
|
+
|
|
142
|
+
def test_ports_multiple_tables_with_different_ports(self):
|
|
143
|
+
server = Table(data)
|
|
144
|
+
client = Table(data)
|
|
145
|
+
|
|
146
|
+
for i in range(random.randint(5, 15)):
|
|
147
|
+
# reserve an arbitary number of ports
|
|
148
|
+
server.make_port()
|
|
149
|
+
|
|
150
|
+
# port for client is now far above the ports "ON" the client, as the
|
|
151
|
+
# client ports will begin creation at 1.
|
|
152
|
+
server_port_for_client = server.make_port()
|
|
153
|
+
client_port = client.make_port()
|
|
154
|
+
|
|
155
|
+
server_view = server.view()
|
|
156
|
+
client_view = client.view()
|
|
157
|
+
|
|
158
|
+
# when the client updates, check whether the port id matches that
|
|
159
|
+
# of the server, and complete the test.
|
|
160
|
+
def client_callback(port_id, delta):
|
|
161
|
+
if port_id == client_port:
|
|
162
|
+
print("UPDATING SERVER")
|
|
163
|
+
server.update(delta, port_id=server_port_for_client)
|
|
164
|
+
|
|
165
|
+
# when the server updates, pass the update back to the client
|
|
166
|
+
def server_callback(port_id, delta):
|
|
167
|
+
print("UPDATING CLIENT")
|
|
168
|
+
assert port_id == server_port_for_client
|
|
169
|
+
assert server.size() == 8
|
|
170
|
+
server_view.delete()
|
|
171
|
+
server.delete()
|
|
172
|
+
client_view.delete()
|
|
173
|
+
client.delete()
|
|
174
|
+
|
|
175
|
+
client_view.on_update(client_callback, mode="row")
|
|
176
|
+
server_view.on_update(server_callback, mode="row")
|
|
177
|
+
|
|
178
|
+
client.update(data, port_id=client_port)
|