summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Reiter <reiter.christoph@gmail.com>2018-04-16 14:38:50 +0200
committerChristoph Reiter <reiter.christoph@gmail.com>2018-04-16 22:02:36 +0200
commit14574267f1711b6d51738f9e371837202babd04b (patch)
treefc4e989413b6753c96627677f84df9c1e2f34c49
parentad1bbfa148b7734e2fca3c9f0e14ddab630bc354 (diff)
downloadpygobject-listmodel-sequence.tar.gz
Gio.ListModel: implement most of the mutable sequence protocol. See #115listmodel-sequence
Adds all the dunder methods for MutableSequence to Gio.ListModel and Gio.ListStore. __delitem__ supports atomic deletion of slices through splice() when possible. __setitem__ has to fall back to remove/delete since adding items with splice doesn't work right, see https://bugzilla.gnome.org/show_bug.cgi?id=795307
-rw-r--r--gi/_compat.py2
-rw-r--r--gi/overrides/Gio.py106
-rw-r--r--tests/test_overrides_gio.py241
3 files changed, 349 insertions, 0 deletions
diff --git a/gi/_compat.py b/gi/_compat.py
index b8a3506c..b4cc46d8 100644
--- a/gi/_compat.py
+++ b/gi/_compat.py
@@ -29,6 +29,7 @@ if sys.version_info[0] == 2:
text_type = eval("unicode")
reload = eval("reload")
+ xrange = eval("xrange")
exec("def reraise(tp, value, tb):\n raise tp, value, tb")
else:
@@ -47,6 +48,7 @@ else:
from importlib import reload
reload
+ xrange = range
def reraise(tp, value, tb):
raise tp(value).with_traceback(tb)
diff --git a/gi/overrides/Gio.py b/gi/overrides/Gio.py
index 5ab23fcd..8604d550 100644
--- a/gi/overrides/Gio.py
+++ b/gi/overrides/Gio.py
@@ -23,6 +23,7 @@ import warnings
from .._ossighelper import wakeup_on_signal, register_sigint_fallback
from ..overrides import override, deprecated_init
from ..module import get_introspection_module
+from .._compat import xrange
from gi import PyGIWarning
from gi.repository import GLib
@@ -271,3 +272,108 @@ class DBusProxy(Gio.DBusProxy):
DBusProxy = override(DBusProxy)
__all__.append('DBusProxy')
+
+
+class ListModel(Gio.ListModel):
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ return [self.get_item(i) for i in xrange(*key.indices(len(self)))]
+ elif isinstance(key, int):
+ if key < 0:
+ key += len(self)
+ if key < 0:
+ raise IndexError
+ ret = self.get_item(key)
+ if ret is None:
+ raise IndexError
+ return ret
+ else:
+ raise TypeError
+
+ def __contains__(self, item):
+ pytype = self.get_item_type().pytype
+ if not isinstance(item, pytype):
+ raise TypeError(
+ "Expected type %s.%s" % (pytype.__module__, pytype.__name__))
+ for i in self:
+ if i == item:
+ return True
+ return False
+
+ def __len__(self):
+ return self.get_n_items()
+
+ def __iter__(self):
+ for i in xrange(len(self)):
+ yield self.get_item(i)
+
+
+ListModel = override(ListModel)
+__all__.append('ListModel')
+
+
+class ListStore(Gio.ListStore):
+
+ def __delitem__(self, key):
+ if isinstance(key, slice):
+ start, stop, step = key.indices(len(self))
+ if step == 1:
+ self.splice(start, max(stop - start, 0), [])
+ elif step == -1:
+ self.splice(stop + 1, max(start - stop, 0), [])
+ else:
+ for i in sorted(xrange(start, stop, step), reverse=True):
+ self.remove(i)
+ elif isinstance(key, int):
+ if key < 0:
+ key += len(self)
+ if key < 0 or key >= len(self):
+ raise IndexError
+ self.remove(key)
+ else:
+ raise TypeError
+
+ def __setitem__(self, key, value):
+ if isinstance(key, slice):
+ pytype = self.get_item_type().pytype
+ valuelist = []
+ for v in value:
+ if not isinstance(v, pytype):
+ raise TypeError(
+ "Expected type %s.%s" % (
+ pytype.__module__, pytype.__name__))
+ valuelist.append(v)
+
+ start, stop, step = key.indices(len(self))
+ if step == 1:
+ self.__delitem__(key)
+ for v in reversed(valuelist):
+ self.insert(start, v)
+ else:
+ indices = list(xrange(start, stop, step))
+ if len(indices) != len(valuelist):
+ raise ValueError
+ for i, v in zip(indices, valuelist):
+ self.remove(i)
+ self.insert(i, v)
+ elif isinstance(key, int):
+ if key < 0:
+ key += len(self)
+ if key < 0 or key >= len(self):
+ raise IndexError
+
+ pytype = self.get_item_type().pytype
+ if not isinstance(value, pytype):
+ raise TypeError(
+ "Expected type %s.%s" % (
+ pytype.__module__, pytype.__name__))
+
+ self.remove(key)
+ self.insert(key, value)
+ else:
+ raise TypeError
+
+
+ListStore = override(ListStore)
+__all__.append('ListStore')
diff --git a/tests/test_overrides_gio.py b/tests/test_overrides_gio.py
new file mode 100644
index 00000000..3a625231
--- /dev/null
+++ b/tests/test_overrides_gio.py
@@ -0,0 +1,241 @@
+from __future__ import absolute_import
+
+import random
+
+import pytest
+
+from gi.repository import Gio, GObject
+
+
+class Item(GObject.Object):
+ _id = 0
+
+ def __init__(self):
+ super(Item, self).__init__()
+ Item._id += 1
+ self._id = self._id
+
+ def __repr__(self):
+ return str(self._id)
+
+
+def test_list_model_len():
+ model = Gio.ListStore.new(Item)
+ assert len(model) == 0
+ assert not model
+ for i in range(1, 10):
+ model.append(Item())
+ assert len(model) == i
+ assert model
+ model.remove_all()
+ assert not model
+ assert len(model) == 0
+
+
+def test_list_model_get_item_simple():
+ model = Gio.ListStore.new(Item)
+ with pytest.raises(IndexError):
+ model[0]
+ first_item = Item()
+ model.append(first_item)
+ assert model[0] is first_item
+ assert model[-1] is first_item
+ second_item = Item()
+ model.append(second_item)
+ assert model[1] is second_item
+ assert model[-1] is second_item
+ assert model[-2] is first_item
+ with pytest.raises(IndexError):
+ model[-3]
+
+
+def test_list_model_get_item_slice():
+ model = Gio.ListStore.new(Item)
+ source = [Item() for i in range(30)]
+ for i in source:
+ model.append(i)
+ assert model[1:10] == source[1:10]
+ assert model[1:-2] == source[1:-2]
+ assert model[-4:-1] == source[-4:-1]
+ assert model[-100:-1] == source[-100:-1]
+ assert model[::-1] == source[::-1]
+ assert model[:] == source[:]
+
+
+def test_list_model_contains():
+ model = Gio.ListStore.new(Item)
+ item = Item()
+ model.append(item)
+ assert item in model
+ assert Item() not in model
+ with pytest.raises(TypeError):
+ object() in model
+ with pytest.raises(TypeError):
+ None in model
+
+
+def test_list_store_delitem_simple():
+ store = Gio.ListStore.new(Item)
+ store.append(Item())
+ del store[0]
+ assert not store
+ with pytest.raises(IndexError):
+ del store[0]
+ with pytest.raises(IndexError):
+ del store[-1]
+
+ store.append(Item())
+ with pytest.raises(IndexError):
+ del store[-2]
+ del store[-1]
+ assert not store
+
+ source = [Item(), Item()]
+ store.append(source[0])
+ store.append(source[1])
+ del store[-1]
+ assert store[:] == [source[0]]
+
+
+def test_list_store_delitem_slice():
+
+ def do_del(count, key):
+
+ events = []
+
+ def on_changed(m, *args):
+ events.append(args)
+
+ store = Gio.ListStore.new(Item)
+ source = [Item() for i in range(count)]
+ for item in source:
+ store.append(item)
+ store.connect("items-changed", on_changed)
+ source.__delitem__(key)
+ store.__delitem__(key)
+ assert source == store[:]
+ return events
+
+ values = [None, 1, -15, 3, -2, 0, -3, 5, 7]
+ variants = set()
+ for i in range(500):
+ start = random.choice(values)
+ stop = random.choice(values)
+ step = random.choice(values)
+ length = abs(random.choice(values) or 0)
+ if step == 0:
+ step += 1
+ variants.add((length, start, stop, step))
+
+ for length, start, stop, step in variants:
+ do_del(length, slice(start, stop, step))
+
+ # basics
+ do_del(10, slice(None, None, None))
+ do_del(10, slice(None, None, None))
+ do_del(10, slice(None, None, -1))
+ do_del(10, slice(0, 5, None))
+ do_del(10, slice(0, 10, 1))
+ do_del(10, slice(0, 10, 2))
+ do_del(10, slice(14, 2, -1))
+
+ # test some fast paths
+ assert do_del(100, slice(None, None, None)) == [(0, 100, 0)]
+ assert do_del(100, slice(None, None, -1)) == [(0, 100, 0)]
+ assert do_del(100, slice(0, 50, 1)) == [(0, 50, 0)]
+
+
+def test_list_store_setitem_simple():
+
+ store = Gio.ListStore.new(Item)
+ first = Item()
+ store.append(first)
+
+ class Wrong(GObject.Object):
+ pass
+
+ with pytest.raises(TypeError):
+ store[0] = object()
+ with pytest.raises(TypeError):
+ store[0] = None
+ with pytest.raises(TypeError):
+ store[0] = Wrong()
+
+ assert store[:] == [first]
+
+ new = Item()
+ store[0] = new
+ assert len(store) == 1
+ store[-1] = Item()
+ assert len(store) == 1
+
+ with pytest.raises(IndexError):
+ store[1] = Item()
+ with pytest.raises(IndexError):
+ store[-2] = Item()
+
+ store = Gio.ListStore.new(Item)
+ source = [Item(), Item(), Item()]
+ for item in source:
+ store.append(item)
+ new = Item()
+ store[1] = new
+ assert store[:] == [source[0], new, source[2]]
+
+
+def test_list_store_setitem_slice():
+
+ def do_set(count, key, new_count):
+ store = Gio.ListStore.new(Item)
+ source = [Item() for i in range(count)]
+ new = [Item() for i in range(new_count)]
+ for item in source:
+ store.append(item)
+ source_error = None
+ try:
+ source.__setitem__(key, new)
+ except ValueError as e:
+ source_error = type(e)
+
+ store_error = None
+ try:
+ store.__setitem__(key, new)
+ except Exception as e:
+ store_error = type(e)
+
+ assert source_error == store_error
+ assert source == store[:]
+
+ values = [None, 1, -15, 3, -2, 0, 3, 4, 100]
+ variants = set()
+ for i in range(500):
+ start = random.choice(values)
+ stop = random.choice(values)
+ step = random.choice(values)
+ length = abs(random.choice(values) or 0)
+ new = random.choice(values) or 0
+ if step == 0:
+ step += 1
+ variants.add((length, start, stop, step, new))
+
+ for length, start, stop, step, new in variants:
+ do_set(length, slice(start, stop, step), new)
+
+ # basics
+ do_set(10, slice(None, None, None), 20)
+ do_set(10, slice(None, None, None), 0)
+ do_set(10, slice(None, None, -1), 20)
+ do_set(10, slice(None, None, -1), 10)
+ do_set(10, slice(0, 5, None), 20)
+ do_set(10, slice(0, 10, 1), 0)
+
+ # test iterators
+ store = Gio.ListStore.new(Item)
+ store[:] = iter([Item() for i in range(10)])
+ assert len(store) == 10
+
+ # make sure we do all or nothing
+ store = Gio.ListStore.new(Item)
+ with pytest.raises(TypeError):
+ store[:] = [Item(), object()]
+ assert len(store) == 0