chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
@@ -0,0 +1,11 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
@@ -0,0 +1,402 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import sys
from random import randint, choice
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class CustomObjectStore(object):
def __init__(self, value):
self._value = value
def _psp_dtype_(self):
return "object"
def __int__(self):
return int(self._value)
def __repr__(self):
return "test" if self._value == 1 else "test{}".format(self._value)
def run():
t = CustomObjectStore(1)
t2 = CustomObjectStore(2)
assert sys.getrefcount(t) == 2
assert sys.getrefcount(t2) == 2
data = {"a": [0], "b": [t]}
assert sys.getrefcount(t) == 3
tbl = Table(data, index="a")
assert sys.getrefcount(t) == 4
assert tbl.schema() == {"a": int, "b": object}
assert tbl.size() == 1
assert tbl.view().to_columns() == {"a": [0], "b": [t]}
# Count references
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
print("t:", id(t))
assert sys.getrefcount(t) == 4
print(sys.getrefcount(t2), "should be", 2)
print("t2:", id(t2))
assert sys.getrefcount(t2) == 2
tbl.update([{"a": i, "b": None} for i in range(1, 6)])
assert tbl.size() == 6
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
assert sys.getrefcount(t) == 4
print()
tbl.update([data])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
assert sys.getrefcount(t) == 4
print()
tbl.update([data])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
assert sys.getrefcount(t) == 4
print()
tbl.update([data])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
assert sys.getrefcount(t) == 4
print()
tbl.update([{"a": 1, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 2 for the table
print(sys.getrefcount(t), "should be", 5)
assert sys.getrefcount(t) == 5
print()
tbl.update([{"a": 1, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 2 for the table
print(sys.getrefcount(t), "should be", 5)
assert sys.getrefcount(t) == 5
print()
tbl.update([{"a": 3, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
assert sys.getrefcount(t) == 6
print()
tbl.update([{"a": 3, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
assert sys.getrefcount(t) == 6
print()
tbl.update([{"a": 5, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 4for the table
print(sys.getrefcount(t), "should be", 7)
# assert sys.getrefcount(t) == 7
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
# clear some, overwrite some with same
tbl.update([{"a": 0, "b": t}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 4 for the table
print(sys.getrefcount(t), "should be", 7)
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 1, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 1, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 1, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 1, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 2, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 4)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
True,
True,
False,
True,
]
print()
tbl.update([{"a": 2, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 2, "b": t2}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 4)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
True,
True,
False,
True,
]
print()
tbl.update([{"a": 2, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 3 for the table
print(sys.getrefcount(t), "should be", 6)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
True,
False,
True,
]
print()
tbl.update([{"a": 3, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 2 for the table
print(sys.getrefcount(t), "should be", 5)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
False,
False,
True,
]
print()
tbl.update([{"a": 3, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 2 for the table
print(sys.getrefcount(t), "should be", 5)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
False,
False,
True,
]
print()
tbl.update([{"a": 5, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
False,
False,
False,
]
print()
tbl.update([{"a": 5, "b": None}])
# 1 for `t`, 1 for `data`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t), "should be", 4)
# 1 for `t2`, 1 for argument to sys.getrefcount, and 1 for the table
print(sys.getrefcount(t2), "should be", 3)
print(tbl.view().to_columns()["b"])
assert list(_ is not None for _ in tbl.view().to_columns()["b"]) == [
True,
True,
False,
False,
False,
False,
]
print()
tbl.clear()
assert tbl.size() == 0
assert tbl.view().to_columns() == {}
# 1 for `t`, one for `data`, one for argument to sys.getrefcount
print(sys.getrefcount(t), "should be", 3)
assert sys.getrefcount(t) == 3
def run2():
t = CustomObjectStore(1)
t_ref_count = 2
assert sys.getrefcount(t) == t_ref_count
indexes = set([0])
tbl = Table({"a": [0], "b": [t]}, index="a")
assert sys.getrefcount(t) == 3
t_ref_count += 1
assert tbl.schema() == {"a": int, "b": object}
assert tbl.size() == 1
assert tbl.view().to_columns() == {"a": [0], "b": [t]}
# seed a few to check
tbl.remove([1])
tbl.remove([1])
tbl.remove([1])
for _ in range(10):
pick = randint(1, 2) if indexes else 1
if pick == 1:
ind = randint(1, 10)
while ind in indexes:
ind = randint(1, 100)
print(
"adding", ind, "refcount", t_ref_count, "should be", sys.getrefcount(t)
)
tbl.update({"a": [ind], "b": [t]})
t_ref_count += 1
indexes.add(ind)
assert sys.getrefcount(t) == t_ref_count
else:
ind = choice(list(indexes))
indexes.remove(ind)
tbl.remove([ind])
t_ref_count -= 1
print(
"removing",
ind,
"refcount",
t_ref_count,
"should be",
sys.getrefcount(t),
)
assert sys.getrefcount(t) == t_ref_count
print(t_ref_count)
print(tbl.view().to_columns())
assert sys.getrefcount(t) == t_ref_count
print()
tbl.clear()
assert tbl.size() == 0
assert tbl.view().to_columns() == {}
# 1 for `t`, one for `data`, one for argument to sys.getrefcount
print(sys.getrefcount(t), "should be", 2)
assert sys.getrefcount(t) == 2
@@ -0,0 +1,89 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
class TestViewColumnPaths(object):
def test_column_paths(self, superstore):
tbl = client.table(superstore)
view = tbl.view()
paths = view.column_paths()
assert paths == [
"index",
"Row ID",
"Order ID",
"Order Date",
"Ship Date",
"Ship Mode",
"Customer ID",
"Segment",
"Country",
"City",
"State",
"Postal Code",
"Region",
"Product ID",
"Category",
"Sub-Category",
"Sales",
"Quantity",
"Discount",
"Profit",
]
view.delete()
tbl.delete()
def test_column_paths_split_by(self, superstore):
tbl = client.table(superstore)
view = tbl.view(group_by=["State"], columns=["Sales"], split_by=["Ship Mode"])
paths = view.column_paths()
assert paths == [
"First Class|Sales",
"Second Class|Sales",
"Standard Class|Sales",
]
view.delete()
tbl.delete()
def test_column_paths_split_by_range(self, superstore):
tbl = client.table(superstore)
view = tbl.view(group_by=["State"], columns=["Sales"], split_by=["Ship Mode"])
paths = view.column_paths(start_col=1)
assert paths == [
"Second Class|Sales",
"Standard Class|Sales",
]
view.delete()
view = tbl.view(group_by=["State"], columns=["Sales"], split_by=["Ship Mode"])
paths = view.column_paths(start_col=1, end_col=2)
assert paths == [
"Second Class|Sales",
"Standard Class|Sales",
]
view.delete()
tbl = client.table(superstore)
view = tbl.view(group_by=["State"], columns=["Sales"], split_by=["Ship Mode"])
paths = view.column_paths(end_col=1)
assert paths == [
"First Class|Sales",
"Second Class|Sales",
]
view.delete()
tbl.delete()
@@ -0,0 +1,124 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestDelete(object):
# delete
def test_table_delete(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.delete()
# don't segfault
def test_table_delete_callback(self, sentinel):
s = sentinel(False)
def callback():
s.set(True)
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.on_delete(callback)
tbl.delete()
assert s.get() is True
def test_table_delete_with_view(self, sentinel):
s = sentinel(False)
def callback():
s.set(True)
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.on_delete(callback)
view = tbl.view()
view.delete()
tbl.delete()
assert s.get() is True
def test_table_delete_multiple_callback(self, sentinel):
s1 = sentinel(False)
s2 = sentinel(False)
def callback1():
s1.set(True)
def callback2():
s2.set(True)
tbl = Table([{"a": 1}])
tbl.on_delete(callback1)
tbl.on_delete(callback2)
tbl.delete()
assert s1.get() is True
assert s2.get() is True
def test_table_remove_delete_callback(self, sentinel):
s = sentinel(False)
def callback():
s.set(True)
tbl = Table([{"a": 1}])
callback_id = tbl.on_delete(callback)
tbl.remove_delete(callback_id)
tbl.delete()
assert s.get() is False
def test_view_delete_multiple_callback(self, sentinel):
s1 = sentinel(False)
s2 = sentinel(False)
def callback1():
s1.set(True)
def callback2():
s2.set(True)
tbl = Table([{"a": 1}])
view = tbl.view()
view.on_delete(callback1)
view.on_delete(callback2)
view.delete()
tbl.delete()
assert s1.get() is True
assert s2.get() is True
def test_view_remove_delete_callback(self, sentinel):
s = sentinel(False)
def callback():
s.set(True)
tbl = Table([{"a": 1}])
view = tbl.view()
callback_id = view.on_delete(callback)
view.remove_delete(callback_id)
view.delete()
tbl.delete()
assert s.get() is False
@@ -0,0 +1,65 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from pytest import raises
from perspective import PerspectiveError
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestException(object):
def test_instantiation_exception_table(self):
from perspective import Table
with raises(TypeError) as ex:
Table()
assert "Do not call Table's constructor directly" in str(ex.value)
def test_instantiation_exception_view(self):
from perspective import View
with raises(TypeError) as ex:
View()
assert "Do not call View's constructor directly" in str(ex.value)
def test_exception_from_core(self):
tbl = Table({"a": [1, 2, 3]})
with raises(PerspectiveError) as ex:
# creating view with unknown column should throw
tbl.view(group_by=["b"])
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
def test_exception_from_core_catch_generic(self):
tbl = Table({"a": [1, 2, 3]})
# `PerspectiveCppError` should inherit from `Exception`
with raises(Exception) as ex:
tbl.view(group_by=["b"])
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
def test_exception_from_core_correct_types(self):
tbl = Table({"a": [1, 2, 3]})
# `PerspectiveError` should be raised from the Python layer
with raises(PerspectiveError) as ex:
tbl.view()
tbl.delete()
assert str(ex.value) == "Abort(): Cannot delete table with views"
with raises(PerspectiveError) as ex:
tbl.view(group_by=["b"])
assert str(ex.value) == "Abort(): Invalid column 'b' found in View group_by.\n"
@@ -0,0 +1,115 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
class TestJoin:
def test_inner_join_two_tables(self):
left = client.table(
[{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
)
right = client.table(
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}, {"id": 4, "y": "d"}]
)
joined = client.join(left, right, "id")
view = joined.view()
json = view.to_json()
assert len(json) == 2
view.delete()
joined.delete()
right.delete()
left.delete()
def test_join_has_correct_schema(self):
left = client.table({"id": "integer", "x": "float"})
right = client.table({"id": "integer", "y": "string"})
joined = client.join(left, right, "id")
schema = joined.schema()
assert schema == {"id": "integer", "x": "float", "y": "string"}
joined.delete()
right.delete()
left.delete()
def test_join_reacts_to_left_updates(self):
left = client.table(
[{"id": 1, "x": 10}, {"id": 2, "x": 20}]
)
right = client.table(
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
)
joined = client.join(left, right, "id")
view = joined.view()
left.update([{"id": 1, "x": 99}])
json = view.to_json()
assert json == [
{"id": 1, "x": 10, "y": "a"},
{"id": 2, "x": 20, "y": "b"},
{"id": 1, "x": 99, "y": "a"},
]
view.delete()
joined.delete()
right.delete()
left.delete()
def test_left_join(self):
left = client.table(
[{"id": 1, "x": 10}, {"id": 2, "x": 20}, {"id": 3, "x": 30}]
)
right = client.table(
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}]
)
joined = client.join(left, right, "id", "left")
view = joined.view()
json = view.to_json()
assert len(json) == 3
view.delete()
joined.delete()
right.delete()
left.delete()
def test_join_by_table_names(self):
left = client.table(
[{"id": 1, "x": 10}, {"id": 2, "x": 20}],
name="left_py",
)
right = client.table(
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
name="right_py",
)
joined = client.join("left_py", "right_py", "id")
view = joined.view()
json = view.to_json()
assert len(json) == 2
view.delete()
joined.delete()
right.delete()
left.delete()
def test_join_mixed_table_and_string(self):
left = client.table(
[{"id": 1, "x": 10}, {"id": 2, "x": 20}]
)
right = client.table(
[{"id": 1, "y": "a"}, {"id": 2, "y": "b"}],
name="right_py_mixed",
)
joined = client.join(left, "right_py_mixed", "id")
view = joined.view()
json = view.to_json()
assert len(json) == 2
view.delete()
joined.delete()
right.delete()
left.delete()
@@ -0,0 +1,105 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import psutil
import os
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestDelete(object):
# delete
def test_table_delete(self):
process = psutil.Process(os.getpid())
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.delete()
mem = process.memory_info().rss
for x in range(10000):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.delete()
mem2 = process.memory_info().rss
# assert 1 < (max2 / max) < 1.01
assert (mem2 - mem) < 2000000
def test_table_delete_with_view(self, sentinel):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
process = psutil.Process(os.getpid())
mem = process.memory_info().rss
for x in range(10000):
view = tbl.view()
view.delete()
tbl.delete()
mem2 = process.memory_info().rss
assert (mem2 - mem) < 2000000
class TestExpressionVocab(object):
# Regression tests for the `t_expression_vocab` leak fixed in PR #3175.
# Pre-fix, updates against a view with a string-producing expression
# interned each result into a new 4KB vocab page once the prior page
# filled, and never deduplicated against older pages. RSS grew without
# bound. The fix adds cross-page deduplication; these tests assert the
# steady state is flat.
def test_string_expression_update_no_leak(self):
long_literal = "X" * 256
tbl = Table({"x": "integer", "c": "string"}, index="x")
view = tbl.view(expressions={"e": 'concat("c", \'' + long_literal + "')"})
for _ in range(100):
tbl.update([{"x": 1, "c": "value"}])
process = psutil.Process(os.getpid())
mem = process.memory_info().rss
for _ in range(5000):
tbl.update([{"x": 1, "c": "value"}])
mem2 = process.memory_info().rss
view.delete()
tbl.delete()
assert (mem2 - mem) < 2000000
def test_string_expression_update_bounded_vocab_no_leak(self):
# Cycles through a small fixed set of distinct values. Exercises the
# cross-page `string_exists` lookup the fix relies on: once the
# vocabulary is fully populated, subsequent interns must resolve to
# existing pointers rather than allocating new ones.
values = ["alpha", "bravo", "charlie", "delta", "echo"]
tbl = Table({"x": "integer", "c": "string"}, index="x")
view = tbl.view(expressions={"e": 'upper("c")'})
for i in range(100):
tbl.update([{"x": 1, "c": values[i % len(values)]}])
process = psutil.Process(os.getpid())
mem = process.memory_info().rss
for i in range(5000):
tbl.update([{"x": 1, "c": values[i % len(values)]}])
mem2 = process.memory_info().rss
view.delete()
tbl.delete()
assert (mem2 - mem) < 2000000
@@ -0,0 +1,178 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import random
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
data = {"a": [1, 2, 3, 4], "b": ["a", "b", "c", "d"], "c": [True, False, True, False]}
class TestPorts(object):
def test_make_port_sequential(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
def test_make_port_sequential_and_update(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
for i in range(1, 11):
table.update({"a": [i], "b": ["a"], "c": [True]}, port_id=i)
view = table.view()
result = view.to_columns()
assert result == {
"a": [1, 2, 3, 4] + [i for i in range(1, 11)],
"b": ["a", "b", "c", "d"] + ["a" for i in range(10)],
"c": [True, False, True, False] + [True for i in range(10)],
}
def test_arbitary_port_updates(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
port = random.randint(0, 10)
table.update(data, port_id=port)
assert table.size() == 8
assert table.view().to_columns() == {
"a": [1, 2, 3, 4] * 2,
"b": ["a", "b", "c", "d"] * 2,
"c": [True, False, True, False] * 2,
}
def test_ports_should_only_notify_if_they_have_a_queued_update(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
view = table.view()
ports_to_update = [random.randint(0, 10) for i in range(5)]
def callback(port_id):
assert port_id in ports_to_update
view.on_update(callback)
for port in ports_to_update:
table.update(data, port_id=port)
def test_ports_should_have_unique_deltas(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
view = table.view()
ports_to_update = [random.randint(0, 10) for i in range(5)]
unique_data = {
port: [{"a": port, "b": str(port), "c": True}] for port in ports_to_update
}
def callback(port_id, delta):
assert port_id in ports_to_update
_t = Table(delta)
_v = _t.view()
assert _v.to_records() == unique_data[port]
_v.delete()
_t.delete()
view.on_update(callback, mode="row")
for port in ports_to_update:
table.update(unique_data[port], port_id=port)
def test_ports_should_queue_updates_properly(self):
table = Table(data)
port_ids = []
for i in range(10):
port_ids.append(table.make_port())
assert port_ids == list(range(1, 11))
view = table.view()
ports_to_update = [random.randint(0, 10) for i in range(5)]
def callback(port_id):
assert port_id in ports_to_update
view.on_update(callback)
for port in ports_to_update:
table.update(data, port_id=port)
def test_ports_multiple_tables_with_different_ports(self):
server = Table(data)
client = Table(data)
for i in range(random.randint(5, 15)):
# reserve an arbitary number of ports
server.make_port()
# port for client is now far above the ports "ON" the client, as the
# client ports will begin creation at 1.
server_port_for_client = server.make_port()
client_port = client.make_port()
server_view = server.view()
client_view = client.view()
# when the client updates, check whether the port id matches that
# of the server, and complete the test.
def client_callback(port_id, delta):
if port_id == client_port:
print("UPDATING SERVER")
server.update(delta, port_id=server_port_for_client)
# when the server updates, pass the update back to the client
def server_callback(port_id, delta):
print("UPDATING CLIENT")
assert port_id == server_port_for_client
assert server.size() == 8
server_view.delete()
server.delete()
client_view.delete()
client.delete()
client_view.on_update(client_callback, mode="row")
server_view.on_update(server_callback, mode="row")
client.update(data, port_id=client_port)
@@ -0,0 +1,102 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestRemove(object):
def test_remove_all(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
tbl.remove(["abc"])
assert tbl.view().to_records() == []
# assert tbl.size() == 0
def test_remove_nonsequential(self):
tbl = Table(
[{"a": "abc", "b": 123}, {"a": "def", "b": 456}, {"a": "efg", "b": 789}],
index="a",
)
tbl.remove(["abc", "efg"])
assert tbl.view().to_records() == [{"a": "def", "b": 456}]
# assert tbl.size() == 1
def test_remove_multiple_single(self):
tbl = Table({"a": "integer", "b": "string"}, index="a")
for i in range(0, 10):
tbl.update([{"a": i, "b": str(i)}])
for i in range(1, 10):
tbl.remove([i])
assert tbl.view().to_records() == [{"a": 0, "b": "0"}]
# assert tbl.size() == 0
def test_remove_expressions(self):
schema = {"key": "string", "delta$": "float", "business_line": "string"}
data = [
{
"key": "A",
"delta$": 46412.3804275,
},
{
"key": "B",
"delta$": 2317615.875,
},
]
table = Table(schema, index="key")
table.update(data)
table.remove(["A"])
view = table.view(
group_by=["business_line"],
columns=["delta$", "alias"],
expressions={
"alias": '"delta$"',
},
)
records = view.to_records()
assert records == [
{"__ROW_PATH__": [], "delta$": 2317615.875, "alias": 2317615.875},
{"__ROW_PATH__": [None], "delta$": 2317615.875, "alias": 2317615.875},
]
def test_remove_expressions_after_view(self):
schema = {"key": "string", "delta$": "float", "business_line": "string"}
data = [
{
"key": "A",
"delta$": 46412.3804275,
},
{
"key": "B",
"delta$": 2317615.875,
},
]
table = Table(schema, index="key")
table.update(data)
view = table.view(
group_by=["business_line"],
columns=["delta$", "alias"],
expressions={
"alias": '"delta$"',
},
)
table.remove(["A"])
records = view.to_records()
assert records == [
{"__ROW_PATH__": [], "delta$": 2317615.875, "alias": 2317615.875},
{"__ROW_PATH__": [None], "delta$": 2317615.875, "alias": 2317615.875},
]
@@ -0,0 +1,641 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from datetime import date, datetime
import perspective
from pytest import mark, raises
import pytest
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestTable:
# table constructors
@mark.skip
def test_empty_table(self):
tbl = Table([])
assert tbl.size() == 0
def test_table_not_iterable(self):
data = {"a": 1}
with raises(TypeError):
Table(data)
# def test_table_synchronous_process(self):
# tbl = Table({"a": [1, 2, 3]})
# assert _PerspectiveStateManager.TO_PROCESS == {}
# tbl.update({"a": [4, 5, 6]})
# assert _PerspectiveStateManager.TO_PROCESS == {}
def test_table_csv(self):
data = "x,y,z\n1,a,true\n2,b,false\n3,c,true\n4,d,false"
tbl = Table(data)
assert tbl.schema() == {"x": "integer", "y": "string", "z": "boolean"}
view = tbl.view()
assert view.to_columns() == {
"x": [1, 2, 3, 4],
"y": ["a", "b", "c", "d"],
"z": [True, False, True, False],
}
def test_table_csv_with_nulls(self):
tbl = Table("x,y\n1,")
assert tbl.schema() == {"x": "integer", "y": "string"}
view = tbl.view()
assert view.to_columns() == {"x": [1], "y": [None]}
def test_table_csv_with_nulls_updated(self):
tbl = Table("x,y\n1,", index="x")
assert tbl.schema() == {"x": "integer", "y": "string"}
view = tbl.view()
assert view.to_columns() == {"x": [1], "y": [None]}
tbl.update("x,y\n1,abc\n2,123")
assert view.to_columns() == {"x": [1, 2], "y": ["abc", "123"]}
def test_table_correct_csv_nan_end(self):
tbl = Table("string,integer\n,1\n,2\nabc,3")
assert tbl.schema() == {"string": "string", "integer": "integer"}
assert tbl.size() == 3
assert tbl.view().to_columns() == {
"string": ["", "", "abc"],
"integer": [1, 2, 3],
}
def test_table_correct_csv_nan_intermittent(self):
tbl = Table("string,float\nabc,\n,2.5\nghi,")
assert tbl.schema() == {"string": "string", "float": "float"}
assert tbl.size() == 3
assert tbl.view().to_columns() == {
"string": ["abc", "", "ghi"],
"float": [None, 2.5, None],
}
def test_table_records_from_string_with_format_override(self):
data = '{"x": [1,2,3], "y": [4,5,6]}'
tbl = Table(data, format="columns")
view = tbl.view()
assert view.to_columns() == {
"x": [1, 2, 3],
"y": [4, 5, 6],
}
def test_table_string_column_with_nulls_update_and_filter(self):
tbl = Table(
[
{"a": "1", "b": 2, "c": "3"},
{"a": "2", "b": 3, "c": "4"},
{"a": "3", "b": 3, "c": None},
],
index="a",
)
view = tbl.view(filter=[["c", "==", "4"]])
records = view.to_records()
tbl.update([{"a": "4", "b": 10}])
assert records == view.to_records()
def test_table_int(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "integer", "b": "integer"}
def test_table_int_column_names(self):
data = {"a": [1, 2, 3], 0: [4, 5, 6]}
Table(data)
def test_table_nones(self):
none_data = [{"a": 1, "b": None}, {"a": None, "b": 2}]
tbl = Table(none_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "integer", "b": "integer"}
def test_table_bool(self):
bool_data = [{"a": True, "b": False}, {"a": True, "b": True}]
tbl = Table(bool_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "boolean", "b": "boolean"}
def test_table_bool_str(self):
bool_data = [{"a": "True", "b": "False"}, {"a": "True", "b": "True"}]
tbl = Table(bool_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "boolean", "b": "boolean"}
assert tbl.view().to_columns() == {"a": [True, True], "b": [False, True]}
def test_table_float(self):
float_data = [{"a": 1.5, "b": 2.5}, {"a": 3.2, "b": 3.1}]
tbl = Table(float_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "float", "b": "float"}
def test_table_str(self):
str_data = [{"a": "b", "b": "b"}, {"a": "3", "b": "3"}]
tbl = Table(str_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "string", "b": "string"}
def test_table_str_with_escape(self):
str_data = [
{"a": 'abc"def"', "b": 'abc"def"'},
{"a": "abc'def'", "b": "abc'def'"},
]
tbl = Table(str_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_records() == str_data
def test_table_str_unicode(self):
str_data = [{"a": "ȀȁȀȃȀȁȀȃȀȁȀȃȀȁȀȃ", "b": "ЖДфйЖДфйЖДфйЖДфй"}]
tbl = Table(str_data)
assert tbl.size() == 1
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_records() == str_data
def test_table_date(self):
str_data = [{"a": date.today(), "b": date.today()}]
tbl = Table(str_data)
assert tbl.size() == 1
assert tbl.schema() == {"a": "date", "b": "date"}
def test_table_datetime(self):
str_data = [{"a": datetime.now(), "b": datetime.now()}]
tbl = Table(str_data)
assert tbl.size() == 1
assert tbl.schema() == {"a": "datetime", "b": "datetime"}
def test_table_columnar(self):
data = {"a": [1, 2, 3], "b": [4, 5, 6]}
tbl = Table(data)
assert tbl.columns() == ["a", "b"]
assert tbl.size() == 3
assert tbl.schema() == {"a": "integer", "b": "integer"}
def test_table_columnar_mixed_length(self):
data = [{"a": 1.5, "b": 2.5}, {"a": 3.2}]
tbl = Table(data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "float", "b": "float"}
assert tbl.view().to_records() == [{"a": 1.5, "b": 2.5}, {"a": 3.2, "b": None}]
# schema
def test_table_schema(self):
data = {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
def test_table_readable_string_schema(self):
data = {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
def test_table_output_readable_schema(self):
data = {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
def test_table_mixed_schema(self):
data = {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
def test_table_output_string_schema(self):
data = {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
def test_table_symmetric_schema(self):
data = {
"a": [1, 2, 3],
"b": [1.5, 2.5, 3.5],
"c": ["a", "b", "c"],
"d": [True, False, True],
"e": [date.today(), date.today(), date.today()],
"f": [datetime.now(), datetime.now(), datetime.now()],
}
tbl = Table(data)
schema = tbl.schema()
assert schema == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl2 = Table(schema)
assert tbl2.schema() == schema
def test_table_symmetric_string_schema(self):
data = {
"a": [1, 2, 3],
"b": [1.5, 2.5, 3.5],
"c": ["a", "b", "c"],
"d": [True, False, True],
"e": [date.today(), date.today(), date.today()],
"f": [datetime.now(), datetime.now(), datetime.now()],
}
tbl = Table(data)
schema = tbl.schema()
assert schema == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
tbl2 = Table(schema)
assert tbl2.schema() == schema
def test_table_python_schema(self):
data = {
"a": int,
"b": float,
"c": str,
"d": bool,
"e": date,
"f": datetime,
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "string",
"d": "boolean",
"e": "date",
"f": "datetime",
}
# is_valid_filter
# def test_table_is_valid_filter_str(self):
# filter = ["a", "<", 1]
# data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
# tbl = Table(data)
# assert tbl.is_valid_filter(filter) is True
# def test_table_not_is_valid_filter_str(self):
# filter = ["a", "<", None]
# data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
# tbl = Table(data)
# assert tbl.is_valid_filter(filter) is False
# def test_table_is_valid_filter_filter_op(self):
# filter = ["a", t_filter_op.FILTER_OP_IS_NULL]
# data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
# tbl = Table(data)
# assert tbl.is_valid_filter(filter) is True
# def test_table_not_is_valid_filter_filter_op(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, None]
# data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
# tbl = Table(data)
# assert tbl.is_valid_filter(filter) is False
# def test_table_is_valid_filter_date(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, date.today()]
# tbl = Table({"a": "date"})
# assert tbl.is_valid_filter(filter) is True
# def test_table_not_is_valid_filter_date(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, None]
# tbl = Table({"a": "date"})
# assert tbl.is_valid_filter(filter) is False
# def test_table_is_valid_filter_datetime(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, datetime.now()]
# tbl = Table({"a": "datetime"})
# assert tbl.is_valid_filter(filter) is True
# def test_table_not_is_valid_filter_datetime(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, None]
# tbl = Table({"a": "datetime"})
# assert tbl.is_valid_filter(filter) is False
# def test_table_is_valid_filter_datetime_str(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, "7/11/2019 5:30PM"]
# tbl = Table({"a": "datetime"})
# assert tbl.is_valid_filter(filter) is True
# def test_table_not_is_valid_filter_datetime_str(self):
# filter = ["a", t_filter_op.FILTER_OP_GT, None]
# tbl = Table({"a": "datetime"})
# assert tbl.is_valid_filter(filter) is False
# def test_table_is_valid_filter_ignores_not_in_schema(self):
# filter = ["not in schema", "<", 1]
# data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
# tbl = Table(data)
# assert tbl.is_valid_filter(filter) is True
# index
def test_table_index(self):
data = [{"a": 1, "b": 2}, {"a": 1, "b": 4}]
tbl = Table(data, index="a")
assert tbl.size() == 1
assert tbl.view().to_records() == [{"a": 1, "b": 4}]
def test_table_index_from_schema(self):
data = [{"a": 1, "b": 2}, {"a": 1, "b": 4}]
tbl = Table({"a": "integer", "b": "integer"}, index="a")
assert tbl.size() == 0
tbl.update(data)
assert tbl.view().to_records() == [{"a": 1, "b": 4}]
# index with None in column
def test_table_index_int_with_none(self):
tbl = Table({"a": [0, 1, 2, None, None], "b": [4, 3, 2, 1, 0]}, index="a")
assert tbl.view().to_columns() == {
"a": [None, 0, 1, 2],
"b": [0, 4, 3, 2],
} # second `None` replaces first
def test_table_index_float_with_none(self):
tbl = Table({"a": [0.0, 1.5, 2.5, None, None], "b": [4, 3, 2, 1, 0]}, index="a")
assert tbl.view().to_columns() == {
"a": [None, 0, 1.5, 2.5],
"b": [0, 4, 3, 2],
} # second `None` replaces first
def test_table_index_bool_with_none(self):
# bools cannot be used as primary key columns
with raises(perspective.PerspectiveError):
Table({"a": [True, False, None, True], "b": [4, 3, 2, 1]}, index="a")
def test_table_index_date_with_none(self):
tbl = Table(
{
"a": [date(2019, 7, 11), None, date(2019, 3, 12), date(2011, 3, 10)],
"b": [4, 3, 2, 1],
},
index="a",
)
def ts(x):
return int(datetime.timestamp(x) * 1000)
assert tbl.view().to_columns() == {
"a": [
None,
ts(datetime(2011, 3, 10)),
ts(datetime(2019, 3, 12)),
ts(datetime(2019, 7, 11)),
],
"b": [3, 1, 2, 4],
}
def test_table_index_datetime_with_none(self, util):
tbl = Table(
{
"a": [
datetime(2019, 7, 11, 15, 30),
None,
datetime(2019, 7, 11, 12, 10),
datetime(2019, 7, 11, 5, 0),
],
"b": [4, 3, 2, 1],
},
index="a",
)
assert tbl.view().to_columns() == {
"a": [
None,
util.to_timestamp(datetime(2019, 7, 11, 5, 0)),
util.to_timestamp(datetime(2019, 7, 11, 12, 10)),
util.to_timestamp(datetime(2019, 7, 11, 15, 30)),
],
"b": [3, 1, 2, 4],
}
def test_table_index_str_with_none(self):
tbl = Table({"a": ["", "a", None, "b"], "b": [4, 3, 2, 1]}, index="a")
assert tbl.view().to_columns() == {"a": [None, "", "a", "b"], "b": [2, 4, 3, 1]}
def test_table_get_index(self):
tbl = Table({"a": ["", "a", None, "b"], "b": [4, 3, 2, 1]}, index="a")
assert tbl.get_index() == "a"
def test_table_get_index_none(self):
tbl = Table({"a": ["", "a", None, "b"], "b": [4, 3, 2, 1]})
assert tbl.get_index() is None
# limit
def test_table_limit(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data, limit=1)
assert tbl.size() == 1
assert tbl.view().to_records() == [{"a": 3, "b": 4}]
def test_table_get_limit(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data, limit=1)
assert tbl.get_limit() == 1
def test_table_get_limit_none(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
assert tbl.get_limit() is None
# num_views
@pytest.mark.skip
def test_table_get_num_views(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
assert tbl.get_num_views() == 0
v1 = tbl.view()
v2 = tbl.view()
v3 = tbl.view()
assert tbl.get_num_views() == 3
v1.delete()
v2.delete()
assert tbl.get_num_views() == 1
failed = False
try:
tbl.delete()
except perspective.PerspectiveError:
failed = True
assert failed
v3.delete()
assert tbl.get_num_views() == 0
tbl.delete()
# clear
def test_table_clear(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
tbl.clear()
view = tbl.view()
assert view.to_records() == []
# replace
def test_table_replace(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data2 = [{"a": 3, "b": 4}, {"a": 1, "b": 2}]
tbl = Table(data)
tbl.replace(data2)
assert tbl.view().to_records() == data2
def test_table_replace_views_should_preserve(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data2 = [{"a": 3, "b": 4}, {"a": 1, "b": 2}]
tbl = Table(data)
view = tbl.view(group_by=["a"], split_by=["b"])
first = view.to_records()
tbl.replace(data2)
assert view.to_records() == first
def test_table_replace_should_fire_on_update(self, sentinel):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data2 = [{"a": 3, "b": 4}, {"a": 1, "b": 2}]
tbl = Table(data)
view = tbl.view()
s = sentinel(False)
def updater(port_id):
s.set(True)
view.on_update(updater)
tbl.replace(data2)
tbl.size()
assert s.get()
def test_table_replace_should_fire_on_update_with_delta(self, sentinel):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data2 = [{"a": 3, "b": 4}, {"a": 1, "b": 2}]
tbl = Table(data)
view = tbl.view()
s = sentinel(False)
def updater(port_id, delta):
# assert port_id == 0
table2 = Table(delta)
assert table2.view().to_records() == data2
s.set(True)
view.on_update(updater, mode="row")
tbl.replace(data2)
tbl.size()
assert s.get() is True
def test_float32_table_construction(self):
float_data = [{"a": 1.5, "b": 2.5}, {"a": 3.2, "b": 3.1}]
tbl = Table(float_data, index="a")
assert tbl.size() == 2
assert tbl.schema() == {"a": "float", "b": "float"}
if __name__ == "__main__":
import pytest
pytest.main(["-vv", "-s", __file__])
@@ -0,0 +1,500 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import os.path
import numpy as np
import pandas as pd
from perspective.tests.conftest import Util
import pyarrow as pa
from datetime import date, datetime
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
DATE32_ARROW = os.path.join(os.path.dirname(__file__), "arrow", "date32.arrow")
DATE64_ARROW = os.path.join(os.path.dirname(__file__), "arrow", "date64.arrow")
DICT_ARROW = os.path.join(os.path.dirname(__file__), "arrow", "dict.arrow")
names = ["a", "b", "c", "d"]
# Create sample data for every integer type
ALL_INTEGERS_DATA = {
"int8": pa.array([1, 2, 3], type=pa.int8()),
"int16": pa.array([1000, 2000, 3000], type=pa.int16()),
"int32": pa.array([100000, 200000, 300000], type=pa.int32()),
"int64": pa.array([10000000000, 20000000000, 30000000000], type=pa.int64()),
"uint8": pa.array([1, 2, 3], type=pa.uint8()),
"uint16": pa.array([1000, 2000, 3000], type=pa.uint16()),
"uint32": pa.array([100000, 200000, 300000], type=pa.uint32()),
"uint64": pa.array([10000000000, 20000000000, 30000000000], type=pa.uint64()),
"float32": pa.array([100000.0, 200000.0, 300000.0], type=pa.float32()),
"float64": pa.array(
[10000000000.0, 20000000000.0, 30000000000.0], type=pa.float64()
),
}
ALL_INTEGERS_TABLE = pa.Table.from_pydict(ALL_INTEGERS_DATA)
class TestTableArrow(object):
def test_table_with_integer_types(self):
tbl = Table(ALL_INTEGERS_TABLE)
assert tbl.size() == 3
assert tbl.schema() == {
"int8": "integer",
"int16": "integer",
"int32": "integer",
"int64": "integer",
"uint8": "integer",
"uint16": "integer",
"uint32": "integer",
"uint64": "integer",
"float32": "float",
"float64": "float",
}
for k, values in ALL_INTEGERS_DATA.items():
v = tbl.view(filter=[[k, "==", values[0].as_py()]])
assert len(v.to_json()) == 1
def test_table_arrow_loads_date32_file(self, util: Util):
with open(DATE32_ARROW, mode="rb") as file: # b is important -> binary
tbl = Table(file.read())
assert tbl.schema() == {
"jan-2019": "date",
"feb-2020": "date",
"mar-2019": "date",
"apr-2020": "date",
}
assert tbl.size() == 31
view = tbl.view()
assert view.to_columns() == {
"jan-2019": [
util.to_timestamp(datetime(2019, 1, i)) for i in range(1, 32)
],
"feb-2020": [
util.to_timestamp(datetime(2020, 2, i)) for i in range(1, 30)
]
+ [None, None],
"mar-2019": [
util.to_timestamp(datetime(2019, 3, i)) for i in range(1, 32)
],
"apr-2020": [
util.to_timestamp(datetime(2020, 4, i)) for i in range(1, 31)
]
+ [None],
}
def test_table_arrow_loads_date64_file(self, util: Util):
with open(DATE64_ARROW, mode="rb") as file: # b is important -> binary
tbl = Table(file.read())
assert tbl.schema() == {
"jan-2019": "date",
"feb-2020": "date",
"mar-2019": "date",
"apr-2020": "date",
}
assert tbl.size() == 31
view = tbl.view()
assert view.to_columns() == {
"jan-2019": [
util.to_timestamp(datetime(2019, 1, i)) for i in range(1, 32)
],
"feb-2020": [
util.to_timestamp(datetime(2020, 2, i)) for i in range(1, 30)
]
+ [None, None],
"mar-2019": [
util.to_timestamp(datetime(2019, 3, i)) for i in range(1, 32)
],
"apr-2020": [
util.to_timestamp(datetime(2020, 4, i)) for i in range(1, 31)
]
+ [None],
}
def test_table_arrow_loads_dict_file(self):
with open(DICT_ARROW, mode="rb") as file: # b is important -> binary
tbl = Table(file.read())
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.size() == 5
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None, "abc"],
"b": ["klm", "hij", None, "hij", "klm"],
}
# streams
def test_table_arrow_loads_int_stream(self, util):
data = [list(range(10)) for i in range(4)]
arrow_data = util.make_arrow(names, data)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "integer",
"b": "integer",
"c": "integer",
"d": "integer",
}
assert tbl.view().to_columns() == {
"a": data[0],
"b": data[1],
"c": data[2],
"d": data[3],
}
def test_empty_arrow(self, util):
table = pa.table(
{
"col1": [1, 2, 3],
"col2": ["abc", "foo", "bar"],
}
)
empty_table = table.schema.empty_table()
assert client.table(table, name="table2").size() == 3
assert client.table(empty_table, name="table_empty_bad").size() == 0
assert client.table(table, name="table3").schema() == {
"col1": "integer",
"col2": "string",
}
assert client.table(empty_table, name="table4").schema() == {
"col1": "integer",
"col2": "string",
}
def test_table_arrow_loads_float_stream(self, util):
data = [[i for i in range(10)], [i * 1.5 for i in range(10)]]
arrow_data = util.make_arrow(["a", "b"], data)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "integer",
"b": "float",
}
assert tbl.view().to_columns() == {"a": data[0], "b": data[1]}
def test_table_arrow_loads_decimal_stream(self, util):
data = [[i * 1000 for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.decimal128(4)])
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "float",
}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_bool_stream(self, util):
data = [[True if i % 2 == 0 else False for i in range(10)]]
arrow_data = util.make_arrow(["a"], data)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "boolean"}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_date32_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date32()])
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "date"}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)]
}
def test_table_arrow_loads_date64_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date64()])
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "date"}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)]
}
def test_table_arrow_loads_timestamp_all_formats_stream(self, util):
data = [
[datetime(2019, 2, i, 9) for i in range(1, 11)],
[datetime(2019, 2, i, 10) for i in range(1, 11)],
[datetime(2019, 2, i, 11) for i in range(1, 11)],
[datetime(2019, 2, i, 12) for i in range(1, 11)],
]
arrow_data = util.make_arrow(
names,
data,
types=[
pa.timestamp("s"),
pa.timestamp("ms"),
pa.timestamp("us"),
pa.timestamp("ns"),
],
)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "datetime",
"b": "datetime",
"c": "datetime",
"d": "datetime",
}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(i) for i in data[0]],
"b": [util.to_timestamp(i) for i in data[1]],
"c": [util.to_timestamp(i) for i in data[2]],
"d": [util.to_timestamp(i) for i in data[3]],
}
def test_table_arrow_loads_string_stream(self, util):
data = [[str(i) for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.string()])
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "string"}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_dictionary_stream_int8(self, util):
data = [
([0, 1, 1, None], ["abc", "def"]),
([0, 1, None, 2], ["xx", "yy", "zz"]),
]
types = [[pa.int8(), pa.string()]] * 2
arrow_data = util.make_dictionary_arrow(["a", "b"], data, types=types)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None],
"b": ["xx", "yy", None, "zz"],
}
def test_table_arrow_loads_dictionary_stream_int16(self, util):
data = [
([0, 1, 1, None], ["abc", "def"]),
([0, 1, None, 2], ["xx", "yy", "zz"]),
]
types = [[pa.int16(), pa.string()]] * 2
arrow_data = util.make_dictionary_arrow(["a", "b"], data, types=types)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None],
"b": ["xx", "yy", None, "zz"],
}
def test_table_arrow_loads_dictionary_stream_int32(self, util):
data = [
([0, 1, 1, None], ["abc", "def"]),
([0, 1, None, 2], ["xx", "yy", "zz"]),
]
types = [[pa.int32(), pa.string()]] * 2
arrow_data = util.make_dictionary_arrow(["a", "b"], data, types=types)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None],
"b": ["xx", "yy", None, "zz"],
}
def test_table_arrow_loads_dictionary_stream_int64(self, util):
data = [
([0, 1, 1, None], ["abc", "def"]),
([0, 1, None, 2], ["xx", "yy", "zz"]),
]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None],
"b": ["xx", "yy", None, "zz"],
}
def test_table_arrow_loads_dictionary_stream_nones(self, util):
data = [([None, 0, 1, 2], ["", "abc", "def"])]
arrow_data = util.make_dictionary_arrow(["a"], data)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string"}
assert tbl.view().to_columns() == {"a": [None, "", "abc", "def"]}
def test_table_arrow_loads_dictionary_stream_nones_indexed(self, util):
data = [
([1, None, 0, 2], ["", "abc", "def"]),
([2, 1, 0, None], ["", "hij", "klm"]),
] # ["abc", None, "", "def"] # ["klm", "hij", "", None]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table(arrow_data, index="a") # column "a" is sorted
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": [None, "", "abc", "def"],
"b": ["hij", "", "klm", None],
}
def test_table_arrow_loads_dictionary_stream_nones_indexed_2(self, util):
"""Test the other column, just in case."""
data = [
([1, None, 0, 2], ["", "abc", "def"]),
([2, 1, 0, None], ["", "hij", "klm"]),
] # ["abc", None, "", "def"] # ["klm", "hij", "", None]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table(arrow_data, index="b") # column "b" is sorted
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["def", "", None, "abc"],
"b": [None, "", "hij", "klm"],
}
# legacy
def test_table_arrow_loads_int_legacy(self, util):
data = [list(range(10)) for i in range(4)]
arrow_data = util.make_arrow(names, data, legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "integer",
"b": "integer",
"c": "integer",
"d": "integer",
}
def test_table_arrow_loads_float_legacy(self, util):
data = [[i for i in range(10)], [i * 1.5 for i in range(10)]]
arrow_data = util.make_arrow(["a", "b"], data, legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "integer",
"b": "float",
}
assert tbl.view().to_columns() == {"a": data[0], "b": data[1]}
def test_table_arrow_loads_decimal128_legacy(self, util):
data = [[i * 1000 for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.decimal128(4)], legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "float",
}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_bool_legacy(self, util):
data = [[True if i % 2 == 0 else False for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "boolean"}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_date32_legacy(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date32()], legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "date"}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)]
}
def test_table_arrow_loads_date64_legacy(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date64()], legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "date"}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)]
}
def test_table_arrow_loads_timestamp_all_formats_legacy(self, util):
data = [
[datetime(2019, 2, i, 9) for i in range(1, 11)],
[datetime(2019, 2, i, 10) for i in range(1, 11)],
[datetime(2019, 2, i, 11) for i in range(1, 11)],
[datetime(2019, 2, i, 12) for i in range(1, 11)],
]
arrow_data = util.make_arrow(
names,
data,
types=[
pa.timestamp("s"),
pa.timestamp("ms"),
pa.timestamp("us"),
pa.timestamp("ns"),
],
legacy=True,
)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {
"a": "datetime",
"b": "datetime",
"c": "datetime",
"d": "datetime",
}
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(i) for i in data[0]],
"b": [util.to_timestamp(i) for i in data[1]],
"c": [util.to_timestamp(i) for i in data[2]],
"d": [util.to_timestamp(i) for i in data[3]],
}
def test_table_arrow_loads_string_legacy(self, util):
data = [[str(i) for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.string()], legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 10
assert tbl.schema() == {"a": "string"}
assert tbl.view().to_columns() == {"a": data[0]}
def test_table_arrow_loads_dictionary_legacy(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data, legacy=True)
tbl = Table(arrow_data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "string", "b": "string"}
assert tbl.view().to_columns() == {
"a": ["a", "b", "b", None],
"b": ["x", "y", None, "z"],
}
def test_table_arrow_loads_arrow_from_df_with_nan(self):
data = pd.DataFrame({"a": [1.5, 2.5, np.nan, 3.5, 4.5, np.nan, np.nan, np.nan]})
arrow_table = pa.Table.from_pandas(data, preserve_index=False)
assert arrow_table["a"].null_count == 4
tbl = Table(arrow_table)
assert tbl.size() == 8
# check types
assert tbl.schema() == {"a": "float"}
# check nans
json = tbl.view().to_columns()
assert json["a"] == [1.5, 2.5, None, 3.5, 4.5, None, None, None]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,201 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from datetime import date, datetime
from pytest import mark
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestTableInfer(object):
def test_table_infer_int(self):
data = {"a": [None, None, None, None, 1, 0, 1, 1, 1]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer"}
def test_table_infer_float(self):
data = {"a": [None, None, None, None, 1.0, 2.0]}
tbl = Table(data)
assert tbl.schema() == {"a": "float"}
def test_table_infer_bool(self):
bool_data = [{"a": True, "b": False}, {"a": True, "b": True}]
tbl = Table(bool_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "boolean", "b": "boolean"}
def test_table_infer_bool_str(self):
bool_data = [{"a": "True", "b": "False"}, {"a": "True", "b": "True"}]
tbl = Table(bool_data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "boolean", "b": "boolean"}
@mark.skip(reason="Unsupported python specific behavior")
def test_table_bool_infer_str_all_formats_from_schema(self):
bool_data = [
{"a": "True", "b": "False"},
{"a": "t", "b": "f"},
{"a": "true", "b": "false"},
{"a": 1, "b": 0},
{"a": "on", "b": "off"},
]
tbl = Table(bool_data)
assert tbl.schema() == {"a": "boolean", "b": "boolean"}
assert tbl.size() == 5
assert tbl.view().to_columns() == {
"a": [True, True, True, True, True],
"b": [False, False, False, False, False],
}
def test_table_infer_bool_variant(self):
data = {"a": [None, None, None, None, True, True, True]}
tbl = Table(data)
assert tbl.schema() == {"a": "boolean"}
def test_table_infer_str(self):
data = {"a": [None, None, None, None, None, None, "abc"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
@mark.skip(reason="Time is not a valid JSON type")
def test_table_infer_time_as_string(self):
# time objects are inferred as string
data = {
"a": [
None,
None,
None,
None,
None,
None,
datetime(2019, 7, 11, 12, 30, 5).time(),
]
}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
@mark.skip(reason="Unsupported python specific behavior")
def test_table_infer_date_from_datetime(self):
# inferrence on non-pandas datasets defaults to datetime
data = {"a": [None, None, None, None, None, None, datetime(2019, 7, 11)]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_date_from_date(self):
# pass in a `date` to make sure it infers as date
data = {"a": [None, None, None, None, None, None, date(2019, 7, 11)]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_valid_date(self):
data = {"a": [None, None, None, None, None, None, "08/31/2019"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_ambiguous_date(self):
data = {"a": [None, None, None, None, None, None, "01/03/2019"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_ymd_date(self):
data = {"a": [None, None, None, None, None, None, "2019/01/03"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_invalid_date(self):
data = {"a": [None, None, None, None, None, None, "08/55/2019"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
def test_table_infer_date_edge(self):
data = {"a": [None, None, None, None, None, None, "08/31/2019 00:00:00"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_datetime_edge(self):
data = {"a": [None, None, None, None, None, None, "08/31/2019 00:00:01"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_valid_datetime(self):
data = {"a": [None, None, None, None, None, None, "08/31/2019 07:30:00"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_iso_datetime(self):
data = {"a": [None, None, None, None, None, None, "2019/07/25T09:00:00"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_datetime_separators(self):
data = {
"a": [
None,
None,
None,
None,
None,
"2019-07-25T09:00:00",
"2019/07/25T09:00:00",
]
}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_datetime_tz(self):
data = {"a": [None, None, None, None, None, "2019-07-25T09:00:00-05:00"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_infer_invalid_datetime(self):
data = {"a": [None, None, None, None, None, None, "08/31/2019 25:30:00"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
def test_table_infer_mixed_date(self):
data = {"a": [None, None, None, None, None, "08/11/2019"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_infer_mixed_datetime(self):
data = {"a": [None, None, None, None, None, "08/11/2019 13:14:15"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
@mark.skip(reason="Numeric strings are not inferred as numbers")
def test_table_strict_datetime_infer(self):
data = {"a": ["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
def test_table_strict_date_infer(self):
data = {"a": ["2019 09 10"]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
def test_table_strict_datetime_separator_infer(self):
data = {"a": ["2019-10-01 7:30"]}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
def test_table_datetime_infer_no_false_positive(self):
data = {"a": [" . - / but clearly not a date"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
@mark.skip
def test_table_datetime_infer_from_string_with_time(self):
data = {"a": ["11:00 ABCD"]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
@@ -0,0 +1,45 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestTableInfer(object):
def test_table_limit_wraparound_does_not_respect_partial_none(self):
t = Table({"a": "float", "b": "float"}, limit=3)
t.update([{"a": 10}, {"b": 1}, {"a": 20}, {"a": None, "b": 2}])
d1 = t.view().to_json()
t2 = Table({"a": "float", "b": "float"}, limit=3)
t2.update([{"a": 10}, {"b": 1}, {"a": 20}, {"b": 2}])
d2 = t2.view().to_json()
assert d1[0] == d2[0]
assert d1[1:] == d2[1:]
def test_table_limit_wraparound_does_not_respect_partial(self):
t = Table({"a": "float", "b": "float"}, limit=3)
t.update([{"a": 10}, {"b": 1}, {"a": 20}, {"a": 10, "b": 2}])
d1 = t.view().to_columns()
t2 = Table({"a": "float", "b": "float"}, limit=3)
t2.update([{"a": 10}, {"b": 1}, {"a": 20}, {"b": 2}])
d2 = t2.view().to_columns()
assert d1 != d2
def test_table_limit_with_json(self):
t = Table({"a": [1, 2, 3]}, limit=1)
assert t.size() == 1
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,304 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import os
import tempfile
import perspective as psp
from pytest import mark
client = psp.Server().new_local_client()
Table = client.table
def _perspective_temp_dirs():
"""All `perspective_*` directories currently in the OS temp directory.
`BACKING_STORE_DISK` columns are written to a unique
`<tempdir>/perspective_<uuid>/` directory.
"""
tmp = tempfile.gettempdir()
try:
entries = os.listdir(tmp)
except OSError:
return set()
print(tmp)
return {
os.path.join(tmp, e)
for e in entries
if e.startswith("perspective_") and os.path.isdir(os.path.join(tmp, e))
}
class TestTableOnDisk:
def test_page_to_disk_schema_and_view(self):
data = {
"x": [1, 2, 3, 4],
"y": ["a", "b", "c", "d"],
"z": [True, False, True, False],
}
mem = Table(data)
disk = Table(data, page_to_disk=True)
assert disk.schema() == mem.schema()
assert disk.view().to_columns() == mem.view().to_columns()
def test_page_to_disk_csv(self):
data = "x,y,z\n1,a,true\n2,b,false\n3,c,true\n4,d,false"
tbl = Table(data, page_to_disk=True)
assert tbl.schema() == {"x": "integer", "y": "string", "z": "boolean"}
assert tbl.view().to_columns() == {
"x": [1, 2, 3, 4],
"y": ["a", "b", "c", "d"],
"z": [True, False, True, False],
}
def test_page_to_disk_update_indexed(self):
mem = Table({"x": [1, 2, 3], "y": [1.0, 2.0, 3.0]}, index="x")
disk = Table(
{"x": [1, 2, 3], "y": [1.0, 2.0, 3.0]}, index="x", page_to_disk=True
)
update = {"x": [2, 4], "y": [20.0, 40.0]}
mem.update(update)
disk.update(update)
assert disk.view().to_columns() == mem.view().to_columns()
assert disk.size() == mem.size()
def test_page_to_disk_group_by_aggregation(self):
data = {"g": ["a", "b", "a", "b", "a"], "v": [1, 2, 3, 4, 5]}
mem = Table(data)
disk = Table(data, page_to_disk=True)
config = {"group_by": ["g"], "columns": ["v"], "aggregates": {"v": "sum"}}
assert disk.view(**config).to_columns() == mem.view(**config).to_columns()
def test_page_to_disk_arrow_roundtrip(self):
data = {"x": list(range(100)), "y": [float(i) / 2 for i in range(100)]}
mem = Table(data)
disk = Table(data, page_to_disk=True)
assert disk.view().to_arrow() == mem.view().to_arrow()
def test_page_to_disk_creates_backing_files(self):
before = _perspective_temp_dirs()
tbl = Table({"x": [1, 2, 3], "y": ["a", "b", "c"]}, page_to_disk=True)
# Touch the table so it is not optimized away before we inspect the FS.
assert tbl.size() == 3
after = _perspective_temp_dirs()
new_dirs = after - before
assert new_dirs, "expected a new perspective_<uuid> directory on disk"
assert any(os.listdir(d) for d in new_dirs), "expected column files on disk"
def test_memory_table_creates_no_backing_files(self):
before = _perspective_temp_dirs()
tbl = Table({"x": [1, 2, 3]})
assert tbl.size() == 3
after = _perspective_temp_dirs()
assert after == before, "in-memory table must not write to disk"
def test_page_to_disk_growth_forces_resize(self):
tbl = Table({"x": [0], "y": [0.0]}, index="x", page_to_disk=True)
n = 50000
tbl.update({"x": list(range(n)), "y": [float(i) for i in range(n)]})
assert tbl.size() == n
cols = tbl.view().to_columns()
assert cols["x"][0] == 0
assert cols["x"][-1] == n - 1
assert cols["y"][-1] == float(n - 1)
def test_page_to_disk_clone_keeps_master_files(self):
# Cloning a disk-backed column (e.g. the masked clone that serializing a
# table with removed rows performs) must give the clone its OWN backing
# file. Otherwise the clone aliases — and on teardown `rmfile`s — the
# master's file, silently unlinking the master's named backing store.
before = _perspective_temp_dirs()
tbl = Table(
{"x": [1, 2, 3, 4], "y": [10.0, 20.0, 30.0, 40.0]},
index="x",
page_to_disk=True,
)
view = tbl.view()
new_dirs = _perspective_temp_dirs() - before
master_dirs = [
d
for d in new_dirs
if not os.path.basename(d).startswith("perspective_expr_")
]
assert len(master_dirs) == 1
master_dir = master_dirs[0]
master_files = set(os.listdir(master_dir))
assert master_files, "expected master backing files on disk"
# Removing rows triggers a masked clone of the disk master columns.
tbl.remove([2, 3])
assert view.to_columns()["x"] == [1, 4]
# The master's own backing files must survive the clone's teardown.
survived = set(os.listdir(master_dir))
assert master_files <= survived, (
"clone teardown unlinked master backing files: {}".format(
master_files - survived
)
)
# And the master must remain usable for subsequent updates.
tbl.update({"x": [5, 6], "y": [50.0, 60.0]})
assert sorted(view.to_columns()["x"]) == [1, 4, 5, 6]
def test_page_to_disk_larger_dataset_matches_memory(self):
n = 20000
data = {
"i": list(range(n)),
"f": [float(i) * 1.5 for i in range(n)],
"s": ["row_{}".format(i % 97) for i in range(n)],
}
mem = Table(data)
disk = Table(data, page_to_disk=True)
assert disk.view().to_arrow() == mem.view().to_arrow()
def _perspective_expr_dirs():
"""`perspective_expr_*` directories — the on-disk expression `m_master`."""
tmp = tempfile.gettempdir()
try:
entries = os.listdir(tmp)
except OSError:
return set()
return {
os.path.join(tmp, e)
for e in entries
if e.startswith("perspective_expr_") and os.path.isdir(os.path.join(tmp, e))
}
class TestTableOnDiskExpressions:
def test_expression_numeric_equivalence(self):
data = {"x": [1, 2, 3, 4], "y": [10.0, 20.0, 30.0, 40.0]}
mem = Table(data)
disk = Table(data, page_to_disk=True)
exprs = {"sum": '"x" + "y"', "prod": '"x" * "y"'}
assert (
disk.view(expressions=exprs).to_columns()
== mem.view(expressions=exprs).to_columns()
)
def test_expression_string_equivalence(self):
data = {"a": ["foo", "bar", "baz"], "b": ["AA", "BB", "CC"]}
mem = Table(data)
disk = Table(data, page_to_disk=True)
exprs = {"up": 'upper("a")', "lo": 'lower("b")'}
assert (
disk.view(expressions=exprs).to_columns()
== mem.view(expressions=exprs).to_columns()
)
def test_expression_with_group_by_equivalence(self):
data = {"g": ["a", "b", "a", "b"], "v": [1, 2, 3, 4]}
mem = Table(data)
disk = Table(data, page_to_disk=True)
config = {
"expressions": {"v2": '"v" * 2'},
"group_by": ["g"],
"columns": ["v2"],
"aggregates": {"v2": "sum"},
}
assert disk.view(**config).to_columns() == mem.view(**config).to_columns()
def test_expression_master_is_page_to_disk(self):
before = _perspective_expr_dirs()
tbl = Table({"x": [1, 2, 3], "y": [10.0, 20.0, 30.0]}, page_to_disk=True)
view = tbl.view(expressions={"e": '"x" + "y"'})
assert view.to_columns()["e"] == [11.0, 22.0, 33.0]
new_dirs = _perspective_expr_dirs() - before
assert new_dirs, (
"expected a perspective_expr_<uuid> dir for the expression master"
)
assert any(os.listdir(d) for d in new_dirs), (
"expected expression backing files on disk"
)
def test_memory_expression_creates_no_expr_dir(self):
before = _perspective_expr_dirs()
tbl = Table({"x": [1, 2, 3], "y": [10.0, 20.0, 30.0]})
view = tbl.view(expressions={"e": '"x" + "y"'})
assert view.to_columns()["e"] == [11.0, 22.0, 33.0]
assert _perspective_expr_dirs() == before, (
"in-memory table must not write expression data to disk"
)
def test_expression_page_to_disk_update_and_larger(self):
n = 10000
tbl = Table({"x": [0], "y": [0.0]}, index="x", page_to_disk=True)
tbl.update({"x": list(range(n)), "y": [float(i) for i in range(n)]})
cols = tbl.view(expressions={"e": '"x" + "y"'}).to_columns()
assert cols["e"][0] == 0.0
assert cols["e"][-1] == float((n - 1) + (n - 1))
class TestResidency:
# The residency manager evicts disk-backed column buffers to their files
# when over the `PSP_MEMORY_BUDGET` and restores them transparently on
# access. Under a tiny budget, eviction fires aggressively and data must
# still round-trip identically to an in-memory table.
@mark.skip(reason="No secret hooks in the engine")
def test_residency_evicts_and_data_is_correct(self):
stats_fd, stats_path = tempfile.mkstemp(prefix="psp_residency_")
os.close(stats_fd)
os.environ["PSP_MEMORY_BUDGET"] = "1024"
os.environ["PSP_RESIDENCY_STATS_FILE"] = stats_path
try:
n = 5000
data = {
"x": list(range(n)),
"y": [float(i) * 1.5 for i in range(n)],
"s": ["row_{}".format(i % 50) for i in range(n)],
}
mem = Table(data)
disk = Table(data, page_to_disk=True)
# Each request is a safepoint that trims to budget; data read back
# must be correct (round-tripped through evict -> restore).
assert disk.view().to_columns() == mem.view().to_columns()
assert disk.view().to_arrow() == mem.view().to_arrow()
upd = {
"x": list(range(n, n + 200)),
"y": [float(i) for i in range(n, n + 200)],
"s": ["upd_{}".format(i) for i in range(200)],
}
mem.update(upd)
disk.update(upd)
assert disk.view().to_columns() == mem.view().to_columns()
# An expression view on an evicted disk table must still compute.
exprs = {"e": '"x" + "y"'}
assert (
disk.view(expressions=exprs).to_columns()
== mem.view(expressions=exprs).to_columns()
)
# Confirm eviction actually occurred (otherwise the test is vacuous).
with open(stats_path) as f:
stats = f.read()
evictions = int(stats.split("evictions=")[1].split()[0])
assert evictions > 0, "expected evictions under a tiny budget: " + stats
finally:
os.environ.pop("PSP_MEMORY_BUDGET", None)
os.environ.pop("PSP_RESIDENCY_STATS_FILE", None)
os.remove(stats_path)
# Drain a safepoint with residency disabled so any evicted stores
# from other live tables are restored before subsequent tests.
Table({"_": [0]}).view().to_columns()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,251 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
from datetime import date, datetime
import numpy as np
import polars as pl
from pytest import mark
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
def arrow_bytes_to_polars(view):
import pyarrow
with pyarrow.ipc.open_stream(pyarrow.BufferReader(view.to_arrow())) as reader:
return pl.from_dataframe(reader.read_pandas())
class TestTablePolars(object):
def test_empty_table(self):
tbl = Table([])
assert tbl.size() == 0
assert tbl.schema() == {}
def test_table_dataframe(self):
d = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data = pl.DataFrame(d)
tbl = Table(data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "integer", "b": "integer"}
assert tbl.view().to_records() == [
{"a": 1, "b": 2},
{"a": 3, "b": 4},
]
def test_table_lazyframe(self):
d = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
data = pl.DataFrame(d).lazy()
tbl = Table(data)
assert tbl.size() == 2
assert tbl.schema() == {"a": "integer", "b": "integer"}
assert tbl.view().to_records() == [
{"a": 1, "b": 2},
{"a": 3, "b": 4},
]
def test_table_dataframe_column_order(self):
d = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
data = pl.DataFrame(d).select(["b", "c", "a", "d"])
tbl = Table(data)
assert tbl.size() == 2
assert tbl.columns() == ["b", "c", "a", "d"]
def test_table_dataframe_selective_column_order(self):
d = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 3, "b": 4, "c": 5, "d": 6}]
data = pl.DataFrame(d).select(["b", "c", "a"])
tbl = Table(data)
assert tbl.size() == 2
assert tbl.columns() == ["b", "c", "a"]
def test_table_dataframe_does_not_mutate(self):
# make sure we don't mutate the dataframe that a user passes in
data = pl.DataFrame(
{
"a": [None, 1, None, 2],
"b": [1.5, None, 2.5, None],
}
)
assert data["a"].to_list() == [None, 1, None, 2]
assert data["b"].to_list() == [1.5, None, 2.5, None]
tbl = Table(data)
assert tbl.size() == 4
assert tbl.schema() == {"a": "integer", "b": "float"}
assert data["a"].to_list() == [None, 1, None, 2]
assert data["b"].to_list() == [1.5, None, 2.5, None]
def test_table_polars_from_schema_int(self):
data = [None, 1, None, 2, None, 3, 4]
df = pl.DataFrame({"a": data})
table = Table({"a": "integer"})
table.update(df)
assert table.view().to_columns()["a"] == data
def test_table_polars_from_schema_bool(self):
data = [True, False, True, False]
df = pl.DataFrame({"a": data})
table = Table({"a": "boolean"})
table.update(df)
assert table.view().to_columns()["a"] == data
def test_table_polars_from_schema_float(self):
data = [None, 1.5, None, 2.5, None, 3.5, 4.5]
df = pl.DataFrame({"a": data})
table = Table({"a": "float"})
table.update(df)
assert table.view().to_columns()["a"] == data
def test_table_polars_from_schema_float_all_nan(self):
data = [np.nan, np.nan, np.nan, np.nan]
df = pl.DataFrame({"a": data})
table = Table({"a": "float"})
table.update(df)
assert table.view().to_columns()["a"] == [None, None, None, None]
def test_table_polars_from_schema_float_to_int(self):
data = [None, 1.5, None, 2.5, None, 3.5, 4.5]
df = pl.DataFrame({"a": data})
table = Table({"a": "integer"})
table.update(df)
# truncates decimal
assert table.view().to_columns()["a"] == [None, 1, None, 2, None, 3, 4]
def test_table_polars_from_schema_int_to_float(self):
data = [None, 1, None, 2, None, 3, 4]
df = pl.DataFrame({"a": data})
table = Table({"a": "float"})
table.update(df)
assert table.view().to_columns()["a"] == [None, 1.0, None, 2.0, None, 3.0, 4.0]
def test_table_polars_from_schema_date(self, util):
data = [date(2019, 8, 15), None, date(2019, 8, 16)]
df = pl.DataFrame({"a": data})
table = Table({"a": "date"})
table.update(df)
assert table.view().to_columns()["a"] == [
util.to_timestamp(datetime(2019, 8, 15)),
None,
util.to_timestamp(datetime(2019, 8, 16)),
]
def test_table_polars_from_schema_str(self):
data = ["a", None, "b", None, "c"]
df = pl.DataFrame({"a": data})
table = Table({"a": "string"})
table.update(df)
assert table.view().to_columns()["a"] == data
def test_table_polars_none(self):
data = [None, None, None]
df = pl.DataFrame({"a": data})
table = Table(df)
assert table.view().to_columns()["a"] == data
def test_table_polars_symmetric_table(self):
# make sure that updates are symmetric to table creation
df = pl.DataFrame({"a": [1, 2, 3, 4], "b": [1.5, 2.5, 3.5, 4.5]})
t1 = Table(df)
t2 = Table({"a": "integer", "b": "float"})
t2.update(df)
assert t1.view().to_columns() == {
"a": [1, 2, 3, 4],
"b": [1.5, 2.5, 3.5, 4.5],
}
def test_table_polars_symmetric_stacked_updates(self):
# make sure that updates are symmetric to table creation
df = pl.DataFrame({"a": [1, 2, 3, 4], "b": [1.5, 2.5, 3.5, 4.5]})
t1 = Table(df)
t1.update(df)
t2 = Table({"a": "integer", "b": "float"})
t2.update(df)
t2.update(df)
assert t1.view().to_columns() == {
"a": [1, 2, 3, 4, 1, 2, 3, 4],
"b": [1.5, 2.5, 3.5, 4.5, 1.5, 2.5, 3.5, 4.5],
}
@mark.skip(reason="Not supported, polars doesnt like input")
def test_table_polars_transitive(self):
# serialized output -> table -> serialized output
records = {
"a": [1, 2, 3, 4],
"b": [1.5, 2.5, 3.5, 4.5],
"c": [np.nan, np.nan, "abc", np.nan],
"d": [None, True, None, False],
"e": [
float("nan"),
datetime(2019, 7, 11, 12, 30),
float("nan"),
datetime(2019, 7, 11, 12, 30),
],
}
df = pl.DataFrame(records, strict=False)
t1 = Table(df)
out1 = arrow_bytes_to_polars(t1.view(columns=["a", "b", "c", "d", "e"]))
t2 = Table(out1)
assert t1.schema() == t2.schema()
out2 = t2.view().to_columns()
assert t1.view().to_columns() == out2
# dtype=object should have correct inferred types
def test_table_polars_object_to_int(self):
df = pl.DataFrame({"a": [1, 2, None, 2, None, 3, 4]})
table = Table(df)
assert table.schema() == {"a": "integer"}
assert table.view().to_columns()["a"] == [1, 2, None, 2, None, 3, 4]
def test_table_polars_object_to_float(self):
df = pl.DataFrame({"a": [None, 1, None, 2, None, 3, 4]})
table = Table(df)
assert table.schema() == {"a": "integer"}
assert table.view().to_columns()["a"] == [None, 1.0, None, 2.0, None, 3.0, 4.0]
def test_table_polars_object_to_bool(self):
df = pl.DataFrame({"a": [True, False, True, False, True, False]})
table = Table(df)
assert table.schema() == {"a": "boolean"}
assert table.view().to_columns()["a"] == [True, False, True, False, True, False]
def test_table_polars_object_to_datetime(self):
df = pl.DataFrame(
{
"a": [
datetime(2019, 7, 11, 1, 2, 3),
datetime(2019, 7, 12, 1, 2, 3),
None,
]
}
)
table = Table(df)
assert table.schema() == {"a": "datetime"}
assert table.view().to_columns()["a"] == [
datetime(2019, 7, 11, 1, 2, 3).timestamp() * 1000,
datetime(2019, 7, 12, 1, 2, 3).timestamp() * 1000,
None,
]
def test_table_polars_object_to_str(self):
df = pl.DataFrame({"a": np.array(["abc", "def", None, "ghi"], dtype=object)})
table = Table(df)
assert table.schema() == {"a": "string"}
assert table.view().to_columns()["a"] == ["abc", "def", None, "ghi"]
@@ -0,0 +1,130 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import pandas as pd
import numpy as np
from perspective import PerspectiveError
from datetime import date, datetime
from pytest import approx, mark, raises
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
def date_timestamp(date):
return int(datetime.combine(date, datetime.min.time()).timestamp()) * 1000
def compare_delta(received, expected):
"""Compare an arrow-serialized row delta by constructing a Table."""
tbl = Table(received)
assert tbl.view().to_columns() == expected
class TestView(object):
def test_view_zero(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
view = tbl.view()
tbl2 = Table(view)
view2 = tbl2.view()
dimms = view2.dimensions()
assert dimms["num_view_rows"] == 2
assert dimms["num_view_columns"] == 2
assert view2.schema() == {"a": "integer", "b": "integer"}
assert view2.to_records() == data
def test_view_one(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
view = tbl.view(group_by=["a"])
tbl2 = Table(view)
view2 = tbl2.view()
dimms = view2.dimensions()
assert dimms["num_view_rows"] == 3
assert dimms["num_view_columns"] == 3
assert view2.schema() == {
"a (Group by 1)": "integer",
"a": "integer",
"b": "integer",
}
assert view2.to_records() == [
{"a (Group by 1)": None, "a": 4, "b": 6},
{"a (Group by 1)": 1, "a": 1, "b": 2},
{"a (Group by 1)": 3, "a": 3, "b": 4},
]
def test_view_two(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
view = tbl.view(group_by=["a"], split_by=["b"])
tbl2 = Table(view)
view2 = tbl2.view()
dimms = view2.dimensions()
assert dimms["num_view_rows"] == 3
assert dimms["num_view_columns"] == 5
assert view2.schema() == {
"a (Group by 1)": "integer",
"2|a": "integer",
"2|b": "integer",
"4|a": "integer",
"4|b": "integer",
}
assert view2.to_records() == [
{
"a (Group by 1)": None,
"2|a": 1,
"2|b": 2,
"4|a": 3,
"4|b": 4,
},
{
"a (Group by 1)": 1,
"2|a": 1,
"2|b": 2,
"4|a": None,
"4|b": None,
},
{
"a (Group by 1)": 3,
"2|a": None,
"2|b": None,
"4|a": 3,
"4|b": 4,
},
]
def test_view_two_column_only(self):
data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
tbl = Table(data)
view = tbl.view(split_by=["b"])
tbl2 = Table(view)
view2 = tbl2.view()
dimms = view2.dimensions()
assert dimms["num_view_rows"] == 2
assert dimms["num_view_columns"] == 4
assert view2.schema() == {
"2|a": "integer",
"2|b": "integer",
"4|a": "integer",
"4|b": "integer",
}
assert view2.to_records() == [
{"2|a": 1, "2|b": 2, "4|a": None, "4|b": None},
{"2|a": None, "2|b": None, "4|a": 3, "4|b": 4},
]
@@ -0,0 +1,417 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import pyarrow as pa
from datetime import date, datetime
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestToArrow(object):
def test_to_arrow_nones_symmetric(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_big_numbers_symmetric(self):
data = {
"a": [1, 2, 3, 4],
"b": [
1.7976931348623157e308,
1.7976931348623157e308,
1.7976931348623157e308,
1.7976931348623157e308,
],
}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_boolean_symmetric(self):
data = {"a": [True, False, None, False, True, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "boolean"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_str_symmetric(self):
data = {"a": ["a", "b", "c", "d", "e", None]}
tbl = Table(data)
assert tbl.schema() == {"a": "string"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_str_dict(self):
data = {
"a": ["abcdefg", "abcdefg", "h"],
"b": ["aaa", "bbb", "bbb"],
"c": ["hello", "world", "world"],
}
tbl = Table(data)
assert tbl.schema() == {"a": "string", "b": "string", "c": "string"}
arr = tbl.view().to_arrow()
# assert that we are actually generating dict arrays
buf = pa.BufferReader(arr)
reader = pa.ipc.open_stream(buf)
arrow_table = reader.read_all()
arrow_schema = arrow_table.schema
for name in ("a", "b", "c"):
arrow_type = arrow_schema.field(name).type
assert pa.types.is_dictionary(arrow_type)
# assert that data is symmetrical
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_date_symmetric(self):
# data = {"a": [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)]}
# tbl = Table(data)
tbl = Table({"a": "date"})
# data = {"a": map(lambda x: x.getTime(), [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)])}
data = {"a": [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)]}
tbl.update(data)
assert tbl.schema() == {"a": "date"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
def ts(x):
return int(datetime.timestamp(x) * 1000)
assert tbl2.schema() == tbl.schema()
assert tbl2.view().to_columns() == {
"a": [
ts(datetime(2019, 7, 11)),
ts(datetime(2016, 2, 29)),
ts(datetime(2019, 12, 10)),
]
}
def test_to_arrow_date_symmetric_january(self, util):
data = {"a": [date(2019, 1, 1), date(2016, 1, 1), date(2019, 1, 1)]}
tbl = Table(data)
assert tbl.schema() == {"a": "date"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.schema() == tbl.schema()
assert tbl2.view().to_columns() == {
"a": [
util.to_timestamp(x)
for x in [
datetime(2019, 1, 1),
datetime(2016, 1, 1),
datetime(2019, 1, 1),
]
]
}
def test_to_arrow_datetime_symmetric(self, util):
data = {
"a": [
datetime(2019, 7, 11, 12, 30),
datetime(2016, 2, 29, 11, 0),
datetime(2019, 12, 10, 12, 0),
]
}
tbl = Table(data)
assert tbl.schema() == {"a": "datetime"}
arr = tbl.view().to_arrow()
tbl2 = Table(arr)
assert tbl2.schema() == tbl.schema()
assert tbl2.view().to_columns() == {
"a": [
util.to_timestamp(x)
for x in [
datetime(2019, 7, 11, 12, 30),
datetime(2016, 2, 29, 11, 0),
datetime(2019, 12, 10, 12, 0),
]
]
}
def test_to_arrow_one_symmetric(self):
data = {
"a": [1, 2, 3, 4],
"b": ["a", "b", "c", "d"],
"c": [
datetime(2019, 7, 11, 12, 0),
datetime(2019, 7, 11, 12, 10),
datetime(2019, 7, 11, 12, 20),
datetime(2019, 7, 11, 12, 30),
],
}
tbl = Table(data)
view = tbl.view(group_by=["a"])
arrow = view.to_arrow()
tbl2 = Table(arrow)
assert tbl2.schema() == {
"a (Group by 1)": "integer",
"a": "integer",
"b": "integer",
"c": "integer",
}
d = view.to_columns()
d["a (Group by 1)"] = [
x[0] if len(x) > 0 else None for x in d.pop("__ROW_PATH__")
]
assert tbl2.view().to_columns() == d
def test_to_arrow_two_symmetric(self):
data = {
"a": [1, 2, 3, 4],
"b": ["hello", "world", "hello2", "world2"],
"c": [datetime(2019, 7, 11, 12, i) for i in range(0, 40, 10)],
}
tbl = Table(data)
view = tbl.view(group_by=["a"], split_by=["b"])
arrow = view.to_arrow()
tbl2 = Table(arrow)
assert tbl2.schema() == {
"a (Group by 1)": "integer",
"hello|a": "integer",
"hello|b": "integer",
"hello|c": "integer",
"world|a": "integer",
"world|b": "integer",
"world|c": "integer",
"hello2|a": "integer",
"hello2|b": "integer",
"hello2|c": "integer",
"world2|a": "integer",
"world2|b": "integer",
"world2|c": "integer",
}
d = view.to_columns()
d["a (Group by 1)"] = [
x[0] if len(x) > 0 else None for x in d.pop("__ROW_PATH__")
]
assert tbl2.view().to_columns() == d
def test_to_arrow_column_only_symmetric(self):
data = {
"a": [1, 2, 3, 4],
"b": ["a", "b", "c", "d"],
"c": [datetime(2019, 7, 11, 12, i) for i in range(0, 40, 10)],
}
tbl = Table(data)
view = tbl.view(split_by=["a"])
arrow = view.to_arrow()
tbl2 = Table(arrow)
assert tbl2.schema() == {
"1|a": "integer",
"1|b": "string",
"1|c": "datetime",
"2|a": "integer",
"2|b": "string",
"2|c": "datetime",
"3|a": "integer",
"3|b": "string",
"3|c": "datetime",
"4|a": "integer",
"4|b": "string",
"4|c": "datetime",
}
d = view.to_columns()
assert tbl2.view().to_columns() == d
# start and end row
def test_to_arrow_start_row(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_row=3)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"a": data["a"][3:], "b": data["b"][3:]}
def test_to_arrow_end_row(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(end_row=2)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"a": data["a"][:2], "b": data["b"][:2]}
def test_to_arrow_start_end_row(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_row=2, end_row=3)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"a": data["a"][2:3], "b": data["b"][2:3]}
def test_to_arrow_start_end_row_equiv(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_row=2, end_row=2)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"a": [], "b": []}
def test_to_arrow_start_row_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_row=-1)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_end_row_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(end_row=6)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_start_end_row_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_row=-1, end_row=6)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_start_col(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_col=1)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"b": data["b"]}
def test_to_arrow_end_col(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(end_col=1)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"a": data["a"]}
def test_to_arrow_start_end_col(self):
data = {
"a": [None, 1, None, 2, 3],
"b": [1.5, 2.5, None, 3.5, None],
"c": [None, 1, None, 2, 3],
"d": [1.5, 2.5, None, 3.5, None],
}
tbl = Table(data)
assert tbl.schema() == {
"a": "integer",
"b": "float",
"c": "integer",
"d": "float",
}
arr = tbl.view().to_arrow(start_col=1, end_col=3)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {"b": data["b"], "c": data["c"]}
def test_to_arrow_start_col_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_col=-1)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_end_col_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(end_col=6)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_start_end_col_invalid(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_col=-1, end_col=6)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == data
def test_to_arrow_start_end_col_equiv_row(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_col=1, end_col=1, start_row=2, end_row=3)
tbl2 = Table(arr)
# start/end col is a range - thus start=end provides no columns
assert tbl2.view().to_columns() == {}
def test_to_arrow_start_end_col_equiv(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(start_col=1, end_col=1)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == {}
def test_to_arrow_start_end_row_end_col(self):
data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float"}
arr = tbl.view().to_arrow(end_col=1, start_row=2, end_row=3)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == tbl.view().to_columns(
end_col=1, start_row=2, end_row=3
)
def test_to_arrow_start_end_col_start_row(self):
data = {
"a": [None, 1, None, 2, 3],
"b": [1.5, 2.5, None, 3.5, None],
"c": [1.5, 2.5, None, 4.5, None],
}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float", "c": "float"}
arr = tbl.view().to_arrow(start_col=1, end_col=2, start_row=2)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == tbl.view().to_columns(
start_col=1, end_col=2, start_row=2
)
def test_to_arrow_start_end_col_end_row(self):
data = {
"a": [None, 1, None, 2, 3],
"b": [1.5, 2.5, None, 3.5, None],
"c": [1.5, 2.5, None, 4.5, None],
}
tbl = Table(data)
assert tbl.schema() == {"a": "integer", "b": "float", "c": "float"}
arr = tbl.view().to_arrow(start_col=1, end_col=2, end_row=2)
tbl2 = Table(arr)
assert tbl2.view().to_columns() == tbl.view().to_columns(
start_col=1, end_col=2, end_row=2
)
def test_to_arrow_one_mean(self):
data = {"a": [1, 2, 3, 4], "b": ["a", "a", "b", "b"]}
table = Table(data)
view = table.view(group_by=["b"], columns=["a"], aggregates={"a": "mean"})
arrow = view.to_arrow()
table2 = Table(arrow)
view2 = table2.view()
result = view2.to_columns()
assert result == {"b (Group by 1)": [None, "a", "b"], "a": [2.5, 1.5, 3.5]}
@@ -0,0 +1,32 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestToArrowLZ4(object):
def test_to_arrow_lz4_roundtrip(self, superstore):
original_tbl = Table(superstore.to_dict(orient="records"))
arrow_uncompressed = original_tbl.view().to_arrow(compression=None)
tbl = Table(arrow_uncompressed)
arr = tbl.view().to_arrow(compression="lz4")
assert len(arr) < len(arrow_uncompressed)
tbl2 = Table(arr)
arr2 = tbl2.view().to_arrow(compression=None)
assert len(arr2) > len(arr)
tbl3 = Table(arr)
arr3 = tbl3.view().to_arrow(compression="lz4")
assert len(arr3) == len(arr)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestToPolars(object):
def test_to_polars(self, superstore):
original_tbl = Table(superstore.to_dict(orient="records"))
df = original_tbl.view().to_polars()
tbl = Table(df)
assert tbl.size() == original_tbl.size()
df2 = tbl.view().to_polars()
assert df.equals(df2)
@@ -0,0 +1,536 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import numpy as np
import pyarrow as pa
import pandas as pd
from datetime import date, datetime
from pytest import mark
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestUpdate(object):
@mark.skip(reason="Fix for null values update yet to be implemented")
def test_update_with_missing_or_null_values(self):
tbl = Table([{"a": "1", "b": "2"}, {"a": "3", "b": "4"}], index="a")
tbl.update([{"a": "1", "b": None}])
assert tbl.view().to_records() == [{"a": "1", "b": None}, {"a": "3", "b": "4"}]
data = pd.DataFrame({"a": ["1"], "b": [None]})
arrow_table = pa.Table.from_pandas(data, preserve_index=False)
tbl.update(arrow_table)
assert tbl.size() == 2
assert tbl.view().to_records() == [{"a": "1", "b": ""}, {"a": "3", "b": "4"}]
def test_update_from_schema(self):
tbl = Table({"a": "string", "b": "integer"})
tbl.update([{"a": "abc", "b": 123}])
assert tbl.view().to_records() == [{"a": "abc", "b": 123}]
def test_update_columnar_from_schema(self):
tbl = Table({"a": "string", "b": "integer"})
tbl.update({"a": ["abc"], "b": [123]})
assert tbl.view().to_records() == [{"a": "abc", "b": 123}]
def test_update_csv(self):
tbl = Table({"a": "string", "b": "integer"})
view = tbl.view()
tbl.update("a,b\nxyz,123\ndef,100000000")
assert view.to_columns() == {"a": ["xyz", "def"], "b": [123, 100000000]}
def test_update_csv_indexed(self):
tbl = Table({"a": "string", "b": "float"}, index="a")
view = tbl.view()
tbl.update("a,b\nxyz,1.23456718\ndef,100000000.1")
assert view.to_columns() == {
"a": ["def", "xyz"],
"b": [100000000.1, 1.23456718],
}
tbl.update("a,b\nxyz,0.00000001\ndef,1234.5678\nefg,100.2")
assert view.to_columns() == {
"a": ["def", "efg", "xyz"],
"b": [1234.5678, 100.2, 0.00000001],
}
def test_update_append(self):
tbl = Table([{"a": "abc", "b": 123}])
tbl.update([{"a": "def", "b": 456}])
assert tbl.view().to_records() == [
{"a": "abc", "b": 123},
{"a": "def", "b": 456},
]
def test_update_partial(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
tbl.update([{"a": "abc", "b": 456}])
assert tbl.view().to_records() == [{"a": "abc", "b": 456}]
def test_update_partial_add_row(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
tbl.update([{"a": "abc", "b": 456}, {"a": "def"}])
assert tbl.view().to_records() == [
{"a": "abc", "b": 456},
{"a": "def", "b": None},
]
def test_update_partial_noop(self):
tbl = Table([{"a": "abc", "b": 1, "c": 2}], index="a")
tbl.update([{"a": "abc", "b": 456}])
assert tbl.view().to_records() == [{"a": "abc", "b": 456, "c": 2}]
def test_update_partial_unset(self):
tbl = Table(
[{"a": "abc", "b": 1, "c": 2}, {"a": "def", "b": 3, "c": 4}], index="a"
)
tbl.update([{"a": "abc"}, {"a": "def", "c": None}])
assert tbl.view().to_records() == [
{"a": "abc", "b": 1, "c": 2},
{"a": "def", "b": 3, "c": None},
]
def test_update_columnar_partial_add_row(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
tbl.update({"a": ["abc", "def"], "b": [456, None]})
assert tbl.view().to_records() == [
{"a": "abc", "b": 456},
{"a": "def", "b": None},
]
def test_update_columnar_partial_noop(self):
tbl = Table([{"a": "abc", "b": 1, "c": 2}], index="a")
# no-op because "c" is not in the update dataset
tbl.update({"a": ["abc"], "b": [456]})
assert tbl.view().to_records() == [{"a": "abc", "b": 456, "c": 2}]
def test_update_columnar_partial_unset(self):
tbl = Table(
[{"a": "abc", "b": 1, "c": 2}, {"a": "def", "b": 3, "c": 4}], index="a"
)
tbl.update({"a": ["abc"], "b": [None]})
assert tbl.view().to_records() == [
{"a": "abc", "b": None, "c": 2},
{"a": "def", "b": 3, "c": 4},
]
def test_update_partial_subcolumn(self):
tbl = Table([{"a": "abc", "b": 123, "c": 456}], index="a")
tbl.update([{"a": "abc", "c": 1234}])
assert tbl.view().to_records() == [{"a": "abc", "b": 123, "c": 1234}]
def test_update_partial_subcolumn_dict(self):
tbl = Table([{"a": "abc", "b": 123, "c": 456}], index="a")
tbl.update({"a": ["abc"], "c": [1234]})
assert tbl.view().to_records() == [{"a": "abc", "b": 123, "c": 1234}]
def test_update_partial_cols_more_columns_than_table(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
tbl.update([{"a": "abc", "b": 456, "c": 789}])
assert tbl.view().to_records() == [{"a": "abc", "b": 456}]
def test_update_columnar_append(self):
tbl = Table({"a": ["abc"], "b": [123]})
tbl.update({"a": ["def"], "b": [456]})
assert tbl.view().to_records() == [
{"a": "abc", "b": 123},
{"a": "def", "b": 456},
]
def test_update_columnar_partial(self):
tbl = Table({"a": ["abc"], "b": [123]}, index="a")
tbl.update({"a": ["abc"], "b": [456]})
assert tbl.view().to_records() == [{"a": "abc", "b": 456}]
# make sure already created views are notified properly
def test_update_from_schema_notify(self):
tbl = Table({"a": "string", "b": "integer"})
view = tbl.view()
assert view.num_rows() == 0
tbl.update([{"a": "abc", "b": 123}])
assert view.to_records() == [{"a": "abc", "b": 123}]
def test_update_columnar_from_schema_notify(self):
tbl = Table({"a": "string", "b": "integer"})
view = tbl.view()
assert view.num_rows() == 0
tbl.update({"a": ["abc"], "b": [123]})
assert view.to_records() == [{"a": "abc", "b": 123}]
def test_update_append_notify(self):
tbl = Table([{"a": "abc", "b": 123}])
view = tbl.view()
assert view.num_rows() == 1
tbl.update([{"a": "def", "b": 456}])
assert view.to_records() == [{"a": "abc", "b": 123}, {"a": "def", "b": 456}]
def test_update_partial_notify(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
view = tbl.view()
assert view.num_rows() == 1
tbl.update([{"a": "abc", "b": 456}])
assert view.to_records() == [{"a": "abc", "b": 456}]
def test_update_partial_cols_not_in_schema_notify(self):
tbl = Table([{"a": "abc", "b": 123}], index="a")
view = tbl.view()
assert view.num_rows() == 1
tbl.update([{"a": "abc", "b": 456, "c": 789}])
assert view.to_records() == [{"a": "abc", "b": 456}]
def test_update_columnar_append_notify(self):
tbl = Table({"a": ["abc"], "b": [123]})
view = tbl.view()
assert view.num_rows() == 1
tbl.update({"a": ["def"], "b": [456]})
assert view.to_records() == [{"a": "abc", "b": 123}, {"a": "def", "b": 456}]
def test_update_columnar_partial_notify(self):
tbl = Table({"a": ["abc"], "b": [123]}, index="a")
view = tbl.view()
assert view.num_rows() == 1
tbl.update({"a": ["abc"], "b": [456]})
assert view.to_records() == [{"a": "abc", "b": 456}]
# bool
def test_update_bool_from_schema(self):
bool_data = [{"a": True, "b": False}, {"a": True, "b": True}]
tbl = Table({"a": "boolean", "b": "boolean"})
tbl.update(bool_data)
assert tbl.size() == 2
assert tbl.view().to_records() == bool_data
def test_update_bool_str_from_schema(self):
bool_data = [{"a": "True", "b": "False"}, {"a": "True", "b": "True"}]
tbl = Table({"a": "boolean", "b": "boolean"})
tbl.update(bool_data)
assert tbl.size() == 2
assert tbl.view().to_records() == [
{"a": True, "b": False},
{"a": True, "b": True},
]
@mark.skip(reason="We may not want to support this")
def test_update_bool_str_all_formats_from_schema(self):
bool_data = [
{"a": "True", "b": "False"},
{"a": "t", "b": "f"},
{"a": "true", "b": "false"},
{"a": 1, "b": 0},
{"a": "on", "b": "off"},
]
tbl = Table({"a": "boolean", "b": "boolean"})
tbl.update(bool_data)
assert tbl.size() == 5
assert tbl.view().to_columns() == {
"a": [True, True, True, True, True],
"b": [False, False, False, False, False],
}
def test_update_bool_int_from_schema(self):
bool_data = [{"a": 1, "b": 0}, {"a": 1, "b": 0}]
tbl = Table({"a": "boolean", "b": "boolean"})
tbl.update(bool_data)
assert tbl.size() == 2
assert tbl.view().to_columns() == {"a": [True, True], "b": [False, False]}
# dates and datetimes
def test_update_date(self, util):
tbl = Table({"a": [date(2019, 7, 11)]})
tbl.update([{"a": date(2019, 7, 12)}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11))},
{"a": util.to_timestamp(datetime(2019, 7, 12))},
]
@mark.skip # We do not support numpy.
def test_update_date_np(self, util):
tbl = Table({"a": [date(2019, 7, 11)]})
tbl.update([{"a": np.datetime64(date(2019, 7, 12))}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11))},
{"a": util.to_timestamp(datetime(2019, 7, 12))},
]
def test_update_datetime(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": datetime(2019, 7, 12, 11, 0)}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))},
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))},
]
@mark.skip # We do not support numpy.
def test_update_datetime_np(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0))}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))},
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))},
]
@mark.skip # We do not support numpy.
def test_update_datetime_np_ts(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": np.datetime64("2019-07-12T11:00")}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))},
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))},
]
def test_update_datetime_timestamp_seconds(self, util):
ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0))
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": ts}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))},
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))},
]
def test_update_datetime_timestamp_ms(self, util):
ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0))
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": ts}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))},
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))},
]
# partial date & datetime updates
def test_update_date_partial(self, util):
tbl = Table({"a": [date(2019, 7, 11)], "b": [1]}, index="b")
tbl.update([{"a": date(2019, 7, 12), "b": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 1}
]
@mark.skip # We do not support numpy anymore.
def test_update_date_np_partial(self, util):
tbl = Table({"a": [date(2019, 7, 11)], "b": [1]}, index="b")
tbl.update([{"a": np.datetime64(date(2019, 7, 12)), "b": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 1}
]
def test_update_datetime_partial(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b")
tbl.update([{"a": datetime(2019, 7, 12, 11, 0), "b": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1}
]
@mark.skip # We do not support numpy anymore.
def test_update_datetime_np_partial(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b")
tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0)), "b": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1}
]
@mark.skip # We do not support numpy anymore.
def test_update_datetime_np_ts_partial(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b")
tbl.update([{"a": np.datetime64("2019-07-12T11:00"), "b": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1}
]
def test_update_datetime_timestamp_seconds_partial(self, util):
ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0))
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "idx": [1]}, index="idx")
tbl.update([{"a": ts, "idx": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "idx": 1}
]
def test_update_datetime_timestamp_ms_partial(self, util):
ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0))
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "idx": [1]}, index="idx")
tbl.update([{"a": ts, "idx": 1}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "idx": 1}
]
# updating dates using implicit index
def test_update_date_partial_implicit(self, util):
tbl = Table({"a": [date(2019, 7, 11)]})
tbl.update([{"a": date(2019, 7, 12), "__INDEX__": 0}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12))}
]
@mark.skip # We do not support numpy anymore.
def test_update_date_np_partial_implicit(self, util):
tbl = Table({"a": [date(2019, 7, 11)]})
tbl.update([{"a": np.datetime64(date(2019, 7, 12)), "__INDEX__": 0}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12))}
]
def test_update_datetime_partial_implicit(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": datetime(2019, 7, 12, 11, 0), "__INDEX__": 0}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}
]
@mark.skip # We do not support numpy anymore.
def test_update_datetime_np_partial_implicit(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0)), "__INDEX__": 0}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}
]
@mark.skip # We do not support numpy anymore.
def test_update_datetime_np_ts_partial_implicit(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
tbl.update([{"a": np.datetime64("2019-07-12T11:00"), "__INDEX__": 0}])
assert tbl.view().to_records() == [
{"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}
]
# implicit index
def test_update_implicit_index(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data)
view = tbl.view()
tbl.update([{"__INDEX__": 0, "a": 3, "b": 15}])
assert view.to_records() == [{"a": 3, "b": 15}, {"a": 2, "b": 3}]
def test_update_implicit_index_dict_noop(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data)
view = tbl.view()
# "b" does not exist in dataset, so no-op
tbl.update(
{
"__INDEX__": [0],
"a": [3],
}
)
assert view.to_records() == [{"a": 3, "b": 2}, {"a": 2, "b": 3}]
def test_update_implicit_index_dict_unset_with_null(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data)
view = tbl.view()
# unset because "b" is null
tbl.update({"__INDEX__": [0], "a": [3], "b": [None]})
assert view.to_records() == [{"a": 3, "b": None}, {"a": 2, "b": 3}]
def test_update_implicit_index_multi(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 4, "b": 5}]
tbl = Table(data)
view = tbl.view()
tbl.update(
[
{
"__INDEX__": 0,
"a": 3,
},
{"__INDEX__": 2, "a": 5},
]
)
assert view.to_records() == [
{"a": 3, "b": 2},
{"a": 2, "b": 3},
{"a": 5, "b": 5},
]
def test_update_implicit_index_symmetric(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data)
view = tbl.view()
records = view.to_records(index=True)
idx = records[0]["__INDEX__"][0] # XXX: How should we grab this?
tbl.update([{"__INDEX__": idx, "a": 3}])
assert view.to_records() == [{"a": 3, "b": 2}, {"a": 2, "b": 3}]
def test_update_explicit_index(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update([{"a": 1, "b": 3}])
assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}]
def test_update_explicit_index_multi(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update([{"a": 1, "b": 3}, {"a": 3, "b": 5}])
assert view.to_records() == [
{"a": 1, "b": 3},
{"a": 2, "b": 3},
{"a": 3, "b": 5},
]
def test_update_explicit_index_multi_append(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update([{"a": 1, "b": 3}, {"a": 12, "b": 5}])
assert view.to_records() == [
{"a": 1, "b": 3},
{"a": 2, "b": 3},
{"a": 3, "b": 4},
{"a": 12, "b": 5},
]
def test_update_explicit_index_multi_append_noindex(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update([{"a": 1, "b": 3}, {"b": 5}])
assert view.to_records() == [
{"a": None, "b": 5},
{"a": 1, "b": 3},
{"a": 2, "b": 3},
{"a": 3, "b": 4},
]
def test_update_implicit_index_with_explicit_unset(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update([{"__INDEX__": 1, "b": 3}])
assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}]
def test_update_implicit_index_with_explicit_set(self):
data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
tbl = Table(data, index="a")
view = tbl.view()
tbl.update(
[{"__INDEX__": 1, "a": 1, "b": 3}]
) # should ignore re-specification of pkey
assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}]
@@ -0,0 +1,980 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import os
import random
import uuid
import pyarrow as pa
import pandas as pd
import numpy as np
from datetime import date, datetime
from pytest import mark
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
SOURCE_STREAM_ARROW = os.path.join(
os.path.dirname(__file__), "arrow", "int_float_str.arrow"
)
SOURCE_FILE_ARROW = os.path.join(
os.path.dirname(__file__), "arrow", "int_float_str.arrow"
)
PARTIAL_ARROW = os.path.join(
os.path.dirname(__file__), "arrow", "int_float_str_update.arrow"
)
DICT_ARROW = os.path.join(os.path.dirname(__file__), "arrow", "dict.arrow")
DICT_UPDATE_ARROW = os.path.join(
os.path.dirname(__file__), "arrow", "dict_update.arrow"
)
names = ["a", "b", "c", "d"]
class TestUpdateArrow(object):
# files
def test_update_arrow_updates_stream_file(self):
tbl = Table({"a": "integer", "b": "float", "c": "string"})
with open(SOURCE_STREAM_ARROW, mode="rb") as file: # b is important -> binary
tbl.update(file.read())
assert tbl.size() == 4
assert tbl.schema() == {"a": "integer", "b": "float", "c": "string"}
with open(SOURCE_FILE_ARROW, mode="rb") as file:
tbl.update(file.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4] * 2,
"b": [1.5, 2.5, 3.5, 4.5] * 2,
"c": ["a", "b", "c", "d"] * 2,
}
def test_update_arrow_partial_updates_file(self):
tbl = Table({"a": "integer", "b": "float", "c": "string"}, index="a")
with open(SOURCE_STREAM_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 4
with open(PARTIAL_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4],
"b": [100.5, 2.5, 3.5, 400.5],
"c": ["x", "b", "c", "y"],
}
def test_update_arrow_updates_dict_file(self):
tbl = Table({"a": "string", "b": "string"})
with open(DICT_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 5
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None, "abc", None, "update1", "update2"],
"b": ["klm", "hij", None, "hij", "klm", "update3", None, "update4"],
}
@mark.skip
def test_update_arrow_updates_dict_partial_file(self):
tbl = None
v = None
with open(DICT_ARROW, mode="rb") as src:
tbl = Table(src.read(), index="a")
v = tbl.view()
assert v.num_rows() == 2
assert v.to_columns() == {"a": ["abc", "def"], "b": ["klm", "hij"]}
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
v.num_rows() == 4
assert v.to_columns() == {
"a": ["abc", "def", "update1", "update2"],
"b": ["klm", "hij", None, "update4"],
}
# update with file arrow with more columns than in schema
def test_update_arrow_updates_more_columns_stream_file(self):
tbl = Table(
{
"a": "integer",
"b": "float",
}
)
with open(SOURCE_STREAM_ARROW, mode="rb") as file: # b is important -> binary
tbl.update(file.read())
assert tbl.size() == 4
assert tbl.schema() == {"a": "integer", "b": "float"}
with open(SOURCE_FILE_ARROW, mode="rb") as file:
tbl.update(file.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4] * 2,
"b": [1.5, 2.5, 3.5, 4.5] * 2,
}
def test_update_arrow_partial_updates_more_columns_file(self):
tbl = Table({"a": "integer", "c": "string"}, index="a")
with open(SOURCE_STREAM_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 4
with open(PARTIAL_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4],
"c": ["x", "b", "c", "y"],
}
def test_update_arrow_updates_dict_more_columns_file(self):
tbl = Table(
{
"a": "string",
}
)
with open(DICT_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 5
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None, "abc", None, "update1", "update2"]
}
@mark.skip
def test_update_arrow_updates_dict_more_columns_partial_file(self):
tbl = Table({"a": "string"}, index="a")
with open(DICT_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 4
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": ["abc", "def", "update1", "update2"]
}
# update with file arrow with less columns than in schema
def test_update_arrow_updates_less_columns_stream_file(self):
tbl = Table(
{
"a": "integer",
"x": "float",
}
)
with open(SOURCE_STREAM_ARROW, mode="rb") as file: # b is important -> binary
tbl.update(file.read())
assert tbl.size() == 4
assert tbl.schema() == {"a": "integer", "x": "float"}
with open(SOURCE_FILE_ARROW, mode="rb") as file:
tbl.update(file.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4] * 2,
"x": [None for i in range(8)],
}
def test_update_arrow_partial_updates_less_columns_file(self):
tbl = Table({"a": "integer", "x": "string"}, index="a")
with open(SOURCE_STREAM_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 4
with open(PARTIAL_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4],
"x": [None for i in range(4)],
}
def test_update_arrow_updates_dict_less_columns_file(self):
tbl = Table({"a": "string", "x": "string"})
with open(DICT_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 5
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": ["abc", "def", "def", None, "abc", None, "update1", "update2"],
"x": [None for i in range(8)],
}
@mark.skip
def test_update_arrow_updates_dict_less_columns_partial_file(self):
tbl = Table({"a": "string", "x": "string"}, index="a")
with open(DICT_ARROW, mode="rb") as src:
tbl.update(src.read())
assert tbl.size() == 4
with open(DICT_UPDATE_ARROW, mode="rb") as partial:
tbl.update(partial.read())
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": ["abc", "def", "update1", "update2"],
"x": [None for i in range(4)],
}
# update int schema with int
def test_update_arrow_update_int_schema_with_uint8(self, util):
array = [random.randint(0, 127) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint8)})
schema = pa.schema({"a": pa.uint8()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_uint16(self, util):
array = [random.randint(0, 32767) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint16)})
schema = pa.schema({"a": pa.uint16()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_uint32(self, util):
array = [random.randint(0, 2000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint32)})
schema = pa.schema({"a": pa.uint32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_uint64(self, util):
array = [random.randint(0, 20000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint64)})
schema = pa.schema({"a": pa.uint64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_int8(self, util):
array = [random.randint(-127, 127) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int8)})
schema = pa.schema({"a": pa.int8()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_int16(self, util):
array = [random.randint(-32767, 32767) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int16)})
schema = pa.schema({"a": pa.int16()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_int32(self, util):
array = [random.randint(-2000000, 2000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int32)})
schema = pa.schema({"a": pa.int32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_int_schema_with_int64(self, util):
array = [random.randint(-20000000, 20000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int64)})
schema = pa.schema({"a": pa.int64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == [x * 1.0 for x in array]
# updating float schema with int
def test_update_arrow_update_float_schema_with_uint8(self, util):
array = [random.randint(0, 127) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint8)})
schema = pa.schema({"a": pa.uint8()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_uint16(self, util):
array = [random.randint(0, 32767) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint16)})
schema = pa.schema({"a": pa.uint16()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_uint32(self, util):
array = [random.randint(0, 2000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint32)})
schema = pa.schema({"a": pa.uint32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_uint64(self, util):
array = [random.randint(0, 20000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.uint64)})
schema = pa.schema({"a": pa.uint64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_int8(self, util):
array = [random.randint(-127, 127) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int8)})
schema = pa.schema({"a": pa.int8()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_int16(self, util):
array = [random.randint(-32767, 32767) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int16)})
schema = pa.schema({"a": pa.int16()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_int32(self, util):
array = [random.randint(-2000000, 2000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int32)})
schema = pa.schema({"a": pa.int32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_int64(self, util):
array = [random.randint(-20000000, 20000000) for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.int64)})
schema = pa.schema({"a": pa.int64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
# updating int schema with float
def test_update_arrow_update_int_schema_with_float32(self, util):
array = [random.randint(-2000000, 2000000) * 0.5 for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.float32)})
schema = pa.schema({"a": pa.float32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == [int(x) for x in array]
def test_update_arrow_update_int_schema_with_float64(self, util):
array = [
random.randint(-20000000, 20000000) * random.random() for i in range(100)
]
data = pd.DataFrame({"a": np.array(array, dtype=np.float64)})
schema = pa.schema({"a": pa.float64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "integer"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == [int(x) for x in array]
# updating float schema with float
def test_update_arrow_update_float_schema_with_float32(self, util):
array = [random.randint(-2000000, 2000000) * 0.5 for i in range(100)]
data = pd.DataFrame({"a": np.array(array, dtype=np.float32)})
schema = pa.schema({"a": pa.float32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
def test_update_arrow_update_float_schema_with_float64(self, util):
array = [
random.randint(-20000000, 20000000) * random.random() for i in range(100)
]
data = pd.DataFrame({"a": np.array(array, dtype=np.float64)})
schema = pa.schema({"a": pa.float64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "float"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == array
# updating date schema
def test_update_arrow_update_date_schema_with_date32(self, util):
array = [date(2019, 2, i) for i in range(1, 11)]
data = pd.DataFrame({"a": array})
schema = pa.schema({"a": pa.date32()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "date"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == [
util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)
]
def test_update_arrow_update_date_schema_with_date64(self, util):
array = [date(2019, 2, i) for i in range(1, 11)]
data = pd.DataFrame({"a": array})
schema = pa.schema({"a": pa.date64()})
arrow = util.make_arrow_from_pandas(data, schema)
tbl = Table({"a": "date"})
tbl.update(arrow)
assert tbl.view().to_columns()["a"] == [
util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)
]
def test_update_arrow_update_datetime_schema_with_timestamp(self, util):
data = [
[datetime(2019, 2, i, 9) for i in range(1, 11)],
[datetime(2019, 2, i, 10) for i in range(1, 11)],
[datetime(2019, 2, i, 11) for i in range(1, 11)],
[datetime(2019, 2, i, 12) for i in range(1, 11)],
]
arrow_data = util.make_arrow(
names,
data,
types=[
pa.timestamp("s"),
pa.timestamp("ms"),
pa.timestamp("us"),
pa.timestamp("ns"),
],
)
tbl = Table(
{
"a": "datetime",
"b": "datetime",
"c": "datetime",
"d": "datetime",
}
)
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(d) for d in data[0]],
"b": [util.to_timestamp(d) for d in data[1]],
"c": [util.to_timestamp(d) for d in data[2]],
"d": [util.to_timestamp(d) for d in data[3]],
}
# streams
def test_update_arrow_updates_int_stream(self, util):
data = [list(range(10)) for i in range(4)]
arrow_data = util.make_arrow(names, data)
tbl = Table({"a": "integer", "b": "integer", "c": "integer", "d": "integer"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {
"a": data[0],
"b": data[1],
"c": data[2],
"d": data[3],
}
def test_update_arrow_updates_float_stream(self, util):
data = [[i for i in range(10)], [i * 1.5 for i in range(10)]]
arrow_data = util.make_arrow(["a", "b"], data)
tbl = Table(
{
"a": "integer",
"b": "float",
}
)
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {"a": data[0], "b": data[1]}
@mark.skip(reason="Decimal128 isn't part of our schema yet")
def test_update_arrow_updates_decimal128_stream(self, util):
data = [[i * 1000000000 for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.decimal128(10)])
tbl = Table({"a": "integer"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {"a": data[0]}
def test_update_arrow_updates_bool_stream(self, util):
data = [[True if i % 2 == 0 else False for i in range(10)]]
arrow_data = util.make_arrow(["a"], data)
tbl = Table({"a": "boolean"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {"a": data[0]}
def test_update_arrow_updates_date32_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date32()])
tbl = Table({"a": "date"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {
"a": [int(1000 * datetime(2019, 2, i).timestamp()) for i in range(1, 11)]
}
def test_update_arrow_updates_date64_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.date64()])
tbl = Table({"a": "date"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 2, i)) for i in range(1, 11)]
}
def test_update_arrow_updates_timestamp_all_formats_stream(self, util):
data = [
[datetime(2019, 2, i, 9) for i in range(1, 11)],
[datetime(2019, 2, i, 10) for i in range(1, 11)],
[datetime(2019, 2, i, 11) for i in range(1, 11)],
[datetime(2019, 2, i, 12) for i in range(1, 11)],
]
arrow_data = util.make_arrow(
names,
data,
types=[
pa.timestamp("s"),
pa.timestamp("ms"),
pa.timestamp("us"),
pa.timestamp("ns"),
],
)
tbl = Table(
{"a": "datetime", "b": "datetime", "c": "datetime", "d": "datetime"}
)
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(d) for d in data[0]],
"b": [util.to_timestamp(d) for d in data[1]],
"c": [util.to_timestamp(d) for d in data[2]],
"d": [util.to_timestamp(d) for d in data[3]],
}
def test_update_arrow_updates_string_stream(self, util):
data = [[str(i) for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.string()])
tbl = Table({"a": "string"})
tbl.update(arrow_data)
assert tbl.size() == 10
assert tbl.view().to_columns() == {"a": data[0]}
def test_update_arrow_updates_dictionary_stream(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table({"a": "string", "b": "string"})
tbl.update(arrow_data)
assert tbl.size() == 4
assert tbl.view().to_columns() == {
"a": ["a", "b", "b", None],
"b": ["x", "y", None, "z"],
}
@mark.skip(reason="Arrow no longer supports partial updates per row")
def test_update_arrow_partial_updates_dictionary_stream(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table({"a": "string", "b": "string"}, index="a")
tbl.update(arrow_data)
assert tbl.size() == 3
assert tbl.view().to_columns() == {"a": [None, "a", "b"], "b": ["z", "x", "y"]}
@mark.skip
def test_update_arrow_partial_updates_dictionary_stream_duplicates(self, util):
"""If there are duplicate values in the dictionary, primary keys
may be duplicated if the column is used as an index. Skip this test
for now - still looking for the best way to fix."""
data = [
([0, 1, 1, None, 2], ["a", "b", "a"]),
([0, 1, None, 2, 1], ["x", "y", "z"]),
]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table({"a": "string", "b": "string"}, index="a")
tbl.update(arrow_data)
assert tbl.size() == 3
assert tbl.view().to_columns() == {"a": [None, "a", "b"], "b": ["z", "x", "y"]}
def test_update_arrow_partial_updates_more_columns_dictionary_stream(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table({"a": "string"}, index="a")
tbl.update(arrow_data)
assert tbl.size() == 3
assert tbl.view().to_columns() == {"a": [None, "a", "b"]}
@mark.skip(reason="Arrow no longer supports partial updates per row")
def test_update_arrow_partial_updates_less_columns_dictionary_stream(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table({"a": "string", "b": "string", "x": "string"}, index="a")
tbl.update(arrow_data)
assert tbl.size() == 3
assert tbl.view().to_columns() == {
"a": [None, "a", "b"],
"b": ["z", "x", "y"],
"x": [None, None, None],
}
def test_update_arrow_arbitary_order(self, util):
data = [[1, 2, 3, 4], ["a", "b", "c", "d"], [1, 2, 3, 4], ["a", "b", "c", "d"]]
update_data = [[5, 6], ["e", "f"], [5, 6], ["e", "f"]]
arrow = util.make_arrow(["a", "b", "c", "d"], data)
update_arrow = util.make_arrow(["c", "b", "a", "d"], update_data)
tbl = Table(arrow)
assert tbl.schema() == {
"a": "integer",
"b": "string",
"c": "integer",
"d": "string",
}
tbl.update(update_arrow)
assert tbl.size() == 6
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4, 5, 6],
"b": ["a", "b", "c", "d", "e", "f"],
"c": [1, 2, 3, 4, 5, 6],
"d": ["a", "b", "c", "d", "e", "f"],
}
# append
def test_update_arrow_updates_append_int_stream(self, util):
data = [list(range(10)) for i in range(4)]
arrow_data = util.make_arrow(names, data)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {
"a": data[0] + data[0],
"b": data[1] + data[1],
"c": data[2] + data[2],
"d": data[3] + data[3],
}
def test_update_arrow_updates_append_float_stream(self, util):
data = [[i for i in range(10)], [i * 1.5 for i in range(10)]]
arrow_data = util.make_arrow(["a", "b"], data)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {
"a": data[0] + data[0],
"b": data[1] + data[1],
}
def test_update_arrow_updates_append_decimal_stream(self, util):
data = [[i * 1000 for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.decimal128(4)])
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {"a": data[0] + data[0]}
def test_update_arrow_updates_append_bool_stream(self, util):
data = [[True if i % 2 == 0 else False for i in range(10)]]
arrow_data = util.make_arrow(["a"], data)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {"a": data[0] + data[0]}
def test_update_arrow_updates_append_date32_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
out_data = [datetime(2019, 2, i) for i in range(1, 11)]
arrow_data = util.make_arrow(["a"], data, types=[pa.date32()])
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(d) for d in out_data + out_data]
}
def test_update_arrow_updates_append_date64_stream(self, util):
data = [[date(2019, 2, i) for i in range(1, 11)]]
out_data = [datetime(2019, 2, i) for i in range(1, 11)]
arrow_data = util.make_arrow(["a"], data, types=[pa.date64()])
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(d) for d in out_data + out_data]
}
def test_update_arrow_updates_append_timestamp_all_formats_stream(self, util):
data = [
[datetime(2019, 2, i, 9) for i in range(1, 11)],
[datetime(2019, 2, i, 10) for i in range(1, 11)],
[datetime(2019, 2, i, 11) for i in range(1, 11)],
[datetime(2019, 2, i, 12) for i in range(1, 11)],
]
arrow_data = util.make_arrow(
names,
data,
types=[
pa.timestamp("s"),
pa.timestamp("ms"),
pa.timestamp("us"),
pa.timestamp("ns"),
],
)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(d) for d in data[0] + data[0]],
"b": [util.to_timestamp(d) for d in data[1] + data[1]],
"c": [util.to_timestamp(d) for d in data[2] + data[2]],
"d": [util.to_timestamp(d) for d in data[3] + data[3]],
}
def test_update_arrow_updates_append_string_stream(self, util):
data = [[str(i) for i in range(10)]]
arrow_data = util.make_arrow(["a"], data, types=[pa.string()])
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 20
assert tbl.view().to_columns() == {"a": data[0] + data[0]}
def test_update_arrow_updates_append_dictionary_stream(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": ["a", "b", "b", None, "a", "b", "b", None],
"b": ["x", "y", None, "z", "x", "y", None, "z"],
}
def test_update_arrow_updates_append_dictionary_stream_legacy(self, util):
data = [([0, 1, 1, None], ["a", "b"]), ([0, 1, None, 2], ["x", "y", "z"])]
arrow_data = util.make_dictionary_arrow(["a", "b"], data, legacy=True)
tbl = Table(arrow_data)
tbl.update(arrow_data)
assert tbl.size() == 8
assert tbl.view().to_columns() == {
"a": ["a", "b", "b", None, "a", "b", "b", None],
"b": ["x", "y", None, "z", "x", "y", None, "z"],
}
# indexed
def test_update_arrow_partial_indexed(self, util):
data = [[1, 2, 3, 4], ["a", "b", "c", "d"]]
update_data = [[2, 4], ["x", "y"]]
arrow = util.make_arrow(["a", "b"], data)
update_arrow = util.make_arrow(["a", "b"], update_data)
tbl = Table(arrow, index="a")
assert tbl.schema() == {"a": "integer", "b": "string"}
tbl.update(update_arrow)
assert tbl.size() == 4
assert tbl.view().to_columns() == {"a": [1, 2, 3, 4], "b": ["a", "x", "c", "y"]}
# update specific columns
def test_update_arrow_specific_column(self, util):
data = [[1, 2, 3, 4], ["a", "b", "c", "d"]]
update_data = [[2, 3, 4]]
arrow = util.make_arrow(["a", "b"], data)
update_arrow = util.make_arrow(["a"], update_data)
tbl = Table(arrow)
assert tbl.schema() == {"a": "integer", "b": "string"}
tbl.update(update_arrow)
assert tbl.size() == 7
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4, 2, 3, 4],
"b": ["a", "b", "c", "d", None, None, None],
}
# try to fuzz column order
def test_update_arrow_column_order_str(self, util):
# use str so it doesn't get promoted
data = [["a", "b", "c"] for i in range(10)]
names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
names_scrambled = names[::-1]
arrow = util.make_arrow(names_scrambled, data)
tbl = Table({name: "string" for name in names})
tbl.update(arrow)
assert tbl.size() == 3
assert tbl.view().to_columns() == {name: data[0] for name in names}
def test_update_arrow_column_order_int(self, util):
data = [[1, 2, 3] for i in range(10)]
names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
names_scrambled = names[::-1]
arrow = util.make_arrow(names_scrambled, data)
tbl = Table({name: "integer" for name in names})
tbl.update(arrow)
assert tbl.size() == 3
assert tbl.view().to_columns() == {name: data[0] for name in names}
def test_update_arrow_thread_safe_int_index(self, util):
data = [["a", "b", "c"] for i in range(10)]
data += [[1, 2, 3]]
names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "uid"]
arrow = util.make_arrow(names, data)
tbl = Table(arrow, index="uid")
for i in range(100):
idx = (1, 2, 3)[random.randint(0, 2)]
update_data = [
[str(uuid.uuid4()) + str(random.randint(100, 1000000000))],
[idx],
]
update_names = [names[random.randint(0, 9)], "uid"]
update_arrow = util.make_arrow(update_names, update_data)
tbl.update(update_arrow)
assert tbl.size() == 3
def test_update_arrow_thread_safe_datetime_index(self, util):
data = [["a", "b", "c"] for i in range(10)]
data += [
[
datetime(2020, 1, 15, 12, 17),
datetime(2020, 1, 15, 12, 18),
datetime(2020, 1, 15, 12, 19),
]
]
names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "uid"]
arrow = util.make_arrow(names, data)
tbl = Table(arrow, index="uid")
for i in range(100):
idx = (
datetime(2020, 1, 15, 12, 17),
datetime(2020, 1, 15, 12, 18),
datetime(2020, 1, 15, 12, 19),
)[random.randint(0, 2)]
update_data = [
[str(uuid.uuid4()) + str(random.randint(100, 1000000000))],
[idx],
]
update_names = [names[random.randint(0, 9)], "uid"]
update_arrow = util.make_arrow(update_names, update_data)
tbl.update(update_arrow)
assert tbl.size() == 3
def test_update_arrow_thread_safe_str_index(self, util):
data = [["a", "b", "c"] for i in range(11)]
names = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "uid"]
arrow = util.make_arrow(names, data)
tbl = Table(arrow, index="uid")
for i in range(100):
idx = ("a", "b", "c")[random.randint(0, 2)]
update_data = [
[str(uuid.uuid4()) + str(random.randint(100, 1000000000))],
[idx],
]
update_names = [names[random.randint(0, 9)], "uid"]
update_arrow = util.make_arrow(update_names, update_data)
tbl.update(update_arrow)
assert tbl.size() == 3
@@ -0,0 +1,211 @@
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
# ┃ This file is part of the Perspective library, distributed under the terms ┃
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import numpy as np
import pandas as pd
from datetime import date, datetime
from pytest import mark
import pytest
import perspective as psp
client = psp.Server().new_local_client()
Table = client.table
class TestUpdatePandas(object):
def test_update_df(self):
tbl = Table({"a": [1, 2, 3, 4]})
update_data = pd.DataFrame({"a": [5, 6, 7, 8]})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [1, 2, 3, 4, 5, 6, 7, 8]}
def test_update_df_i32_vs_i64(self):
tbl = Table({"a": "integer"})
update_data = pd.DataFrame({"a": np.array([5, 6, 7, 8], dtype="int64")})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [5, 6, 7, 8]}
def test_update_df_bool(self):
tbl = Table({"a": [True, False, True, False]})
update_data = pd.DataFrame({"a": [True, False, True, False]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [True, False, True, False, True, False, True, False]
}
def test_update_df_str(self):
tbl = Table({"a": ["a", "b", "c", "d"]})
update_data = pd.DataFrame({"a": ["a", "b", "c", "d"]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": ["a", "b", "c", "d", "a", "b", "c", "d"]
}
def test_update_df_date(self, util):
# set_global_serializer(serializer)
tbl = Table({"a": [date(2019, 7, 11)]})
assert tbl.schema() == {"a": "date"}
update_data = pd.DataFrame({"a": [date(2019, 7, 12)]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [
util.to_timestamp(d)
for d in [datetime(2019, 7, 11), datetime(2019, 7, 12)]
]
}
@mark.skip(reason="this test relies on a lossy conversion from datetime -> date.")
def test_update_df_date_timestamp(self, util):
tbl = Table({"a": [date(2019, 7, 11)]})
assert tbl.schema() == {"a": "date"}
update_data = pd.DataFrame({"a": [datetime(2019, 7, 12, 0, 0, 0)]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [
util.to_timestamp(datetime(2019, 7, 11)),
util.to_timestamp(datetime(2019, 7, 12)),
]
}
def test_update_df_datetime(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
update_data = pd.DataFrame({"a": [datetime(2019, 7, 12, 11, 0)]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [
util.to_timestamp(datetime(2019, 7, 11, 11, 0)),
util.to_timestamp(datetime(2019, 7, 12, 11, 0)),
]
}
def test_update_df_datetime_timestamp_seconds(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
update_data = pd.DataFrame({"a": [datetime(2019, 7, 12, 11, 0)]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [
util.to_timestamp(datetime(2019, 7, 11, 11, 0)),
util.to_timestamp(datetime(2019, 7, 12, 11, 0)),
]
}
def test_update_df_datetime_timestamp_ms(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]})
update_data = pd.DataFrame({"a": [datetime(2019, 7, 12, 11, 0)]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [
util.to_timestamp(datetime(2019, 7, 11, 11, 0)),
util.to_timestamp(datetime(2019, 7, 12, 11, 0)),
]
}
def test_update_df_partial(self):
tbl = Table({"a": [1, 2, 3, 4], "b": ["a", "b", "c", "d"]}, index="b")
update_data = pd.DataFrame({"a": [5, 6, 7, 8], "b": ["a", "b", "c", "d"]})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [5, 6, 7, 8], "b": ["a", "b", "c", "d"]}
def test_df_update_index(self):
tbl = Table(pd.DataFrame({"a": [1, 2, 3, 4]}))
update_data = pd.DataFrame({"a": [5, 6, 7, 8], "__INDEX__": [0, 1, 2, 3]})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [5, 6, 7, 8], "index": [0, 1, 2, 3]}
def test_update_df_partial_implicit(self):
tbl = Table({"a": [1, 2, 3, 4]})
update_data = pd.DataFrame({"a": [5, 6, 7, 8], "__INDEX__": [0, 1, 2, 3]})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [5, 6, 7, 8]}
def test_update_df_datetime_partial(self, util):
tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b")
update_data = pd.DataFrame({"a": [datetime(2019, 7, 12, 11, 0)], "b": [1]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [util.to_timestamp(datetime(2019, 7, 12, 11, 0))],
"b": [1],
}
def test_update_df_one_col(self):
tbl = Table({"a": [1, 2, 3, 4], "b": ["a", "b", "c", "d"]})
update_data = pd.DataFrame({"a": [5, 6, 7]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [1, 2, 3, 4, 5, 6, 7],
"b": ["a", "b", "c", "d", None, None, None],
}
def test_update_df_nonseq_partial(self):
tbl = Table({"a": [1, 2, 3, 4], "b": ["a", "b", "c", "d"]}, index="b")
update_data = pd.DataFrame({"a": [5, 6, 7], "b": ["a", "c", "d"]})
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [5, 2, 6, 7], "b": ["a", "b", "c", "d"]}
@pytest.mark.skip
def test_update_df_with_none_partial(self):
tbl = Table({"a": [1, float("nan"), 3], "b": ["a", None, "d"]}, index="b")
update_data = pd.DataFrame({"a": [4, 5], "b": ["a", "d"]})
tbl.update(update_data)
assert tbl.view().to_columns() == {
"a": [None, 4, 5],
"b": [None, "a", "d"],
} # pkeys are ordered
def test_update_df_unset_partial(self):
tbl = Table({"a": [1, 2, 3], "b": ["a", "b", "c"]}, index="b")
update_data = pd.DataFrame(
{"a": pd.Series([None, None], dtype=pd.Int32Dtype()), "b": ["a", "c"]}
)
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [None, 2, None], "b": ["a", "b", "c"]}
def test_update_df_nan_partial(self):
tbl = Table({"a": [1, 2, 3], "b": ["a", "b", "c"]}, index="b")
update_data = pd.DataFrame(
{"a": pd.Series([None, None], dtype=pd.Int32Dtype()), "b": ["a", "c"]}
)
tbl.update(update_data)
assert tbl.view().to_columns() == {"a": [None, 2, None], "b": ["a", "b", "c"]}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff