store: Add a Python-based ListStore

This is implemented as an alternative to the Gio.ListStore that uses a
Python list object to hold items.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-01-03 13:08:54 -05:00
parent 4be26c5fee
commit 482a199731
2 changed files with 244 additions and 0 deletions

86
emmental/store.py Normal file
View File

@ -0,0 +1,86 @@
# Copyright 2023 (c) Anna Schumaker.
"""A Python-based ListStore implementation."""
import typing
from gi.repository import GObject
from gi.repository import Gio
class ListStore(GObject.GObject, Gio.ListModel):
"""A ListStore using Python lists."""
n_items = GObject.Property(type=int)
def __init__(self):
"""Initialize a ListStore."""
super().__init__()
self.items = []
def __contains__(self, item: GObject.GObject) -> bool:
"""Check if the item is contained in the list."""
return self.index(item) is not None
def do_get_item_type(self) -> GObject.GType:
"""Get the type of items that can be stored."""
return GObject.GObject.__gtype__
def do_get_n_items(self) -> int:
"""Get the length of the list."""
return len(self.items)
def do_get_item(self, index: int) -> GObject.GObject:
"""Get the item at the specific index."""
if index < len(self.items):
return self.items[index]
def append(self, item: GObject.GObject) -> bool:
"""Append an item to the list."""
pos = len(self.items)
self.items.append(item)
self.items_changed(pos, 0, 1)
return True
def clear(self) -> None:
"""Clear the list."""
if size := len(self.items):
self.items.clear()
self.items_changed(0, size, 0)
def extend(self, items: typing.Iterable) -> None:
"""Append multiple items to the list."""
pos = len(self.items)
self.items.extend(items)
if (added := len(self.items) - pos) > 0:
self.items_changed(pos, 0, added)
def index(self, item: GObject.GObject) -> int | None:
"""Find the index of an item in the list."""
try:
return self.items.index(item)
except ValueError:
return None
def insert(self, index: int, item: GObject.GObject) -> bool:
"""Insert an item into the list."""
index = max(0, min(len(self.items), index))
self.items.insert(index, item)
self.items_changed(index, 0, 1)
return True
def items_changed(self, index: int, removed: int, added: int):
"""Notify that the items in the list have changed."""
self.n_items = len(self.items)
super().items_changed(index, removed, added)
def pop(self, index: int) -> GObject.GObject:
"""Remove and return an item by index."""
n_items = len(self.items)
if n_items > 0 and index < len(self.items):
item = self.items.pop(index)
self.items_changed(index, 1, 0)
return item
def remove(self, item: GObject.GObject) -> bool:
"""Remove an item from the list."""
if (index := self.index(item)) is not None:
return self.pop(index) is not None
return False

158
tests/test_store.py Normal file
View File

