Adding upstream version 0.12.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
d887bee5ca
commit
148efc9122
69 changed files with 12923 additions and 0 deletions
34
tests/conftest.py
Normal file
34
tests/conftest.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence, Type
|
||||
|
||||
import pytest
|
||||
from textual_fastdatatable.backend import ArrowBackend, DataTableBackend, PolarsBackend
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pydict() -> dict[str, Sequence[str | int]]:
|
||||
return {
|
||||
"first column": [1, 2, 3, 4, 5],
|
||||
"two": ["a", "b", "c", "d", "asdfasdf"],
|
||||
"three": ["foo", "bar", "baz", "qux", "foofoo"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def records(pydict: dict[str, Sequence[str | int]]) -> list[tuple[str | int, ...]]:
|
||||
header = tuple(pydict.keys())
|
||||
cols = list(pydict.values())
|
||||
num_rows = len(cols[0])
|
||||
data = [tuple([col[i] for col in cols]) for i in range(num_rows)]
|
||||
return [header, *data]
|
||||
|
||||
|
||||
@pytest.fixture(params=[ArrowBackend, PolarsBackend])
|
||||
def backend(
|
||||
request: Type[pytest.FixtureRequest], pydict: dict[str, Sequence[str | int]]
|
||||
) -> DataTableBackend:
|
||||
backend_cls = request.param
|
||||
assert issubclass(backend_cls, (ArrowBackend, PolarsBackend))
|
||||
backend: ArrowBackend | PolarsBackend = backend_cls.from_pydict(pydict)
|
||||
return backend
|
BIN
tests/data/lap_times_100.parquet
Normal file
BIN
tests/data/lap_times_100.parquet
Normal file
Binary file not shown.
BIN
tests/data/lap_times_1000.parquet
Normal file
BIN
tests/data/lap_times_1000.parquet
Normal file
Binary file not shown.
BIN
tests/data/lap_times_10000.parquet
Normal file
BIN
tests/data/lap_times_10000.parquet
Normal file
Binary file not shown.
BIN
tests/data/lap_times_100000.parquet
Normal file
BIN
tests/data/lap_times_100000.parquet
Normal file
Binary file not shown.
BIN
tests/data/lap_times_538121.parquet
Normal file
BIN
tests/data/lap_times_538121.parquet
Normal file
Binary file not shown.
BIN
tests/data/wide_10000.parquet
Normal file
BIN
tests/data/wide_10000.parquet
Normal file
Binary file not shown.
BIN
tests/data/wide_100000.parquet
Normal file
BIN
tests/data/wide_100000.parquet
Normal file
Binary file not shown.
21
tests/snapshot_tests/LICENSE
Normal file
21
tests/snapshot_tests/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Will McGugan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
2924
tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Normal file
2924
tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Normal file
File diff suppressed because one or more lines are too long
1814
tests/snapshot_tests/_snapshots_backup/test_snapshots.ambr
Normal file
1814
tests/snapshot_tests/_snapshots_backup/test_snapshots.ambr
Normal file
File diff suppressed because one or more lines are too long
185
tests/snapshot_tests/snapshot_apps/auto-table.py
Normal file
185
tests/snapshot_tests/snapshot_apps/auto-table.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
from textual.app import App
|
||||
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Label
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
|
||||
class LabeledBox(Container):
|
||||
DEFAULT_CSS = """
|
||||
LabeledBox {
|
||||
layers: base_ top_;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
LabeledBox > Container {
|
||||
layer: base_;
|
||||
border: round $primary;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
LabeledBox > Label {
|
||||
layer: top_;
|
||||
offset-x: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, title, *args, **kwargs):
|
||||
self.__label = Label(title)
|
||||
|
||||
super().__init__(self.__label, Container(*args, **kwargs))
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self.__label
|
||||
|
||||
|
||||
class StatusTable(DataTable):
|
||||
def __init__(self) -> None:
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{
|
||||
"Foo": ["ABCDEFGH"] * 50,
|
||||
"Bar": ["0123456789"] * 50,
|
||||
"Baz": ["IJKLMNOPQRSTUVWXYZ"] * 50,
|
||||
}
|
||||
)
|
||||
super().__init__(backend=backend)
|
||||
|
||||
self.cursor_type = "row"
|
||||
self.show_cursor = False
|
||||
|
||||
|
||||
class Status(LabeledBox):
|
||||
DEFAULT_CSS = """
|
||||
Status {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
Status Container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
Status StatusTable {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
margin-top: 1;
|
||||
scrollbar-gutter: stable;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.__name = name
|
||||
self.__table = StatusTable()
|
||||
|
||||
super().__init__(f" {self.__name} ", self.__table)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.__name
|
||||
|
||||
@property
|
||||
def table(self) -> StatusTable:
|
||||
return self.__table
|
||||
|
||||
|
||||
class Rendering(LabeledBox):
|
||||
DEFAULT_CSS = """
|
||||
#issue-info {
|
||||
height: auto;
|
||||
border-bottom: dashed #632CA6;
|
||||
}
|
||||
|
||||
#statuses-box {
|
||||
height: 1fr;
|
||||
width: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__info = Label("test")
|
||||
|
||||
super().__init__(
|
||||
"",
|
||||
ScrollableContainer(
|
||||
Horizontal(self.__info, id="issue-info"),
|
||||
Horizontal(*[Status(str(i)) for i in range(4)], id="statuses-box"),
|
||||
id="issues-box",
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def info(self) -> Label:
|
||||
return self.__info
|
||||
|
||||
|
||||
class Sidebar(LabeledBox):
|
||||
DEFAULT_CSS = """
|
||||
#sidebar-status {
|
||||
height: auto;
|
||||
border-bottom: dashed #632CA6;
|
||||
}
|
||||
|
||||
#sidebar-options {
|
||||
height: 1fr;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__status = Label("ok")
|
||||
self.__options = Vertical()
|
||||
|
||||
super().__init__(
|
||||
"",
|
||||
Container(self.__status, id="sidebar-status"),
|
||||
Container(self.__options, id="sidebar-options"),
|
||||
)
|
||||
|
||||
@property
|
||||
def status(self) -> Label:
|
||||
return self.__status
|
||||
|
||||
@property
|
||||
def options(self) -> Vertical:
|
||||
return self.__options
|
||||
|
||||
|
||||
class MyScreen(Screen):
|
||||
DEFAULT_CSS = """
|
||||
#main-content {
|
||||
layout: grid;
|
||||
grid-size: 2;
|
||||
grid-columns: 1fr 5fr;
|
||||
grid-rows: 1fr;
|
||||
}
|
||||
|
||||
#main-content-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#main-content-rendering {
|
||||
height: 100%;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
yield Header()
|
||||
yield Container(
|
||||
Container(Sidebar(), id="main-content-sidebar"),
|
||||
Container(Rendering(), id="main-content-rendering"),
|
||||
id="main-content",
|
||||
)
|
||||
|
||||
|
||||
class MyApp(App):
|
||||
async def on_mount(self):
|
||||
self.install_screen(MyScreen(), "myscreen")
|
||||
await self.push_screen("myscreen")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MyApp()
|
||||
app.run()
|
26
tests/snapshot_tests/snapshot_apps/data_table.py
Normal file
26
tests/snapshot_tests/snapshot_apps/data_table.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(4, "Joseph Schooling", "Singapore", 50.39),
|
||||
(2, "Michael Phelps", "United States", 51.14),
|
||||
(5, "Chad le Clos", "South Africa", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_records(ROWS, has_header=True)
|
||||
yield DataTable(backend=backend)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
36
tests/snapshot_tests/snapshot_apps/data_table_add_column.py
Normal file
36
tests/snapshot_tests/snapshot_apps/data_table_add_column.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
MOVIES = [
|
||||
"Severance",
|
||||
"Foundation",
|
||||
"Dark",
|
||||
"The Boys",
|
||||
"The Last of Us",
|
||||
"Lost in Space",
|
||||
"Altered Carbon",
|
||||
]
|
||||
|
||||
|
||||
class AddColumn(App):
|
||||
BINDINGS = [
|
||||
Binding(key="c", action="add_column", description="Add Column"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_pydict({"Movies": MOVIES})
|
||||
table = DataTable(backend=backend)
|
||||
|
||||
column_idx = table.add_column("No Default")
|
||||
table.add_column("With Default", default="ABC")
|
||||
table.add_column("Long Default", default="01234567890123456789")
|
||||
|
||||
# Ensure we can update a cell
|
||||
table.update_cell(2, column_idx, "Hello!")
|
||||
yield table
|
||||
|
||||
|
||||
app = AddColumn()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -0,0 +1,24 @@
|
|||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from textual.app import App
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class AutoHeightRowsApp(App[None]):
|
||||
def compose(self):
|
||||
table = DataTable()
|
||||
self.column = table.add_column("N")
|
||||
table.add_column("Column", width=10)
|
||||
table.add_row(3, "hey there", height=None)
|
||||
table.add_row(1, Text("hey there"), height=None)
|
||||
table.add_row(5, Text("long string", overflow="fold"), height=None)
|
||||
table.add_row(2, Panel.fit("Hello\nworld"), height=None)
|
||||
table.add_row(4, "1\n2\n3\n4\n5\n6\n7", height=None)
|
||||
yield table
|
||||
|
||||
def key_s(self):
|
||||
self.query_one(DataTable).sort(self.column)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
AutoHeightRowsApp().run()
|
|
@ -0,0 +1,35 @@
|
|||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
labels = next(rows)
|
||||
data = [row for row in rows]
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{label: [row[i] for row in data] for i, label in enumerate(labels)}
|
||||
)
|
||||
table = DataTable(
|
||||
backend=backend, cursor_type="column", fixed_columns=1, fixed_rows=1
|
||||
)
|
||||
table.focus()
|
||||
yield table
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
35
tests/snapshot_tests/snapshot_apps/data_table_max_width.py
Normal file
35
tests/snapshot_tests/snapshot_apps/data_table_max_width.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
labels = next(rows)
|
||||
data = [row for row in rows]
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{label: [row[i] for row in data] for i, label in enumerate(labels)}
|
||||
)
|
||||
table = DataTable(
|
||||
backend=backend, cursor_type="range", max_column_content_width=8
|
||||
)
|
||||
table.focus()
|
||||
yield table
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
|
@ -0,0 +1,26 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(4, "[Joseph Schooling]", "Singapore", 50.39),
|
||||
(2, "[red]Michael Phelps[/]", "United States", 51.14),
|
||||
(5, "[bold]Chad le Clos[/]", "South Africa", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_records(ROWS, has_header=True)
|
||||
yield DataTable(backend=backend, render_markup=False)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -0,0 +1,22 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
("eight", None, "France", 51.58),
|
||||
("seven", "Tom Shields", "United States", None),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
(None, "Darren Burns", "Scotland", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_records(ROWS, has_header=True)
|
||||
yield DataTable(backend=backend, null_rep="[dim]∅ null[/]")
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -0,0 +1,33 @@
|
|||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
labels = next(rows)
|
||||
data = [row for row in rows]
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{label: [row[i] for row in data] for i, label in enumerate(labels)}
|
||||
)
|
||||
table = DataTable(backend=backend, cursor_type="range")
|
||||
table.focus()
|
||||
yield table
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
45
tests/snapshot_tests/snapshot_apps/data_table_remove_row.py
Normal file
45
tests/snapshot_tests/snapshot_apps/data_table_remove_row.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(5, "Chad le Clos", "South Africa", 51.14),
|
||||
(4, "Joseph Schooling", "Singapore", 50.39),
|
||||
(2, "Michael Phelps", "United States", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
"""Snapshot app for testing removal of rows.
|
||||
Removes several rows, so we can check that the display of the
|
||||
DataTable updates as expected."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("r", "remove_row", "Remove Row"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_records(ROWS, has_header=True)
|
||||
yield DataTable(backend=backend)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
table.focus()
|
||||
|
||||
def action_remove_row(self):
|
||||
table = self.query_one(DataTable)
|
||||
table.remove_row(2)
|
||||
table.remove_row(4)
|
||||
table.remove_row(6)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
36
tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py
Normal file
36
tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
labels = next(rows)
|
||||
data = [row for row in rows]
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{label: [row[i] for row in data] for i, label in enumerate(labels)}
|
||||
)
|
||||
table = DataTable(backend=backend)
|
||||
table.focus()
|
||||
table.cursor_type = "row"
|
||||
table.fixed_columns = 1
|
||||
table.fixed_rows = 1
|
||||
yield table
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
37
tests/snapshot_tests/snapshot_apps/data_table_row_labels.py
Normal file
37
tests/snapshot_tests/snapshot_apps/data_table_row_labels.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(5, "Chad le Clos", "South Africa", 51.14),
|
||||
(4, "Joseph Schooling", "Singapore", 50.39),
|
||||
(2, "Michael Phelps", "United States", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
table.fixed_rows = 1
|
||||
table.fixed_columns = 1
|
||||
table.focus()
|
||||
rows = iter(ROWS)
|
||||
column_labels = next(rows)
|
||||
for column in column_labels:
|
||||
table.add_column(column, key=column)
|
||||
for index, row in enumerate(rows):
|
||||
table.add_row(*row, label=str(index))
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
40
tests/snapshot_tests/snapshot_apps/data_table_sort.py
Normal file
40
tests/snapshot_tests/snapshot_apps/data_table_sort.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
# Shuffled around a bit to exercise sorting.
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(5, "Chad le Clos", "South Africa", 51.14),
|
||||
(4, "Joseph Schooling", "Singapore", 50.39),
|
||||
(2, "Michael Phelps", "United States", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
BINDINGS = [
|
||||
Binding("s", "sort", "Sort"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
backend = ArrowBackend.from_records(ROWS, has_header=True)
|
||||
yield DataTable(backend=backend)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
table.focus()
|
||||
|
||||
def action_sort(self):
|
||||
table = self.query_one(DataTable)
|
||||
table.sort([("time", "ascending"), ("lane", "ascending")])
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
65
tests/snapshot_tests/snapshot_apps/data_table_style_order.py
Normal file
65
tests/snapshot_tests/snapshot_apps/data_table_style_order.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Label
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
from typing_extensions import Literal
|
||||
|
||||
data = [
|
||||
"Severance",
|
||||
"Foundation",
|
||||
"Dark",
|
||||
]
|
||||
|
||||
|
||||
def make_datatable(
|
||||
foreground_priority: Literal["css", "renderable"],
|
||||
background_priority: Literal["css", "renderable"],
|
||||
) -> DataTable:
|
||||
backend = ArrowBackend.from_pydict(
|
||||
{"Movies": [f"[red on blue]{row}" for row in data]}
|
||||
)
|
||||
table = DataTable(
|
||||
backend=backend,
|
||||
cursor_foreground_priority=foreground_priority,
|
||||
cursor_background_priority=background_priority,
|
||||
)
|
||||
table.zebra_stripes = True
|
||||
return table
|
||||
|
||||
|
||||
class DataTableCursorStyles(App):
|
||||
"""Regression test snapshot app which ensures that styles
|
||||
are layered on top of each other correctly in the DataTable.
|
||||
In this example, the colour of the text in the cells under
|
||||
the cursor should not be red, because the CSS should be applied
|
||||
on top."""
|
||||
|
||||
CSS = """
|
||||
DataTable {margin-bottom: 1;}
|
||||
DataTable > .datatable--cursor {
|
||||
color: $secondary;
|
||||
background: $success;
|
||||
text-style: bold italic;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
priorities: list[
|
||||
tuple[Literal["css", "renderable"], Literal["css", "renderable"]]
|
||||
] = [
|
||||
("css", "css"),
|
||||
("css", "renderable"),
|
||||
("renderable", "renderable"),
|
||||
("renderable", "css"),
|
||||
]
|
||||
for foreground, background in priorities:
|
||||
yield Label(f"Foreground is {foreground!r}, background is {background!r}:")
|
||||
table = make_datatable(foreground, background)
|
||||
yield table
|
||||
|
||||
|
||||
app = DataTableCursorStyles()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -0,0 +1,58 @@
|
|||
from pathlib import Path
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import ArrowBackend, DataTable
|
||||
|
||||
CSS_PATH = (Path(__file__) / "../datatable_hot_reloading.tcss").resolve()
|
||||
|
||||
# Write some CSS to the file before the app loads.
|
||||
# Then, the test will clear all the CSS to see if the
|
||||
# hot reloading applies the changes correctly.
|
||||
CSS_PATH.write_text(
|
||||
"""\
|
||||
DataTable > .datatable--cursor {
|
||||
background: purple;
|
||||
}
|
||||
|
||||
DataTable > .datatable--fixed {
|
||||
background: red;
|
||||
}
|
||||
|
||||
DataTable > .datatable--fixed-cursor {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
DataTable > .datatable--header {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
DataTable > .datatable--odd-row {
|
||||
background: pink;
|
||||
}
|
||||
|
||||
DataTable > .datatable--even-row {
|
||||
background: brown;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class DataTableHotReloadingApp(App[None]):
|
||||
CSS_PATH = CSS_PATH
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
data = {
|
||||
# orig test set A width=10, we fake it with spaces
|
||||
"A ": ["one", "three", "five"],
|
||||
"B": ["two", "four", "six"],
|
||||
}
|
||||
backend = ArrowBackend.from_pydict(data)
|
||||
yield DataTable(backend, zebra_stripes=True, cursor_type="row", fixed_columns=1)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one(DataTable)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = DataTableHotReloadingApp()
|
||||
app.run()
|
|
@ -0,0 +1 @@
|
|||
/* This file is purposefully empty. */
|
12
tests/snapshot_tests/snapshot_apps/empty.py
Normal file
12
tests/snapshot_tests/snapshot_apps/empty.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable()
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
17
tests/snapshot_tests/snapshot_apps/empty_add_col.py
Normal file
17
tests/snapshot_tests/snapshot_apps/empty_add_col.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
table.add_column("Foo")
|
||||
table.add_rows([("1",), ("2",)])
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
16
tests/snapshot_tests/snapshot_apps/from_parquet.py
Normal file
16
tests/snapshot_tests/snapshot_apps/from_parquet.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from pathlib import Path
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(
|
||||
data=Path(__file__).parent.parent.parent / "data" / "lap_times_100.parquet"
|
||||
)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
|
@ -0,0 +1,20 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
DATA = {
|
||||
"Foo": list(range(50)),
|
||||
"Bar": ["0123456789"] * 50,
|
||||
"Baz": ["IJKLMNOPQRSTUVWXYZ"] * 50,
|
||||
}
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(
|
||||
data=DATA, column_labels=["[red]Not Foo[/red]", "Zig", "[reverse]Zag[/]"]
|
||||
)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
25
tests/snapshot_tests/snapshot_apps/from_records.py
Normal file
25
tests/snapshot_tests/snapshot_apps/from_records.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
ROWS = [
|
||||
("lane", "swimmer", "country", "time"),
|
||||
(4, "Joseph Schooling", "Singapore", 50.39),
|
||||
(2, "Michael Phelps", "United States", 51.14),
|
||||
(5, "Chad le Clos", "South Africa", 51.14),
|
||||
(6, "László Cseh", "Hungary", 51.14),
|
||||
(3, "Li Zhuhao", "China", 51.26),
|
||||
(8, "Mehdy Metella", "France", 51.58),
|
||||
(7, "Tom Shields", "United States", 51.73),
|
||||
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
|
||||
(10, "Darren Burns", "Scotland", 51.84),
|
||||
]
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(data=ROWS)
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
12
tests/snapshot_tests/snapshot_apps/no_rows.py
Normal file
12
tests/snapshot_tests/snapshot_apps/no_rows.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(column_labels=["foo [red]foo[/red]", "bar"])
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
12
tests/snapshot_tests/snapshot_apps/no_rows_empty_sequence.py
Normal file
12
tests/snapshot_tests/snapshot_apps/no_rows_empty_sequence.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from textual.app import App, ComposeResult
|
||||
from textual_fastdatatable import DataTable
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(column_labels=["foo [red]foo[/red]", "bar"])
|
||||
|
||||
|
||||
app = TableApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
119
tests/snapshot_tests/test_snapshots.py
Normal file
119
tests/snapshot_tests/test_snapshots.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
# These paths should be relative to THIS directory.
|
||||
SNAPSHOT_APPS_DIR = Path("./snapshot_apps")
|
||||
|
||||
|
||||
def test_auto_table(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "auto-table.py", terminal_size=(120, 40))
|
||||
|
||||
|
||||
def test_datatable_render(snap_compare: Callable) -> None:
|
||||
press = ["down", "down", "right", "up", "left"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_row_cursor_render(snap_compare: Callable) -> None:
|
||||
press = ["up", "left", "right", "down", "down"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_cursor.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_no_render_markup(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_no_render_markup.py")
|
||||
|
||||
|
||||
def test_datatable_null_mixed_cols(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_null_mixed_cols.py")
|
||||
|
||||
|
||||
def test_datatable_range_cursor_render(snap_compare: Callable) -> None:
|
||||
press = ["right", "down", "shift+right", "shift+down", "shift+down"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_range_cursor.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_column_cursor_render(snap_compare: Callable) -> None:
|
||||
press = ["left", "up", "down", "right", "right"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_column_cursor.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_max_width_render(snap_compare: Callable) -> None:
|
||||
press = ["right", "down", "shift+right", "shift+down", "shift+down"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_max_width.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_sort_multikey(snap_compare: Callable) -> None:
|
||||
press = ["down", "right", "s"] # Also checks that sort doesn't move cursor.
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_sort.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_remove_row(snap_compare: Callable) -> None:
|
||||
press = ["r"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_remove_row.py", press=press)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Don't support row labels.")
|
||||
def test_datatable_labels_and_fixed_data(snap_compare: Callable) -> None:
|
||||
# Ensure that we render correctly when there are fixed rows/cols and labels.
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_labels.py")
|
||||
|
||||
|
||||
# skip, don't xfail; see: https://github.com/Textualize/pytest-textual-snapshot/issues/6
|
||||
@pytest.mark.skip(
|
||||
reason=(
|
||||
"The data in this test includes markup; the backend doesn't"
|
||||
"know these have zero width, so we draw the column wider than we used to"
|
||||
)
|
||||
)
|
||||
def test_datatable_style_ordering(snap_compare: Callable) -> None:
|
||||
# Regression test for https -> None://github.com/Textualize/textual/issues/2061
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_style_order.py")
|
||||
|
||||
|
||||
def test_datatable_add_column(snap_compare: Callable) -> None:
|
||||
# Checking adding columns after adding rows
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_column.py")
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="No multi-height rows. No Rich objects.")
|
||||
def test_datatable_add_row_auto_height(snap_compare: Callable) -> None:
|
||||
# Check that rows added with auto height computation look right.
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py")
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="No multi-height rows. No Rich objects.")
|
||||
def test_datatable_add_row_auto_height_sorted(snap_compare: Callable) -> None:
|
||||
# Check that rows added with auto height computation look right.
|
||||
assert snap_compare(
|
||||
SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py", press=["s"]
|
||||
)
|
||||
|
||||
|
||||
def test_datatable_empty(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "empty.py")
|
||||
|
||||
|
||||
def test_datatable_empty_add_col(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "empty_add_col.py")
|
||||
|
||||
|
||||
def test_datatable_no_rows(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "no_rows.py")
|
||||
|
||||
|
||||
def test_datatable_no_rows_empty_sequence(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "no_rows_empty_sequence.py")
|
||||
|
||||
|
||||
def test_datatable_from_parquet(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "from_parquet.py")
|
||||
|
||||
|
||||
def test_datatable_from_records(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "from_records.py")
|
||||
|
||||
|
||||
def test_datatable_from_pydict(snap_compare: Callable) -> None:
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "from_pydict_with_col_labels.py")
|
94
tests/unit_tests/test_arrow_backend.py
Normal file
94
tests/unit_tests/test_arrow_backend.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
import pyarrow as pa
|
||||
from textual_fastdatatable import ArrowBackend
|
||||
|
||||
|
||||
def test_from_records(records: list[tuple[str | int, ...]]) -> None:
|
||||
backend = ArrowBackend.from_records(records, has_header=True)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 5
|
||||
assert tuple(backend.columns) == records[0]
|
||||
|
||||
|
||||
def test_from_records_no_header(records: list[tuple[str | int, ...]]) -> None:
|
||||
backend = ArrowBackend.from_records(records[1:], has_header=False)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 5
|
||||
assert tuple(backend.columns) == ("f0", "f1", "f2")
|
||||
|
||||
|
||||
def test_from_pydict(pydict: dict[str, Sequence[str | int]]) -> None:
|
||||
backend = ArrowBackend.from_pydict(pydict)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 5
|
||||
assert backend.source_row_count == 5
|
||||
assert tuple(backend.columns) == tuple(pydict.keys())
|
||||
|
||||
|
||||
def test_from_pydict_with_limit(pydict: dict[str, Sequence[str | int]]) -> None:
|
||||
backend = ArrowBackend.from_pydict(pydict, max_rows=2)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 2
|
||||
assert backend.source_row_count == 5
|
||||
assert tuple(backend.columns) == tuple(pydict.keys())
|
||||
|
||||
|
||||
def test_from_parquet(pydict: dict[str, Sequence[str | int]], tmp_path: Path) -> None:
|
||||
tbl = pa.Table.from_pydict(pydict)
|
||||
p = tmp_path / "test.parquet"
|
||||
pa.parquet.write_table(tbl, str(p))
|
||||
|
||||
backend = ArrowBackend.from_parquet(p)
|
||||
assert backend.data.equals(tbl)
|
||||
|
||||
|
||||
def test_empty_query() -> None:
|
||||
data: dict[str, list] = {"a": []}
|
||||
backend = ArrowBackend.from_pydict(data)
|
||||
assert backend.column_content_widths == [0]
|
||||
|
||||
|
||||
def test_dupe_column_labels() -> None:
|
||||
arr = pa.array([0, 1, 2, 3])
|
||||
tab = pa.table([arr] * 3, names=["a", "a", "a"])
|
||||
backend = ArrowBackend(data=tab)
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 4
|
||||
assert backend.get_row_at(2) == [2, 2, 2]
|
||||
|
||||
|
||||
def test_timestamp_with_tz() -> None:
|
||||
"""
|
||||
Ensure datetimes with offsets but no names do not crash the data table
|
||||
when casting to string.
|
||||
"""
|
||||
dt = datetime(2024, 1, 1, hour=15, tzinfo=timezone(offset=timedelta(hours=-5)))
|
||||
arr = pa.array([dt, dt, dt])
|
||||
tab = pa.table([arr], names=["created_at"])
|
||||
backend = ArrowBackend(data=tab)
|
||||
assert backend.column_content_widths == [29]
|
||||
|
||||
|
||||
def test_mixed_types() -> None:
|
||||
data = [(1000,), ("hi",)]
|
||||
backend = ArrowBackend.from_records(records=data)
|
||||
assert backend
|
||||
assert backend.row_count == 2
|
||||
assert backend.get_row_at(0) == ["1000"]
|
||||
assert backend.get_row_at(1) == ["hi"]
|
||||
|
||||
|
||||
def test_negative_timestamps() -> None:
|
||||
dt = datetime(1, 1, 1, tzinfo=timezone.utc)
|
||||
arr = pa.array([dt, dt, dt], type=pa.timestamp("s", tz="America/New_York"))
|
||||
tab = pa.table([arr], names=["created_at"])
|
||||
backend = ArrowBackend(data=tab)
|
||||
assert backend.column_content_widths == [26]
|
||||
assert backend.get_column_at(0) == [datetime.min, datetime.min, datetime.min]
|
||||
assert backend.get_row_at(0) == [datetime.min]
|
||||
assert backend.get_cell_at(0, 0) is datetime.min
|
109
tests/unit_tests/test_backends.py
Normal file
109
tests/unit_tests/test_backends.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from textual_fastdatatable.backend import DataTableBackend
|
||||
|
||||
|
||||
def test_column_content_widths(backend: DataTableBackend) -> None:
|
||||
assert backend.column_content_widths == [1, 8, 6]
|
||||
|
||||
|
||||
def test_get_row_at(backend: DataTableBackend) -> None:
|
||||
assert backend.get_row_at(0) == [1, "a", "foo"]
|
||||
assert backend.get_row_at(4) == [5, "asdfasdf", "foofoo"]
|
||||
with pytest.raises(IndexError):
|
||||
backend.get_row_at(10)
|
||||
with pytest.raises(IndexError):
|
||||
backend.get_row_at(-1)
|
||||
|
||||
|
||||
def test_get_column_at(backend: DataTableBackend) -> None:
|
||||
assert backend.get_column_at(0) == [1, 2, 3, 4, 5]
|
||||
assert backend.get_column_at(2) == ["foo", "bar", "baz", "qux", "foofoo"]
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
backend.get_column_at(10)
|
||||
|
||||
|
||||
def test_get_cell_at(backend: DataTableBackend) -> None:
|
||||
assert backend.get_cell_at(0, 0) == 1
|
||||
assert backend.get_cell_at(4, 1) == "asdfasdf"
|
||||
with pytest.raises(IndexError):
|
||||
backend.get_cell_at(10, 0)
|
||||
with pytest.raises(IndexError):
|
||||
backend.get_cell_at(0, 10)
|
||||
|
||||
|
||||
def test_append_column(backend: DataTableBackend) -> None:
|
||||
original_table = backend.data
|
||||
backend.append_column("new")
|
||||
assert backend.column_count == 4
|
||||
assert backend.row_count == 5
|
||||
assert backend.get_column_at(3) == [None] * backend.row_count
|
||||
|
||||
backend.append_column("def", default="zzz")
|
||||
assert backend.column_count == 5
|
||||
assert backend.row_count == 5
|
||||
assert backend.get_column_at(4) == ["zzz"] * backend.row_count
|
||||
|
||||
assert backend.data.select(["first column", "two", "three"]).equals(original_table)
|
||||
|
||||
|
||||
def test_append_rows(backend: DataTableBackend) -> None:
|
||||
original_table = backend.data
|
||||
backend.append_rows([(6, "w", "x"), (7, "y", "z")])
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 7
|
||||
assert backend.column_content_widths == [1, 8, 6]
|
||||
|
||||
backend.append_rows([(999, "w" * 12, "x" * 15)])
|
||||
assert backend.column_count == 3
|
||||
assert backend.row_count == 8
|
||||
assert backend.column_content_widths == [3, 12, 15]
|
||||
|
||||
assert backend.data.slice(0, 5).equals(original_table)
|
||||
|
||||
|
||||
def test_drop_row(backend: DataTableBackend) -> None:
|
||||
backend.drop_row(0)
|
||||
assert backend.row_count == 4
|
||||
assert backend.column_count == 3
|
||||
assert backend.column_content_widths == [1, 8, 6]
|
||||
|
||||
backend.drop_row(3)
|
||||
assert backend.row_count == 3
|
||||
assert backend.column_count == 3
|
||||
assert backend.column_content_widths == [1, 1, 3]
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
backend.drop_row(3)
|
||||
|
||||
|
||||
def test_update_cell(backend: DataTableBackend) -> None:
|
||||
backend.update_cell(0, 0, 0)
|
||||
assert backend.get_column_at(0) == [0, 2, 3, 4, 5]
|
||||
assert backend.row_count == 5
|
||||
assert backend.column_count == 3
|
||||
assert backend.column_content_widths == [1, 8, 6]
|
||||
|
||||
backend.update_cell(3, 1, "z" * 50)
|
||||
assert backend.get_row_at(3) == [4, "z" * 50, "qux"]
|
||||
assert backend.row_count == 5
|
||||
assert backend.column_count == 3
|
||||
assert backend.column_content_widths == [1, 50, 6]
|
||||
|
||||
|
||||
def test_sort(backend: DataTableBackend) -> None:
|
||||
original_table = backend.data
|
||||
original_col_one = list(backend.get_column_at(0)).copy()
|
||||
original_col_two = list(backend.get_column_at(1)).copy()
|
||||
backend.sort(by="two")
|
||||
assert backend.get_column_at(0) != original_col_one
|
||||
assert backend.get_column_at(1) == sorted(original_col_two)
|
||||
|
||||
backend.sort(by=[("two", "descending")])
|
||||
assert backend.get_column_at(0) != original_col_one
|
||||
assert backend.get_column_at(1) == sorted(original_col_two, reverse=True)
|
||||
|
||||
backend.sort(by=[("first column", "ascending")])
|
||||
assert backend.data.equals(original_table)
|
54
tests/unit_tests/test_create_backend.py
Normal file
54
tests/unit_tests/test_create_backend.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from datetime import date, datetime
|
||||
|
||||
import pyarrow as pa
|
||||
from textual_fastdatatable.backend import create_backend
|
||||
|
||||
MAX_32BIT_INT = 2**31 - 1
|
||||
MAX_64BIT_INT = 2**63 - 1
|
||||
|
||||
|
||||
def test_empty_sequence() -> None:
|
||||
backend = create_backend(data=[])
|
||||
assert backend
|
||||
assert backend.row_count == 0
|
||||
assert backend.column_count == 0
|
||||
assert backend.columns == []
|
||||
assert backend.column_content_widths == []
|
||||
|
||||
|
||||
def test_infinity_timestamps() -> None:
|
||||
from_py = create_backend(
|
||||
data={"dt": [date.max, date.min], "ts": [datetime.max, datetime.min]}
|
||||
)
|
||||
assert from_py
|
||||
assert from_py.row_count == 2
|
||||
|
||||
from_arrow = create_backend(
|
||||
data=pa.table(
|
||||
{
|
||||
"dt32": [
|
||||
pa.scalar(MAX_32BIT_INT, type=pa.date32()),
|
||||
pa.scalar(-MAX_32BIT_INT, type=pa.date32()),
|
||||
],
|
||||
"dt64": [
|
||||
pa.scalar(MAX_64BIT_INT, type=pa.date64()),
|
||||
pa.scalar(-MAX_64BIT_INT, type=pa.date64()),
|
||||
],
|
||||
"ts": [
|
||||
pa.scalar(MAX_64BIT_INT, type=pa.timestamp("s")),
|
||||
pa.scalar(-MAX_64BIT_INT, type=pa.timestamp("s")),
|
||||
],
|
||||
"tns": [
|
||||
pa.scalar(MAX_64BIT_INT, type=pa.timestamp("ns")),
|
||||
pa.scalar(-MAX_64BIT_INT, type=pa.timestamp("ns")),
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert from_arrow
|
||||
assert from_arrow.row_count == 2
|
||||
assert from_arrow.get_row_at(0) == [date.max, date.max, datetime.max, datetime.max]
|
||||
assert from_arrow.get_row_at(1) == [date.min, date.min, datetime.min, datetime.min]
|
||||
assert from_arrow.get_column_at(0) == [date.max, date.min]
|
||||
assert from_arrow.get_column_at(2) == [datetime.max, datetime.min]
|
||||
assert from_arrow.get_cell_at(0, 0) == date.max
|
Loading…
Add table
Add a link
Reference in a new issue