1
0
Fork 0

Adding upstream version 0.12.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-24 10:57:24 +01:00
parent d887bee5ca
commit 148efc9122
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
69 changed files with 12923 additions and 0 deletions

34
tests/conftest.py Normal file
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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()

View 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()

View 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()

View file

@ -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()

View 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="column", fixed_columns=1, fixed_rows=1
)
table.focus()
yield table
if __name__ == "__main__":
app = TableApp()
app.run()

View 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()

View 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, "[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()

View file

@ -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()

View file

@ -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()

View 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()

View 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()

View 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()

View 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()

View 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()

View file

@ -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()

View file

@ -0,0 +1 @@
/* This file is purposefully empty. */

View 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()

View 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()

View 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()

View file

@ -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()

View 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()

View 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()

View 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()

View 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")

View 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

View 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)

View 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