Commit 66104283 authored by Simon Feltman's avatar Simon Feltman Committed by Martin Pitt
Browse files

[API add] Add GObject.bind_property method

This adds the "bind_property" method for binding two gobject properties
together. The method returns a weak reference to a GBinding object.
The BindingWeakRef object is used to manage GBinding objects within python
created through GObject.bind_property. It is a sub-class PyGObjectWeakRef so
that we can maintain the same reference counting semantics between Python
and GObject Binding objects. This gives explicit direct control of the
binding lifetime by using the "unbind" method on the BindingWeakRef object
along with implicit management based on the lifetime of the source or
target objects.

Note this does not yet include support for converter closures. This can come
later after the initial implementation is accepted.

https://bugzilla.gnome.org/show_bug.cgi?id=675582

Signed-off-by: default avatarMartin Pitt <martinpitt@gnome.org>
parent 88babe73
......@@ -38,8 +38,8 @@ static int pygobject_clear(PyGObject *self);
static PyObject * pyg_type_get_bases(GType gtype);
static inline int pygobject_clear(PyGObject *self);
static PyObject * pygobject_weak_ref_new(GObject *obj, PyObject *callback, PyObject *user_data);
static PyObject * pygbinding_weak_ref_new(GObject *obj);
static inline PyGObjectData * pyg_object_peek_inst_data(GObject *obj);
static PyObject * pygobject_weak_ref_new(GObject *obj, PyObject *callback, PyObject *user_data);
static void pygobject_inherit_slots(PyTypeObject *type, PyObject *bases,
gboolean check_for_present);
static void pygobject_find_slot_for(PyTypeObject *type, PyObject *bases, int slot_offset,
......@@ -1485,6 +1485,50 @@ pygobject_set_properties(PyGObject *self, PyObject *args, PyObject *kwargs)
return result;
}
static PyObject *
pygobject_bind_property(PyGObject *self, PyObject *args)
{
gchar *source_name, *target_name;
gchar *source_canon, *target_canon;
PyObject *target, *source_repr, *target_repr, *pybinding;
GBinding *binding;
GBindingFlags flags = G_BINDING_DEFAULT;
if (!PyArg_ParseTuple(args, "sOs|i:GObject.bind_property",
&source_name, &target, &target_name, &flags))
return NULL;
CHECK_GOBJECT(self);
if (!PyObject_TypeCheck(target, &PyGObject_Type)) {
PyErr_SetString(PyExc_TypeError, "Second argument must be a GObject");
return NULL;
}
/* Canonicalize underscores to hyphens. Note the results must be freed. */
source_canon = g_strdelimit(g_strdup(source_name), "_", '-');
target_canon = g_strdelimit(g_strdup(target_name), "_", '-');
binding = g_object_bind_property(G_OBJECT(self->obj), source_canon,
pygobject_get(target), target_canon, flags);
g_free(source_canon);
g_free(target_canon);
source_canon = target_canon = NULL;
if (binding == NULL) {
source_repr = PyObject_Repr((PyObject*)self);
target_repr = PyObject_Repr(target);
PyErr_Format(PyExc_TypeError, "Cannot create binding from %s.%s to %s.%s",
PYGLIB_PyUnicode_AsString(source_repr), source_name,
PYGLIB_PyUnicode_AsString(target_repr), target_name);
Py_DECREF(source_repr);
Py_DECREF(target_repr);
return NULL;
}
return pygbinding_weak_ref_new(binding);
}
static PyObject *
pygobject_freeze_notify(PyGObject *self, PyObject *args)
{
......@@ -2118,11 +2162,13 @@ pygobject_handler_unblock_by_func(PyGObject *self, PyObject *args)
return PYGLIB_PyLong_FromLong(retval);
}
static PyMethodDef pygobject_methods[] = {
{ "get_property", (PyCFunction)pygobject_get_property, METH_VARARGS },
{ "get_properties", (PyCFunction)pygobject_get_properties, METH_VARARGS },
{ "set_property", (PyCFunction)pygobject_set_property, METH_VARARGS },
{ "set_properties", (PyCFunction)pygobject_set_properties, METH_VARARGS|METH_KEYWORDS },
{ "bind_property", (PyCFunction)pygobject_bind_property, METH_VARARGS|METH_KEYWORDS },
{ "freeze_notify", (PyCFunction)pygobject_freeze_notify, METH_VARARGS },
{ "notify", (PyCFunction)pygobject_notify, METH_VARARGS },
{ "thaw_notify", (PyCFunction)pygobject_thaw_notify, METH_VARARGS },
......@@ -2325,6 +2371,54 @@ pygobject_weak_ref_call(PyGObjectWeakRef *self, PyObject *args, PyObject *kw)
}
}
/* -------------- GBinding Weak Reference ----------------- */
/**
* BindingWeakRef
*
* The BindingWeakRef object is used to manage GBinding objects within python
* created through GObject.bind_property. It is a sub-class PyGObjectWeakRef so
* that we can maintain the same reference counting semantics between Python
* and GObject Binding objects. This gives explicit direct control of the
* binding lifetime by using the "unbind" method on the BindingWeakRef object
* along with implicit management based on the lifetime of the source or
* target objects.
*/
PYGLIB_DEFINE_TYPE("gi._gobject.GBindingWeakRef", PyGBindingWeakRef_Type, PyGObjectWeakRef);
static PyObject *
pygbinding_weak_ref_new(GObject *obj)
{
PyGObjectWeakRef *self;
self = PyObject_GC_New(PyGObjectWeakRef, &PyGBindingWeakRef_Type);
self->callback = NULL;
self->user_data = NULL;
self->obj = obj;
g_object_weak_ref(self->obj, (GWeakNotify) pygobject_weak_ref_notify, self);
return (PyObject *) self;
}
static PyObject *
pygbinding_weak_ref_unbind(PyGObjectWeakRef *self, PyObject *args)
{
if (!self->obj) {
PyErr_SetString(PyExc_ValueError, "weak binding ref already unreffed");
return NULL;
}
g_object_unref(self->obj);
Py_INCREF(Py_None);
return Py_None;
}
static PyMethodDef pygbinding_weak_ref_methods[] = {
{ "unbind", (PyCFunction)pygbinding_weak_ref_unbind, METH_NOARGS},
{ NULL, NULL, 0}
};
static gpointer
pyobject_copy(gpointer boxed)
{
......@@ -2427,6 +2521,14 @@ pygobject_object_register_types(PyObject *d)
return;
PyDict_SetItemString(d, "GObjectWeakRef", (PyObject *) &PyGObjectWeakRef_Type);
PyGBindingWeakRef_Type.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_GC;
PyGBindingWeakRef_Type.tp_doc = "A GBinding weak reference";
PyGBindingWeakRef_Type.tp_methods = pygbinding_weak_ref_methods;
PyGBindingWeakRef_Type.tp_base = &PyGObjectWeakRef_Type;
if (PyType_Ready(&PyGBindingWeakRef_Type) < 0)
return;
PyDict_SetItemString(d, "GBindingWeakRef", (PyObject *) &PyGBindingWeakRef_Type);
PyGContextFreezeNotify_Type.tp_dealloc = (destructor)pygcontext_freeze_notify_dealloc;
PyGContextFreezeNotify_Type.tp_flags = Py_TPFLAGS_DEFAULT;
PyGContextFreezeNotify_Type.tp_doc = "Context manager for freeze/thaw of GObjects";
......
# -*- Mode: Python -*-
import gc
import unittest
from gi.repository import GObject
......@@ -346,5 +347,94 @@ class TestContextManagers(unittest.TestCase):
self.obj.props.prop = 2
self.assertEqual(self.tracking, [2])
class TestPropertyBindings(unittest.TestCase):
class TestObject(GObject.GObject):
int_prop = GObject.Property(default=0, type=int)
def setUp(self):
self.source = self.TestObject()
self.target = self.TestObject()
def testDefaultBinding(self):
binding = self.source.bind_property('int_prop', self.target, 'int_prop',
GObject.BindingFlags.DEFAULT)
binding = binding # PyFlakes
# Test setting value on source gets pushed to target
self.source.int_prop = 1
self.assertEqual(self.source.int_prop, 1)
self.assertEqual(self.target.int_prop, 1)
# Test setting value on target does not change source
self.target.props.int_prop = 2
self.assertEqual(self.source.int_prop, 1)
self.assertEqual(self.target.int_prop, 2)
def testBiDirectionalBinding(self):
binding = self.source.bind_property('int_prop', self.target, 'int_prop',
GObject.BindingFlags.BIDIRECTIONAL)
binding = binding # PyFlakes
# Test setting value on source gets pushed to target
self.source.int_prop = 1
self.assertEqual(self.source.int_prop, 1)
self.assertEqual(self.target.int_prop, 1)
# Test setting value on target does not change source
self.target.props.int_prop = 2
self.assertEqual(self.source.int_prop, 2)
self.assertEqual(self.target.int_prop, 2)
def testExplicitUnbindClearsConnection(self):
self.assertEqual(self.source.int_prop, 0)
self.assertEqual(self.target.int_prop, 0)
# Test deleting binding reference removes binding.
binding = self.source.bind_property('int_prop', self.target, 'int_prop')
self.source.int_prop = 1
self.assertEqual(self.source.int_prop, 1)
self.assertEqual(self.target.int_prop, 1)
binding.unbind()
self.assertEqual(binding(), None)
self.source.int_prop = 10
self.assertEqual(self.source.int_prop, 10)
self.assertEqual(self.target.int_prop, 1)
# An already unbound BindingWeakRef will raise if unbind is attempted a second time.
self.assertRaises(ValueError, binding.unbind)
def testReferenceCounts(self):
self.assertEqual(self.source.__grefcount__, 1)
self.assertEqual(self.target.__grefcount__, 1)
# Binding ref count will be 2 do to the initial ref implicitly held by
# the act of binding and the ref incurred by using __call__ to generate
# a wrapper from the weak binding ref within python.
binding = self.source.bind_property('int_prop', self.target, 'int_prop')
self.assertEqual(binding().__grefcount__, 2)
# Creating a binding does not inc refs on source and target (they are weak
# on the binding object itself)
self.assertEqual(self.source.__grefcount__, 1)
self.assertEqual(self.target.__grefcount__, 1)
# Use GObject.get_property because the "props" accessor leaks.
# Note property names are canonicalized.
self.assertEqual(binding().get_property('source'), self.source)
self.assertEqual(binding().get_property('source_property'), 'int-prop')
self.assertEqual(binding().get_property('target'), self.target)
self.assertEqual(binding().get_property('target_property'), 'int-prop')
self.assertEqual(binding().get_property('flags'), GObject.BindingFlags.DEFAULT)
# Delete reference to source or target and the binding should listen.
ref = self.source.weak_ref()
del self.source
gc.collect()
self.assertEqual(ref(), None)
self.assertEqual(binding(), None)
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment