diff options
author | Christoph Reiter <reiter.christoph@gmail.com> | 2018-04-16 14:38:50 +0200 |
---|---|---|
committer | Christoph Reiter <reiter.christoph@gmail.com> | 2018-04-16 22:02:36 +0200 |
commit | 14574267f1711b6d51738f9e371837202babd04b (patch) | |
tree | fc4e989413b6753c96627677f84df9c1e2f34c49 | |
parent | ad1bbfa148b7734e2fca3c9f0e14ddab630bc354 (diff) | |
download | pygobject-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.py | 2 | ||||
-rw-r--r-- | gi/overrides/Gio.py | 106 | ||||
-rw-r--r-- | tests/test_overrides_gio.py | 241 |
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 |