summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Reiter <reiter.christoph@gmail.com>2018-04-03 21:07:56 +0200
committerChristoph Reiter <reiter.christoph@gmail.com>2018-04-12 16:46:42 +0200
commitc745866df9e4394c6615e79779fff1221a84953a (patch)
tree0fd1a94ead9d5b0a0b0615f3db4f3ae52737278d
parent5441286a41336617b6ce9f37a41980d3941eb8ee (diff)
downloadpygobject-c745866df9e4394c6615e79779fff1221a84953a.tar.gz
Add a minimal implementation of Gtk.Template. See #52
This tries to add a minimal API which allows for basic template usage with the possibility to maybe add more automation/subclassing/nesting later on. Compared to gi_composites.py this adds parameters to Child and Callback to set the name, init_template() doesn't need to be called and is stricter in what it supports to allow future improvements. The _gtktemplate.py file should be resuable with older PyGObject versions with the only difference that init_template() needs to be called.
-rw-r--r--gi/_gtktemplate.py218
-rw-r--r--gi/gimodule.c19
-rw-r--r--gi/overrides/Gtk.py5
-rw-r--r--tests/test_gtk_template.py482
4 files changed, 721 insertions, 3 deletions
diff --git a/gi/_gtktemplate.py b/gi/_gtktemplate.py
new file mode 100644
index 00000000..37707e8c
--- /dev/null
+++ b/gi/_gtktemplate.py
@@ -0,0 +1,218 @@
+# Copyright 2015 Dustin Spicuzza <dustin@virtualroadside.com>
+# 2018 Nikita Churaev <lamefun.x0r@gmail.com>
+# 2018 Christoph Reiter <reiter.christoph@gmail.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+from gi.repository import GLib, GObject, Gio
+
+
+def connect_func(builder, obj, signal_name, handler_name,
+ connect_object, flags, cls):
+
+ if handler_name not in cls.__gtktemplate_methods__:
+ return
+
+ method_name = cls.__gtktemplate_methods__[handler_name]
+ template_inst = builder.get_object(cls.__gtype_name__)
+ template_inst.__gtktemplate_handlers__.add(handler_name)
+ handler = getattr(template_inst, method_name)
+
+ after = int(flags & GObject.ConnectFlags.AFTER)
+ swapped = int(flags & GObject.ConnectFlags.SWAPPED)
+ if swapped:
+ raise RuntimeError(
+ "%r not supported" % GObject.ConnectFlags.SWAPPED)
+
+ if connect_object is not None:
+ if after:
+ func = obj.connect_object_after
+ else:
+ func = obj.connect_object
+ func(signal_name, handler, connect_object)
+ else:
+ if after:
+ func = obj.connect_after
+ else:
+ func = obj.connect
+ func(signal_name, handler)
+
+
+def register_template(cls):
+ bound_methods = {}
+ bound_widgets = {}
+
+ for attr_name, obj in list(cls.__dict__.items()):
+ if isinstance(obj, CallThing):
+ setattr(cls, attr_name, obj._func)
+ handler_name = obj._name
+ if handler_name is None:
+ handler_name = attr_name
+
+ if handler_name in bound_methods:
+ old_attr_name = bound_methods[handler_name]
+ raise RuntimeError(
+ "Error while exposing handler %r as %r, "
+ "already available as %r" % (
+ handler_name, attr_name, old_attr_name))
+ else:
+ bound_methods[handler_name] = attr_name
+ elif isinstance(obj, Child):
+ widget_name = obj._name
+ if widget_name is None:
+ widget_name = attr_name
+
+ if widget_name in bound_widgets:
+ old_attr_name = bound_widgets[widget_name]
+ raise RuntimeError(
+ "Error while exposing child %r as %r, "
+ "already available as %r" % (
+ widget_name, attr_name, old_attr_name))
+ else:
+ bound_widgets[widget_name] = attr_name
+ cls.bind_template_child_full(widget_name, False, 0)
+
+ cls.__gtktemplate_methods__ = bound_methods
+ cls.__gtktemplate_widgets__ = bound_widgets
+
+ cls.set_connect_func(connect_func, cls)
+
+ base_init_template = cls.init_template
+ cls.__dontuse_ginstance_init__ = \
+ lambda s: init_template(s, cls, base_init_template)
+ # To make this file work with older PyGObject we expose our init code
+ # as init_template() but make it a noop when we call it ourselves first
+ cls.init_template = cls.__dontuse_ginstance_init__
+
+
+def init_template(self, cls, base_init_template):
+ cls.init_template = lambda s: None
+
+ if self.__class__ is not cls:
+ raise TypeError(
+ "Inheritance from classes with @Gtk.Template decorators "
+ "is not allowed at this time")
+
+ self.__gtktemplate_handlers__ = set()
+
+ base_init_template(self)
+
+ for widget_name, attr_name in self.__gtktemplate_widgets__.items():
+ self.__dict__[attr_name] = self.get_template_child(cls, widget_name)
+
+ for handler_name, attr_name in self.__gtktemplate_methods__.items():
+ if handler_name not in self.__gtktemplate_handlers__:
+ raise RuntimeError(
+ "Handler '%s' was declared with @Gtk.Template.Callback "
+ "but was not present in template" % handler_name)
+
+
+class Child(object):
+
+ def __init__(self, name=None):
+ self._name = name
+
+
+class CallThing(object):
+
+ def __init__(self, name, func):
+ self._name = name
+ self._func = func
+
+
+class Callback(object):
+
+ def __init__(self, name=None):
+ self._name = name
+
+ def __call__(self, func):
+ return CallThing(self._name, func)
+
+
+class Template(object):
+
+ def __init__(self, **kwargs):
+ self.string = None
+ self.filename = None
+ self.resource_path = None
+ if "string" in kwargs:
+ self.string = kwargs.pop("string")
+ elif "filename" in kwargs:
+ self.filename = kwargs.pop("filename")
+ elif "resource_path" in kwargs:
+ self.resource_path = kwargs.pop("resource_path")
+ else:
+ raise TypeError(
+ "Requires one of the following arguments: "
+ "string, filename, resource_path")
+
+ if kwargs:
+ raise TypeError("Unhandled keyword arguments %r" % kwargs)
+
+ @classmethod
+ def from_file(cls, filename):
+ return cls(filename=filename)
+
+ @classmethod
+ def from_string(cls, string):
+ return cls(string=string)
+
+ @classmethod
+ def from_resource(cls, resource_path):
+ return cls(resource_path=resource_path)
+
+ Callback = Callback
+
+ Child = Child
+
+ def __call__(self, cls):
+ from gi.repository import Gtk
+
+ if not isinstance(cls, type) or not issubclass(cls, Gtk.Widget):
+ raise TypeError("Can only use @Gtk.Template on Widgets")
+
+ if "__gtype_name__" not in cls.__dict__:
+ raise TypeError(
+ "%r does not have a __gtype_name__. Set it to the name "
+ "of the class in your template" % cls.__name__)
+
+ if hasattr(cls, "__gtktemplate_methods__"):
+ raise TypeError("Cannot nest template classes")
+
+ if self.string is not None:
+ data = self.string
+ if not isinstance(data, bytes):
+ data = data.encode("utf-8")
+ bytes_ = GLib.Bytes.new(data)
+ cls.set_template(bytes_)
+ register_template(cls)
+ return cls
+ elif self.resource_path is not None:
+ Gio.resources_get_info(
+ self.resource_path, Gio.ResourceLookupFlags.NONE)
+ cls.set_template_from_resource(self.resource_path)
+ register_template(cls)
+ return cls
+ else:
+ assert self.filename is not None
+ file_ = Gio.File.new_for_path(self.filename)
+ bytes_ = GLib.Bytes.new(file_.load_contents()[1])
+ cls.set_template(bytes_)
+ register_template(cls)
+ return cls
+
+
+__all__ = ["Template"]
diff --git a/gi/gimodule.c b/gi/gimodule.c
index f42ad2b1..8e5d7225 100644
--- a/gi/gimodule.c
+++ b/gi/gimodule.c
@@ -1031,6 +1031,7 @@ pygobject__g_instance_init(GTypeInstance *instance,
{
GObject *object = (GObject *) instance;
PyObject *wrapper, *args, *kwargs;
+ PyGILState_STATE state;
wrapper = g_object_get_qdata(object, pygobject_wrapper_key);
if (wrapper == NULL) {
@@ -1041,12 +1042,13 @@ pygobject__g_instance_init(GTypeInstance *instance,
}
}
pygobject_init_wrapper_set(NULL);
+
+ state = PyGILState_Ensure();
+
if (wrapper == NULL) {
/* this looks like a python object created through
* g_object_new -> we have no python wrapper, so create it
* now */
- PyGILState_STATE state;
- state = PyGILState_Ensure();
wrapper = pygobject_new_full(object,
/*steal=*/ FALSE,
g_class);
@@ -1062,8 +1064,19 @@ pygobject__g_instance_init(GTypeInstance *instance,
Py_DECREF(args);
Py_DECREF(kwargs);
- PyGILState_Release(state);
}
+
+ /* XXX: used for Gtk.Template */
+ if (PyObject_HasAttrString (wrapper, "__dontuse_ginstance_init__")) {
+ PyObject *result;
+ result = PyObject_CallMethod (wrapper, "__dontuse_ginstance_init__", NULL);
+ if (result == NULL)
+ PyErr_Print ();
+ else
+ Py_DECREF (result);
+ }
+
+ PyGILState_Release(state);
}
/* This implementation is bad, see bug 566571 for an example why.
diff --git a/gi/overrides/Gtk.py b/gi/overrides/Gtk.py
index 0cdd648f..23d06adf 100644
--- a/gi/overrides/Gtk.py
+++ b/gi/overrides/Gtk.py
@@ -25,6 +25,7 @@ import warnings
from gi.repository import GObject
from .._ossighelper import wakeup_on_signal, register_sigint_fallback
+from .._gtktemplate import Template
from ..overrides import override, strip_boolean_result, deprecated_init
from ..module import get_introspection_module
from .._compat import string_types
@@ -35,6 +36,10 @@ Gtk = get_introspection_module('Gtk')
__all__ = []
+
+Template = Template
+__all__.append('Template')
+
if Gtk._version == '2.0':
warn_msg = "You have imported the Gtk 2.0 module. Because Gtk 2.0 \
was not designed for use with introspection some of the \
diff --git a/tests/test_gtk_template.py b/tests/test_gtk_template.py
new file mode 100644
index 00000000..e55f2983
--- /dev/null
+++ b/tests/test_gtk_template.py
@@ -0,0 +1,482 @@
+# coding: UTF-8
+
+from __future__ import absolute_import
+
+import tempfile
+import os
+import pytest
+
+Gtk = pytest.importorskip("gi.repository.Gtk")
+GLib = pytest.importorskip("gi.repository.GLib")
+GObject = pytest.importorskip("gi.repository.GObject")
+Gio = pytest.importorskip("gi.repository.Gio")
+
+
+from .helper import capture_exceptions
+
+
+def new_gtype_name(_count=[0]):
+ _count[0] += 1
+ return "GtkTemplateTest%d" % _count[0]
+
+
+def ensure_resource_registered():
+ resource_path = "/org/gnome/pygobject/test/a.ui"
+
+ def is_registered(path):
+ try:
+ Gio.resources_get_info(path, Gio.ResourceLookupFlags.NONE)
+ except GLib.Error:
+ return False
+ return True
+
+ if is_registered(resource_path):
+ return resource_path
+
+ gresource_data = (
+ b'GVariant\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00'
+ b'\xc8\x00\x00\x00\x00\x00\x00(\x06\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00'
+ b'\x06\x00\x00\x00KP\x90\x0b\x03\x00\x00\x00\xc8\x00\x00\x00'
+ b'\x04\x00L\x00\xcc\x00\x00\x00\xd0\x00\x00\x00\xb0\xb7$0'
+ b'\x00\x00\x00\x00\xd0\x00\x00\x00\x06\x00L\x00\xd8\x00\x00\x00'
+ b'\xdc\x00\x00\x00f\xc30\xd1\x01\x00\x00\x00\xdc\x00\x00\x00'
+ b'\n\x00L\x00\xe8\x00\x00\x00\xec\x00\x00\x00\xd4\xb5\x02\x00'
+ b'\xff\xff\xff\xff\xec\x00\x00\x00\x01\x00L\x00\xf0\x00\x00\x00'
+ b'\xf4\x00\x00\x005H}\xe3\x02\x00\x00\x00\xf4\x00\x00\x00'
+ b'\x05\x00L\x00\xfc\x00\x00\x00\x00\x01\x00\x00\xa2^\xd6t'
+ b'\x04\x00\x00\x00\x00\x01\x00\x00\x04\x00v\x00\x08\x01\x00\x00'
+ b'\xa5\x01\x00\x00org/\x01\x00\x00\x00gnome/\x00\x00\x02\x00\x00\x00'
+ b'pygobject/\x00\x00\x04\x00\x00\x00/\x00\x00\x00\x00\x00\x00\x00'
+ b'test/\x00\x00\x00\x05\x00\x00\x00a.ui\x00\x00\x00\x00'
+ b'\x8d\x00\x00\x00\x00\x00\x00\x00<interface>\n <template class="G'
+ b'tkTemplateTestResource" parent="GtkBox">\n <property name="spaci'
+ b'ng">42</property>\n </template>\n</interface>\n\x00\x00(uuay)'
+ )
+
+ resource = Gio.Resource.new_from_data(GLib.Bytes.new(gresource_data))
+ Gio.resources_register(resource)
+ assert is_registered(resource_path)
+ return resource_path
+
+
+def test_allow_init_template_call():
+
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ </template>
+</interface>
+""".format(type_name)
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ def __init__(self):
+ super(Foo, self).__init__()
+ self.init_template()
+
+ Foo()
+
+
+def test_main_example():
+
+ type_name = new_gtype_name()
+
+ example_xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkButton" id="hello_button">
+ <property name="label">Hello World</property>
+ <signal name="clicked" handler="hello_button_clicked"
+ object="{0}" swapped="no"/>
+ <signal name="clicked" handler="hello_button_clicked_after"
+ object="{0}" swapped="no" after="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="goodbye_button">
+ <property name="label">Goodbye World</property>
+ <signal name="clicked" handler="goodbye_button_clicked"/>
+ <signal name="clicked" handler="goodbye_button_clicked_after"
+ after="yes"/>
+ </object>
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ @Gtk.Template.from_string(example_xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ def __init__(self):
+ super(Foo, self).__init__()
+ self.callback_hello = []
+ self.callback_hello_after = []
+ self.callback_goodbye = []
+ self.callback_goodbye_after = []
+
+ @Gtk.Template.Callback("hello_button_clicked")
+ def _hello_button_clicked(self, *args):
+ self.callback_hello.append(args)
+
+ @Gtk.Template.Callback("hello_button_clicked_after")
+ def _hello_after(self, *args):
+ self.callback_hello_after.append(args)
+
+ _hello_button = Gtk.Template.Child("hello_button")
+
+ goodbye_button = Gtk.Template.Child()
+
+ @Gtk.Template.Callback("goodbye_button_clicked")
+ def _goodbye_button_clicked(self, *args):
+ self.callback_goodbye.append(args)
+
+ @Gtk.Template.Callback("goodbye_button_clicked_after")
+ def _goodbye_after(self, *args):
+ self.callback_goodbye_after.append(args)
+
+ w = Foo()
+ assert w.__gtype__.name == type_name
+ assert w.props.orientation == Gtk.Orientation.HORIZONTAL
+ assert w.props.spacing == 4
+ assert isinstance(w._hello_button, Gtk.Button)
+ assert w._hello_button.props.label == "Hello World"
+ assert isinstance(w.goodbye_button, Gtk.Button)
+ assert w.goodbye_button.props.label == "Goodbye World"
+
+ assert w.callback_hello == []
+ w._hello_button.clicked()
+ assert w.callback_hello == [(w,)]
+ assert w.callback_hello_after == [(w,)]
+
+ assert w.callback_goodbye == []
+ w.goodbye_button.clicked()
+ assert w.callback_goodbye == [(w.goodbye_button,)]
+ assert w.callback_goodbye_after == [(w.goodbye_button,)]
+
+
+def test_duplicate_handler():
+
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <child>
+ <object class="GtkButton" id="hello_button">
+ <signal name="clicked" handler="hello_button_clicked">
+ </object>
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ @Gtk.Template.Callback("hello_button_clicked")
+ def _hello_button_clicked(self, *args):
+ pass
+
+ @Gtk.Template.Callback()
+ def hello_button_clicked(self, *args):
+ pass
+
+ with pytest.raises(RuntimeError, match=".*hello_button_clicked.*"):
+ Gtk.Template.from_string(xml)(Foo)
+
+
+def test_duplicate_child():
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <child>
+ <object class="GtkButton" id="hello_button" />
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ foo = Gtk.Template.Child("hello_button")
+ hello_button = Gtk.Template.Child()
+
+ with pytest.raises(RuntimeError, match=".*hello_button.*"):
+ Gtk.Template.from_string(xml)(Foo)
+
+
+def test_nonexist_handler():
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ </template>
+</interface>
+""".format(type_name)
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ @Gtk.Template.Callback("nonexit")
+ def foo(self, *args):
+ pass
+
+ with capture_exceptions() as exc_info:
+ Foo()
+ assert "nonexit" in str(exc_info[0].value)
+ assert exc_info[0].type is RuntimeError
+
+
+def test_missing_handler_callback():
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <child>
+ <object class="GtkButton" id="hello_button">
+ <signal name="clicked" handler="i_am_not_used_in_python" />
+ </object>
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ Gtk.Template.from_string(xml)(Foo)()
+
+
+def test_handler_swapped_not_supported():
+
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <child>
+ <object class="GtkButton" id="hello_button">
+ <signal name="clicked" handler="hello_button_clicked"
+ object="{0}" swapped="yes" />
+ </object>
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ hello_button = Gtk.Template.Child()
+
+ @Gtk.Template.Callback("hello_button_clicked")
+ def foo(self, *args):
+ pass
+
+ with capture_exceptions() as exc_info:
+ Foo()
+ assert "G_CONNECT_SWAPPED" in str(exc_info[0].value)
+
+
+def test_handler_class_staticmethod():
+
+ type_name = new_gtype_name()
+
+ xml = """\
+<interface>
+ <template class="{0}" parent="GtkBox">
+ <child>
+ <object class="GtkButton" id="hello_button">
+ <signal name="clicked" handler="clicked_class" />
+ <signal name="clicked" handler="clicked_static" />
+ </object>
+ </child>
+ </template>
+</interface>
+""".format(type_name)
+
+ signal_args_class = []
+ signal_args_static = []
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ hello_button = Gtk.Template.Child()
+
+ @Gtk.Template.Callback("clicked_class")
+ @classmethod
+ def cb1(*args):
+ signal_args_class.append(args)
+
+ @Gtk.Template.Callback("clicked_static")
+ @staticmethod
+ def cb2(*args):
+ signal_args_static.append(args)
+
+ foo = Foo()
+ foo.hello_button.clicked()
+ assert signal_args_class == [(Foo, foo.hello_button)]
+ assert signal_args_static == [(foo.hello_button,)]
+
+
+def test_check_decorated_class():
+
+ NonWidget = type("Foo", (object,), {})
+ with pytest.raises(TypeError, match=".*on Widgets.*"):
+ Gtk.Template.from_string("")(NonWidget)
+
+ Widget = type("Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
+ with pytest.raises(TypeError, match=".*Cannot nest.*"):
+ Gtk.Template.from_string("")(Gtk.Template.from_string("")(Widget))
+
+ Widget = type("Foo", (Gtk.Widget,), {})
+ with pytest.raises(TypeError, match=".*__gtype_name__.*"):
+ Gtk.Template.from_string("")(Widget)
+
+ with pytest.raises(TypeError, match=".*on Widgets.*"):
+ Gtk.Template.from_string("")(object())
+
+ @Gtk.Template.from_string("")
+ class Base(Gtk.Widget):
+ __gtype_name__ = new_gtype_name()
+
+ with capture_exceptions() as exc_info:
+ type("Sub", (Base,), {})()
+ assert "not allowed at this time" in str(exc_info[0].value)
+ assert exc_info[0].type is TypeError
+
+
+def test_from_file():
+ fd, name = tempfile.mkstemp()
+ try:
+ os.close(fd)
+
+ type_name = new_gtype_name()
+
+ with open(name, "wb") as h:
+ h.write(u"""\
+ <interface>
+ <template class="{0}" parent="GtkBox">
+ <property name="spacing">42</property>
+ </template>
+ </interface>
+ """.format(type_name).encode())
+
+ @Gtk.Template.from_file(name)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ foo = Foo()
+ assert foo.props.spacing == 42
+ finally:
+ os.remove(name)
+
+
+def test_property_override():
+ type_name = new_gtype_name()
+
+ xml = """\
+ <interface>
+ <template class="{0}" parent="GtkBox">
+ <property name="spacing">42</property>
+ </template>
+ </interface>
+""".format(type_name)
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ foo = Foo()
+ assert foo.props.spacing == 42
+
+ foo = Foo(spacing=124)
+ assert foo.props.spacing == 124
+
+
+def test_from_file_non_exist():
+ dirname = tempfile.mkdtemp()
+ try:
+ path = os.path.join(dirname, "noexist")
+
+ Widget = type(
+ "Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
+ with pytest.raises(GLib.Error, match=".*No such file.*"):
+ Gtk.Template.from_file(path)(Widget)
+ finally:
+ os.rmdir(dirname)
+
+
+def test_from_string_bytes():
+ type_name = new_gtype_name()
+
+ xml = u"""\
+ <interface>
+ <template class="{0}" parent="GtkBox">
+ <property name="spacing">42</property>
+ </template>
+ </interface>
+ """.format(type_name).encode()
+
+ @Gtk.Template.from_string(xml)
+ class Foo(Gtk.Box):
+ __gtype_name__ = type_name
+
+ foo = Foo()
+ assert foo.props.spacing == 42
+
+
+def test_from_resource():
+ resource_path = ensure_resource_registered()
+
+ @Gtk.Template.from_resource(resource_path)
+ class Foo(Gtk.Box):
+ __gtype_name__ = "GtkTemplateTestResource"
+
+ foo = Foo()
+ assert foo.props.spacing == 42
+
+
+def test_from_resource_non_exit():
+ Widget = type("Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
+ with pytest.raises(GLib.Error, match=".*/or/gnome/pygobject/noexit.*"):
+ Gtk.Template.from_resource("/or/gnome/pygobject/noexit")(Widget)
+
+
+def test_constructors():
+ with pytest.raises(TypeError):
+ Gtk.Template()
+
+ with pytest.raises(TypeError):
+ Gtk.Template(foo=1)
+
+ Gtk.Template(filename="foo")
+ Gtk.Template(resource_path="foo")
+ Gtk.Template(string="foo")
+
+ with pytest.raises(TypeError):
+ Gtk.Template(filename="foo", resource_path="bar")
+
+ with pytest.raises(TypeError):
+ Gtk.Template(filename="foo", nope="bar")
+
+ Gtk.Template.from_string("bla")
+ Gtk.Template.from_resource("foo")
+ Gtk.Template.from_file("foo")