"""Logical list ownership under native recycling requests."""
from typing import Any
import pytest
import pythonnative as pn
from pythonnative.testing import render
def test_rows_inherit_provider_and_keep_state_by_key() -> None:
theme = pn.create_context("default")
setters = {}
@pn.component
def Row(item: Any) -> pn.Element:
value, setter = pn.use_state(item["label"])
setters[item["id"]] = setter
return pn.Text(f"{item['label']}/{value}/{pn.use_context(theme)}")
def tree(data: list[dict[str, str]]) -> pn.Element:
return theme.Provider(
pn.FlatList(data=data, key_extractor=lambda item, _: item["id"], render_item=lambda item, _: Row(item)),
value="inherited",
)
result = render(tree([{"id": "a", "label": "A"}, {"id": "b", "label": "B"}]))
assert result.get_by_text("A/A/inherited")
setters["a"]("edited")
result.rerender(tree([{"id": "b", "label": "B2"}, {"id": "a", "label": "A2"}]))
assert result.get_by_text("A2/edited/inherited")
assert result.get_by_text("B2/B/inherited")
def test_same_length_data_edits_advance_native_revision() -> None:
result = render(pn.FlatList(data=["a", "b"], item_height=44))
first = result.backend.list_stores[result.get_by_type("VirtualList").tag].revision
result.rerender(pn.FlatList(data=["z", "b"], item_height=44))
assert result.backend.list_stores[result.get_by_type("VirtualList").tag].revision > first
assert result.get_by_text("z")
def test_native_requests_are_bounded_and_stale_requests_are_ignored() -> None:
result = render(pn.FlatList(data=list(range(10_000)), item_height=44))
view = result.get_by_type("VirtualList")
store = result.backend.list_stores[view.tag]
revision = store.revision
assert len(result.get_all_by_type("Text")) pn.Element:
original, _ = pn.use_state(item)
return pn.Text(f"{item}/{original}")
result = render(pn.FlatList(data=["a"], key_extractor=lambda item, _: item, render_item=lambda item, _: Row(item)))
result.rerender(pn.FlatList(data=["b"], key_extractor=lambda item, _: item, render_item=lambda item, _: Row(item)))
assert result.get_by_text("b/b")
def test_duplicate_keys_fail_before_native_commit() -> None:
with pytest.raises(ValueError, match="unique"):
render(pn.FlatList(data=[1, 2], key_extractor=lambda *_: "duplicate"))
def test_fixed_list_height_survives_an_unbounded_scroll_parent() -> None:
result = render(
pn.ScrollView(
pn.Column(pn.FlatList(data=list(range(40)), item_height=44, style={"height": 400}), pn.Text("After"))
)
)
assert result.get_by_type("VirtualList").frame[3] == 400
result.unmount()
def test_parent_renders_keep_dataset_revision_and_only_changed_items_advance() -> None:
def row(item: str, _: int) -> pn.Element:
return pn.Text(item)
result = render(pn.FlatList(data=["a", "b"], render_item=row))
view = result.get_by_type("VirtualList")
store = result.backend.list_stores[view.tag]
revision, items = store.revision, [store.rows[key].revision for key in store.keys]
result.rerender(pn.FlatList(data=["a", "b"], render_item=row))
assert store.revision == revision
assert [store.rows[key].revision for key in store.keys] == items
result.rerender(pn.FlatList(data=["changed", "b"], render_item=row))
assert [store.rows[key].revision for key in store.keys] == [items[0] + 1, items[1]]
result.unmount()
def test_public_indices_exclude_global_and_section_headers() -> None:
reference: pn.Ref[Any] = pn.Ref()
result = render(
pn.SectionList(
sections=[pn.Section(key="A", title="A", data=["a", "b"]), pn.Section(key="B", title="B", data=["c"])],
list_header=pn.Text("Global header"),
ref=reference,
)
)
reference.current.scroll_to_index(2, animated=False)
assert result.backend.commands[-1][-1]["index"] == 5
with pytest.raises(IndexError):
reference.current.scroll_to_index(3)
result.unmount()
def test_viewport_changes_reuse_dataset_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
import pythonnative.components.lists as lists
original = lists._prepare_snapshot
prepared = []
def prepare(*args: Any, **kwargs: Any) -> Any:
prepared.append(len(args[0]))
return original(*args, **kwargs)
monkeypatch.setattr(lists, "_prepare_snapshot", prepare)
result = render(pn.FlatList(data=range(100_000), item_height=44), viewport=None, settle_first=False)
view = result.get_by_type("VirtualList")
store = result.backend.list_stores[view.tag]
keys, metadata = store.keys, store.rows
for first in range(50, 500, 25):
result.fire(view, "on_window", {"first": first, "last": first + 18, "extent": 800, "y": first * 44})
assert store.keys is keys
assert store.rows is metadata
assert len(result.get_all_by_type("Text")) None:
calls = []
data = [{"id": index, "title": str(index)} for index in range(10_000)]
def key(item: Any, index: int) -> str:
calls.append(index)
return str(item["id"])
def row(item: Any, _: int) -> pn.Element:
return pn.Text(item["title"])
result = render(pn.FlatList(data=data, key_extractor=key, render_item=row), viewport=None)
assert len(calls) == len(data)
result.rerender(pn.FlatList(data=data, key_extractor=key, render_item=row))
assert len(calls) == len(data)
data[0]["title"] = "Edited in place"
result.rerender(pn.FlatList(data=data, data_revision=1, key_extractor=key, render_item=row))
assert result.get_by_text("Edited in place")
assert len(calls) == 2 * len(data)
result.unmount()