@ -0,0 +1,158 @@
# Copyright 2023 (c) Anna Schumaker.
"""Tests our custom Python-based ListStore."""
import unittest
import emmental.store
from gi.repository import GObject
from gi.repository import Gio
class Object(GObject.GObject):
"""A simple object for testing."""
value = GObject.Property(type=int)
class TestListStore(unittest.TestCase):
"""Test case for the Python-based ListStore."""
def items_changed(self, store: emmental.store.ListStore,
pos: int, removed: int, added: int) -> None:
"""Handle the items-changed signal."""
self.changed = (pos, removed, added)
def setUp(self):
"""Set up common variables."""
self.store = emmental.store.ListStore()
self.store.connect("items-changed", self.items_changed)
self.changed = (None, None, None)
def test_init(self):
"""Check that the ListStore is initialized properly."""
self.assertIsInstance(self.store, GObject.GObject)
self.assertIsInstance(self.store, Gio.ListModel)
self.assertListEqual(self.store.items, [])
self.assertEqual(self.store.n_items, 0)
def test_get_item_type(self):
"""Test getting the item type for the ListStore."""
self.assertEqual(self.store.get_item_type(), GObject.GObject.__gtype__)
def test_get_item(self):
"""Test accessing items in the ListStore."""
objs = [Object(value=i) for i in range(5)]
self.store.extend(objs)
for i, obj in enumerate(objs):
with self.subTest(i=i):
self.assertEqual(self.store.get_item(i), obj)
self.assertEqual(self.store[i], obj)
self.assertIsNone(self.store.get_item(6))
def test_append(self):
"""Test the ListStore's append() function."""
for i in [0, 1, 2]:
with self.subTest(i=i):
self.assertTrue(self.store.append(Object(value=i)))
self.assertTupleEqual(self.changed, (i, 0, 1))
self.assertListEqual([obj.value for obj in self.store.items],
[v for v in range(i + 1)])
def test_clear(self):
"""Test clearing a ListStore."""
self.store.clear()
self.assertTupleEqual(self.changed, (None, None, None))
self.store.extend([Object(value=i) for i in range(5)])
self.store.clear()
self.assertTupleEqual(self.changed, (0, 5, 0))
def test_extend(self):
"""Test appending multiple values to the ListStore."""
self.store.extend([])
self.assertTupleEqual(self.changed, (None, None, None))
self.assertListEqual(self.store.items, [])
self.store.extend([Object(value=0), Object(value=1)])
self.assertTupleEqual(self.changed, (0, 0, 2))
self.assertListEqual([obj.value for obj in self.store.items],
[0, 1])
self.store.extend([Object(value=2), Object(value=3), Object(value=4)])
self.assertTupleEqual(self.changed, (2, 0, 3))
self.assertListEqual([obj.value for obj in self.store.items],
[0, 1, 2, 3, 4])
def test_index(self):
"""Test finding the index of items in the ListStore."""
objs = [Object(value=i) for i in range(5)]
self.store.extend(objs)
for i, obj in enumerate(objs):
with self.subTest(i=i):
self.assertEqual(self.store.index(obj), i)
self.assertTrue(obj in self.store)
self.assertIsNone(self.store.index(Object(value=10)))
self.assertFalse(Object(value=10) in self.store)
def test_insert(self):
"""Test the ListStore's insert() function."""
self.assertTrue(self.store.insert(0, Object(value=0)))
self.assertTupleEqual(self.changed, (0, 0, 1))
self.assertListEqual([obj.value for obj in self.store.items],
[0])
self.assertTrue(self.store.insert(0, Object(value=1)))
self.assertTupleEqual(self.changed, (0, 0, 1))
self.assertListEqual([obj.value for obj in self.store.items],
[1, 0])
self.assertTrue(self.store.insert(5, Object(value=5)))
self.assertTupleEqual(self.changed, (2, 0, 1))
self.assertListEqual([obj.value for obj in self.store.items],
[1, 0, 5])
self.assertTrue(self.store.insert(-3, Object(value=3)))
self.assertTupleEqual(self.changed, (0, 0, 1))
self.assertListEqual([obj.value for obj in self.store.items],
[3, 1, 0, 5])
def test_length(self):
"""Test requesting the ListStore's length."""
for n in [0, 1, 2]:
with self.subTest(n=n):
self.assertEqual(self.store.get_n_items(), n)
self.assertEqual(self.store.n_items, n)
self.assertEqual(len(self.store), n)
self.store.append(Object(value=n))
def test_pop(self):
"""Test popping objects from the ListStore."""
self.assertIsNone(self.store.pop(0))
self.assertTupleEqual(self.changed, (None, None, None))
objs = [Object(value=v) for v in range(3)]
self.store.extend(objs)
self.changed = (None, None, None)
self.assertIsNone(self.store.pop(5))
self.assertTupleEqual(self.changed, (None, None, None))
self.assertEqual(self.store.pop(0), objs[0])
self.assertTupleEqual(self.changed, (0, 1, 0))
self.assertListEqual([obj.value for obj in self.store.items],
[1, 2])
def test_remove(self):
"""Test removing objects from the ListStore."""
objs = [Object(value=v) for v in range(3)]
self.store.extend(objs)
self.assertTrue(self.store.remove(objs[0]))
self.assertEqual(self.changed, (0, 1, 0))
self.changed = (None, None, None)
self.assertFalse(self.store.remove(objs[0]))
self.assertTupleEqual(self.changed, (None, None, None))