diff options
-rw-r--r-- | .gitlab-ci.yml | 6 | ||||
-rwxr-xr-x | .gitlab-ci/test-docker-gtk4.sh | 10 | ||||
-rwxr-xr-x | .gitlab-ci/test-docker-old.sh | 10 | ||||
-rwxr-xr-x | .gitlab-ci/test-flatpak.sh | 4 | ||||
-rw-r--r-- | gi/overrides/Gtk.py | 50 | ||||
-rw-r--r-- | tests/test_overrides_gtk.py | 286 |
6 files changed, 320 insertions, 46 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a1a569b7..e00e1343 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -114,12 +114,18 @@ pypy3: xenial-i386-py2: stage: build_and_test image: registry.gitlab.gnome.org/gnome/pygobject/old:v2 + artifacts: + paths: + - coverage/ script: - bash -x ./.gitlab-ci/test-docker-old.sh gtk4: stage: build_and_test image: registry.gitlab.gnome.org/gnome/pygobject/gtk4:v1 + artifacts: + paths: + - coverage/ script: - bash -x ./.gitlab-ci/test-docker-gtk4.sh diff --git a/.gitlab-ci/test-docker-gtk4.sh b/.gitlab-ci/test-docker-gtk4.sh index e36c715d..0dd12199 100755 --- a/.gitlab-ci/test-docker-gtk4.sh +++ b/.gitlab-ci/test-docker-gtk4.sh @@ -3,13 +3,17 @@ set -e # ccache setup -mkdir -p _ccache export CCACHE_BASEDIR="$(pwd)" export CCACHE_DIR="${CCACHE_BASEDIR}/_ccache" +COV_DIR="$(pwd)/coverage" +export COVERAGE_FILE="${COV_DIR}/.coverage.${CI_JOB_NAME}" +mkdir -p "${COV_DIR}" +mkdir -p "${CCACHE_DIR}" # test python -m pip install git+https://github.com/pygobject/pycairo.git -python -m pip install pytest pytest-faulthandler +python -m pip install pytest pytest-faulthandler coverage g-ir-inspect Gtk --version=4.0 --print-typelibs export TEST_GTK_VERSION=4.0 -xvfb-run -a python setup.py test +python setup.py build_tests +xvfb-run -a python -m coverage run tests/runtests.py diff --git a/.gitlab-ci/test-docker-old.sh b/.gitlab-ci/test-docker-old.sh index 74c81c77..91312c7c 100755 --- a/.gitlab-ci/test-docker-old.sh +++ b/.gitlab-ci/test-docker-old.sh @@ -7,11 +7,15 @@ virtualenv --python=python _venv source _venv/bin/activate # ccache setup -mkdir -p _ccache export CCACHE_BASEDIR="$(pwd)" export CCACHE_DIR="${CCACHE_BASEDIR}/_ccache" +COV_DIR="$(pwd)/coverage" +export COVERAGE_FILE="${COV_DIR}/.coverage.${CI_JOB_NAME}" +mkdir -p "${COV_DIR}" +mkdir -p "${CCACHE_DIR}" # test python -m pip install git+https://github.com/pygobject/pycairo.git -python -m pip install pytest pytest-faulthandler -xvfb-run -a python setup.py test +python -m pip install pytest pytest-faulthandler coverage +python setup.py build_tests +xvfb-run -a python -m coverage run tests/runtests.py diff --git a/.gitlab-ci/test-flatpak.sh b/.gitlab-ci/test-flatpak.sh index 3e3a9923..3ca5a746 100755 --- a/.gitlab-ci/test-flatpak.sh +++ b/.gitlab-ci/test-flatpak.sh @@ -2,5 +2,5 @@ set -e -python3 -m pip install --user pytest -python3 setup.py test +python3 -m pip install --user pytest pytest-faulthandler +python3 setup.py test -s diff --git a/gi/overrides/Gtk.py b/gi/overrides/Gtk.py index 7e00c611..d9c1a05f 100644 --- a/gi/overrides/Gtk.py +++ b/gi/overrides/Gtk.py @@ -381,12 +381,7 @@ if Gtk._version in ("2.0", "3.0"): def _process_action(group_source, name, stock_id=None, label=None, accelerator=None, tooltip=None, entry_value=0): action = RadioAction(name=name, label=label, tooltip=tooltip, stock_id=stock_id, value=entry_value) - # FIXME: join_group is a patch to Gtk+ 3.0 - # otherwise we can't effectively add radio actions to a - # group. Should we depend on 3.0 and error out here - # or should we offer the functionality via a compat - # C module? - if hasattr(action, 'join_group'): + if Gtk._version == '3.0': action.join_group(group_source) if value == entry_value: @@ -466,8 +461,7 @@ __all__.append('MenuItem') def _get_utf8_length(string): - if not isinstance(string, string_types): - raise TypeError('must be a string') + assert isinstance(string, string_types) if not isinstance(string, bytes): string = string.encode("utf-8") return len(string) @@ -553,8 +547,7 @@ class Dialog(Gtk.Dialog, Container): 'Please use the "add_buttons" method for adding buttons. ' 'See: https://wiki.gnome.org/PyGObject/InitializerDeprecations', PyGTKDeprecationWarning, stacklevel=stacklevel) - if 'buttons' in new_kwargs: - del new_kwargs['buttons'] + new_kwargs.pop('buttons', None) else: add_buttons = None @@ -600,15 +593,15 @@ class Dialog(Gtk.Dialog, Container): """ def _button(b): while b: - t, r = b[0:2] + try: + t, r = b[0:2] + except ValueError: + raise ValueError('Must pass an even number of arguments') b = b[2:] yield t, r - try: - for text, response in _button(args): - self.add_button(text, response) - except (IndexError): - raise TypeError('Must pass an even number of arguments') + for text, response in _button(args): + self.add_button(text, response) Dialog = override(Dialog) @@ -720,13 +713,6 @@ __all__.append('RecentInfo') class TextBuffer(Gtk.TextBuffer): - def _get_or_create_tag_table(self): - table = self.get_tag_table() - if table is None: - table = Gtk.TextTagTable() - self.set_tag_table(table) - - return table def create_tag(self, tag_name=None, **properties): """Creates a tag and adds it to the tag table of the TextBuffer. @@ -753,7 +739,7 @@ class TextBuffer(Gtk.TextBuffer): """ tag = Gtk.TextTag(name=tag_name, **properties) - self._get_or_create_tag_table().add(tag) + self.get_tag_table().add(tag) return tag def create_mark(self, mark_name, where, left_gravity=False): @@ -830,11 +816,7 @@ class TreeModel(Gtk.TreeModel): index = len(self) + key if index < 0: raise IndexError("row index is out of bounds: %d" % key) - try: - aiter = self.get_iter(index) - except ValueError: - raise IndexError("could not find tree path '%s'" % key) - return aiter + return self.get_iter(index) else: try: aiter = self.get_iter(key) @@ -912,11 +894,7 @@ class TreeModel(Gtk.TreeModel): def set_row(self, treeiter, row): converted_row, columns = self._convert_row(row) for column in columns: - value = row[column] - if value is None: - continue # None means skip this row - - self.set_value(treeiter, column, value) + self.set_value(treeiter, column, row[column]) def _convert_value(self, column, value): '''Convert value to a GObject.Value of the expected type''' @@ -1086,8 +1064,8 @@ class TreeModelRow(object): elif isinstance(iter_or_path, Gtk.TreeIter): self.iter = iter_or_path else: - raise TypeError("expected Gtk.TreeIter or Gtk.TreePath, \ - %s found" % type(iter_or_path).__name__) + raise TypeError("expected Gtk.TreeIter or Gtk.TreePath, " + "%s found" % type(iter_or_path).__name__) @property def path(self): diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py index 887a4f3d..ddd652e3 100644 --- a/tests/test_overrides_gtk.py +++ b/tests/test_overrides_gtk.py @@ -11,10 +11,13 @@ import sys import gc import warnings +import pytest + from .helper import ignore_gi_deprecation_warnings, capture_glib_warnings import gi.overrides import gi.types +from gi._compat import cmp from gi.repository import GLib, GObject try: @@ -105,6 +108,24 @@ def test_freeze_child_notif(): @unittest.skipUnless(Gtk, 'Gtk not available') +@unittest.skipIf(Gtk_version == "4.0", "not in gtk4") +def test_menu_popup(): + m = Gtk.Menu() + with capture_glib_warnings(): + m.popup(None, None, None, None, 0, 0) + m.popdown() + + +@unittest.skipUnless(Gtk, 'Gtk not available') +@unittest.skipIf(Gtk_version == "4.0", "not in gtk4") +def test_button_stock(): + with capture_glib_warnings(): + button = Gtk.Button(stock=Gtk.STOCK_OK) + assert button.props.label == Gtk.STOCK_OK + assert button.props.use_stock + + +@unittest.skipUnless(Gtk, 'Gtk not available') def test_wrapper_toggle_refs(): class MyButton(Gtk.Button): def __init__(self, height): @@ -216,14 +237,60 @@ class TestGtk(unittest.TestCase): action.activate() @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") + def test_action_group_error_handling(self): + action_group = Gtk.ActionGroup(name='TestActionGroup') + with pytest.raises(TypeError): + action_group.add_actions(42) + + with pytest.raises(TypeError): + action_group.add_toggle_actions(42) + + with pytest.raises(TypeError): + action_group.add_radio_actions(42) + + @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") + def test_action_group_no_user_data(self): + action_group = Gtk.ActionGroup(name='TestActionGroup') + + called = [] + + def test_action_callback_no_data(action): + called.append(action) + + action_group.add_actions([ + ('test-action1', None, 'Test Action 1', + None, None, test_action_callback_no_data)]) + action_group.add_actions([('test2-action1',)]) + action_group.get_action('test-action1').activate() + + action_group.add_toggle_actions([ + ('test-action2', None, 'Test Action 2', + None, None, test_action_callback_no_data)]) + action_group.add_toggle_actions([('test2-action2',)]) + action_group.get_action('test-action2').activate() + + def test_action_callback_no_data_radio(action, current): + called.append(action) + + action_group.add_radio_actions([ + ('test-action3', None, 'Test Action 3', None, None, 0), + ('test-action4', None, 'Test Action 4', None, None, 1)], + 1, test_action_callback_no_data_radio) + action_group.add_radio_actions([('test2-action3',)]) + action = action_group.get_action('test-action3') + assert action.get_current_value() == 1 + action.activate() + + assert len(called) == 3 + + @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") def test_uimanager(self): self.assertEqual(Gtk.UIManager, gi.overrides.Gtk.UIManager) ui = Gtk.UIManager() ui.add_ui_from_string("""<ui> <menubar name="menubar1"></menubar> </ui> -""" -) +""") menubar = ui.get_widget("/menubar1") self.assertEqual(type(menubar), Gtk.MenuBar) @@ -235,6 +302,9 @@ class TestGtk(unittest.TestCase): self.assertEqual(ag, groups[-2]) self.assertEqual(ag2, groups[-1]) + with pytest.raises(TypeError): + ui.add_ui_from_string(42) + @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") def test_uimanager_nonascii(self): ui = Gtk.UIManager() @@ -356,6 +426,15 @@ class TestGtk(unittest.TestCase): button = dialog.get_widget_for_response(Gtk.ResponseType.CLOSE) self.assertEqual('gtk-close', button.get_label()) + with pytest.raises(ValueError, match="even number"): + dialog.add_buttons('test-button2', 2, 'gtk-close') + + @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") + def test_dialog_deprecated_attributes(self): + dialog = Gtk.Dialog() + assert dialog.action_area == dialog.get_action_area() + assert dialog.vbox == dialog.get_content_area() + def test_about_dialog(self): dialog = Gtk.AboutDialog() self.assertTrue(isinstance(dialog, Gtk.Dialog)) @@ -717,6 +796,11 @@ class TestGtk(unittest.TestCase): pixbuf = GdkPixbuf.Pixbuf() Gtk.IconSet.new_from_pixbuf(pixbuf) + with warnings.catch_warnings(record=True) as warn: + warnings.simplefilter('always') + Gtk.IconSet(pixbuf) + assert issubclass(warn[0].category, PyGTKDeprecationWarning) + def test_viewport(self): vadjustment = Gtk.Adjustment() hadjustment = Gtk.Adjustment() @@ -853,8 +937,15 @@ class TestBuilder(unittest.TestCase): []), } + class SignalTestObject(GObject.GObject): + __gtype_name__ = "GIOverrideSignalTestObject" + def test_add_from_string(self): builder = Gtk.Builder() + + with pytest.raises(TypeError): + builder.add_from_string(object()) + builder.add_from_string(u"") builder.add_from_string("") @@ -877,6 +968,9 @@ class TestBuilder(unittest.TestCase): builder.add_objects_from_string("", ['']) builder.add_objects_from_string(get_example(u"รค" * 1000), ['']) + with pytest.raises(TypeError): + builder.add_objects_from_string(object(), []) + def test_extract_handler_and_args_object(self): class Obj(): pass @@ -908,6 +1002,13 @@ class TestBuilder(unittest.TestCase): Gtk._extract_handler_and_args, obj, 'not_a_handler') + def test_extract_handler_and_args_type_mismatch(self): + with pytest.raises(TypeError): + Gtk._extract_handler_and_args({"foo": object()}, "foo") + + with pytest.raises(TypeError): + Gtk._extract_handler_and_args({"foo": []}, "foo") + def test_builder_with_handler_and_args(self): builder = Gtk.Builder() builder.add_from_string(""" @@ -936,6 +1037,31 @@ class TestBuilder(unittest.TestCase): self.assertSequenceEqual(args_collector[0], (obj, 1, 2)) self.assertSequenceEqual(args_collector[1], (obj, )) + def test_builder_with_handler_object(self): + builder = Gtk.Builder() + builder.add_from_string(""" + <interface> + <object class="GIOverrideSignalTestObject" id="foo"/> + <object class="GIOverrideSignalTest" id="object_sig_test"> + <signal name="test-signal" handler="on_signal1" object="foo"/> + <signal name="test-signal" handler="on_signal2" after="yes" object="foo" /> + </object> + </interface> + """) + + args_collector = [] + + def on_signal(*args): + args_collector.append(args) + + builder.connect_signals({'on_signal1': (on_signal, 1, 2), + 'on_signal2': on_signal}) + obj, emitter = builder.get_objects() + emitter.emit("test-signal") + assert len(args_collector) == 2 + assert args_collector[0] == (obj, 1, 2) + assert args_collector[1] == (obj, ) + def test_builder(self): self.assertEqual(Gtk.Builder, gi.overrides.Gtk.Builder) @@ -996,6 +1122,31 @@ class TestBuilder(unittest.TestCase): self.assertEqual(signal_checker.after_sentinel, 2) +@unittest.skipUnless(Gtk, 'Gtk not available') +class TestTreeModelRow(unittest.TestCase): + def test_tree_model_row(self): + model = Gtk.TreeStore(int) + iter_ = model.append(None, [42]) + path = model.get_path(iter_) + Gtk.TreeModelRow(model, iter_) + row = Gtk.TreeModelRow(model, path) + + with pytest.raises(TypeError): + row["foo"] + + with pytest.raises(TypeError): + Gtk.TreeModelRow(model, 42) + + with pytest.raises(TypeError): + Gtk.TreeModelRow(42, path) + + iter_ = model.append(None, [24]) + row = Gtk.TreeModelRow(model, iter_) + assert row.previous[0] == 42 + assert row.get_previous()[0] == 42 + assert row.previous.previous is None + + @ignore_gi_deprecation_warnings @unittest.skipUnless(Gtk, 'Gtk not available') class TestTreeModel(unittest.TestCase): @@ -2056,6 +2207,14 @@ class TestTreeModel(unittest.TestCase): filtered[0][1] = 'ONE' self.assertEqual(filtered[0][1], 'ONE') + def foo(store, iter_, data): + assert data is None + return False + + filtered.set_visible_func(foo) + filtered.refilter() + assert len(filtered) == 0 + def test_list_store_performance(self): model = Gtk.ListStore(int, str) @@ -2074,6 +2233,89 @@ class TestTreeModel(unittest.TestCase): filt = model.filter_new() self.assertTrue(filt is not None) + def test_tree_store_set(self): + tree_store = Gtk.TreeStore(int, int) + iter_ = tree_store.append(None) + tree_store.set(iter_) + assert tree_store.get_value(iter_, 0) == 0 + tree_store.set(iter_, 0, 42) + assert tree_store.get_value(iter_, 0) == 42 + assert tree_store.get_value(iter_, 1) == 0 + tree_store.set(iter_, 0, 42, 1, 24) + assert tree_store.get_value(iter_, 1) == 24 + tree_store.set(iter_, 1, 124) + assert tree_store.get_value(iter_, 1) == 124 + + with pytest.raises(TypeError): + tree_store.set(iter_, 0, 42, "foo", 24) + with pytest.raises(TypeError): + tree_store.set(iter_, "foo") + with pytest.raises(TypeError): + tree_store.set(iter_, [1, 2, 3]) + with pytest.raises(TypeError): + tree_store.set(iter_, 0, 42, 24) + + def test_list_store_set(self): + list_store = Gtk.ListStore(int, int) + iter_ = list_store.append() + list_store.set(iter_) + assert list_store.get_value(iter_, 0) == 0 + list_store.set(iter_, 0, 42) + assert list_store.get_value(iter_, 0) == 42 + assert list_store.get_value(iter_, 1) == 0 + list_store.set(iter_, 0, 42, 1, 24) + assert list_store.get_value(iter_, 1) == 24 + list_store.set(iter_, 1, 124) + assert list_store.get_value(iter_, 1) == 124 + + with pytest.raises(TypeError): + list_store.set(iter_, 0, 42, "foo", 24) + with pytest.raises(TypeError): + list_store.set(iter_, "foo") + with pytest.raises(TypeError): + list_store.set(iter_, [1, 2, 3]) + with pytest.raises(TypeError): + list_store.set(iter_, 0, 42, 24) + + def test_set_default_sort_func(self): + list_store = Gtk.ListStore(int) + list_store.append([2]) + list_store.append([1]) + + def sort_func(store, iter1, iter2, data): + assert data is None + return cmp(store[iter1][0], store[iter2][0]) + + list_store.set_default_sort_func(sort_func) + list_store.set_sort_column_id( + Gtk.TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, Gtk.SortType.ASCENDING) + assert [v[0] for v in list_store] == [1, 2] + + def test_model_rows_reordered(self): + list_store = Gtk.ListStore(int) + list_store.append([2]) + list_store.append([1]) + list_store.rows_reordered(Gtk.TreePath.new_first(), None, [0, 1]) + list_store.rows_reordered(0, None, [0, 1]) + + def test_model_set_row(self): + list_store = Gtk.ListStore(int, int) + list_store.append([1, 2]) + iter_ = list_store.get_iter_first() + list_store.set_row(iter_, [3, 4]) + assert list_store[0][:] == [3, 4] + list_store.set_row(iter_, [None, 7]) + assert list_store[0][:] == [3, 7] + list_store.set_row(iter_, [None, GObject.Value(int, 9)]) + assert list_store[0][:] == [3, 9] + + def test_model_set_row_skip_on_none(self): + list_store = Gtk.ListStore(int, int, int, int) + list_store.append([1, 2, 3, 4]) + iter_ = list_store.get_iter_first() + list_store.set_row(iter_, [None, 7, None, 9]) + assert list_store[0][:] == [1, 7, 3, 9] + @unittest.skipIf(sys.platform == "darwin", "hangs") @unittest.skipUnless(Gtk, 'Gtk not available') @@ -2184,12 +2426,31 @@ class TestTreeView(unittest.TestCase): self.assertEqual(m, store) self.assertEqual(store.get_path(s), firstpath) + sel.unselect_all() + (m, s) = sel.get_selected() + assert s is None + + sel.select_path(0) + m, r = sel.get_selected_rows() + assert m == store + assert len(r) == 1 + assert r[0] == store[0].path + + def test_scroll_to_cell(self): + store = Gtk.ListStore(int, str) + store.append((0, "foo")) + view = Gtk.TreeView() + view.set_model(store) + view.scroll_to_cell(store[0].path) + view.scroll_to_cell(0) + @unittest.skipUnless(Gtk, 'Gtk not available') class TestTextBuffer(unittest.TestCase): def test_text_buffer(self): self.assertEqual(Gtk.TextBuffer, gi.overrides.Gtk.TextBuffer) buffer = Gtk.TextBuffer() + assert buffer.get_tag_table() is not None tag = buffer.create_tag('title', font='Sans 18') self.assertEqual(tag.props.name, 'title') @@ -2261,6 +2522,15 @@ class TestTextBuffer(unittest.TestCase): self.assertRaises(ValueError, buffer.insert_with_tags_by_name, buffer.get_start_iter(), 'HelloHello', 'unknowntag') + def test_insert(self): + buffer = Gtk.TextBuffer() + start = buffer.get_bounds()[0] + with pytest.raises(TypeError): + buffer.insert(start, 42) + + with pytest.raises(TypeError): + buffer.insert_at_cursor(42) + def test_text_iter(self): try: starts_tag = Gtk.TextIter.starts_tag @@ -2337,6 +2607,18 @@ class TestTextBuffer(unittest.TestCase): @unittest.skipUnless(Gtk, 'Gtk not available') +class TestPaned(unittest.TestCase): + + def test_pack_defaults(self): + p = Gtk.Paned() + l1 = Gtk.Label() + l2 = Gtk.Label() + p.pack1(l1) + p.pack2(l2) + assert p.get_children() == [l1, l2] + + +@unittest.skipUnless(Gtk, 'Gtk not available') class TestContainer(unittest.TestCase): @unittest.skipIf(Gtk_version == "4.0", "not in gtk4") |