[pygobject] Add a minimal implementation of Gtk.Template. See #52



commit c745866df9e4394c6615e79779fff1221a84953a
Author: Christoph Reiter <reiter christoph gmail com>
Date:   Tue Apr 3 21:07:56 2018 +0200

    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.

 gi/_gtktemplate.py         | 218 ++++++++++++++++++++
 gi/gimodule.c              |  19 +-
 gi/overrides/Gtk.py        |   5 +
 tests/test_gtk_template.py | 482 +++++++++++++++++++++++++++++++++++++++++++++
 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")


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]