1

The background to this question (and my overall goal) is to structure a Python GTK application in a nice way. I am trying to bind widget properties to model properties using GTK's bidirectional data bindings.

My expectation is that the bidirectional binding should keep two properties in sync. I find instead that changes propagate in one direction only, even though I am using the GObject.BindingFlags.BIDIRECTIONAL flag. I created the following minimal example and the failing test case test_widget_syncs_to_model to illustrate the problem. Note that in a more realistic example, the model object could be an instance of Gtk.Application and the widget object could be an instance of Gtk.Entry.

import gi

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
import unittest


class Obj(GObject.Object):
    """A very simple GObject with a `txt` property."""

    name = "default"
    txt = GObject.Property(type=str, default="default")

    def __init__(self, name, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.name = name
        self.connect("notify", self.log)

    def log(self, source, parameter_name):
        print(
            f"The '{self.name}' object received a notify event, "
            f"its txt now is '{self.txt}'."
        )


class TestBindings(unittest.TestCase):
    def setUp(self):
        """Sets up a bidirectional binding between a model and a widget"""
        print(f"\n\n{self.id()}")
        self.model = Obj("model")
        self.widget = Obj("widget")
        self.model.bind_property(
            "txt", self.widget, "txt", flags=GObject.BindingFlags.BIDIRECTIONAL
        )

    @unittest.skip("suceeds")
    def test_properties_are_found(self):
        """Verifies that the `txt` properties are correctly set up."""
        for obj in [self.model, self.widget]:
            self.assertIsNotNone(obj.find_property("txt"))

    @unittest.skip("suceeds")
    def test_model_syncs_to_widget(self, data="hello"):
        """Verifies that model changes propagate to the widget"""
        self.model.txt = data
        self.assertEqual(self.widget.txt, data)

    def test_widget_syncs_to_model(self, data="world"):
        """Verifies that widget changes propagate back into the model"""
        self.widget.txt = data
        self.assertEqual(self.widget.txt, data)  # SUCCEEDS
        self.assertEqual(self.model.txt, data)  # FAILS


if __name__ == "__main__":
    unittest.main()

The above program outputs:

ssF
======================================================================
FAIL: test_widget_syncs_to_model (__main__.TestBindings)
Verifies that widget changes propagate back into the model
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/jh/.config/JetBrains/PyCharmCE2021.1/scratches/scratch_14.py", line 52, in test_widget_syncs_to_model
    self.assertEqual(self.model.txt, data)  # FAILS
AssertionError: 'default' != 'world'
- default
+ world


----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1, skipped=2)


__main__.TestBindings.test_widget_syncs_to_model
The 'widget' object received a notify event, its txt now is 'world'.

Process finished with exit code 1

My specific question is, how can I get the bidirectional data bindings to work?... I would be glad if someone could fix my example or provide another working example.

In a broader sense, are bidirectional bindings the way to go for syncing UI state and model state in a well-structured Python GTK application? What is the intended and well-supported way to do this? thanks!

9
  • Is the GObject::notify signal being emitted for your txt property when it’s changed? Property bindings rely on the notify signal to work. Commented Jun 1, 2021 at 8:43
  • Thank you! I added logging to the code example. This seems to indicate that a notify signal is received with wrong text value "txt". Commented Jun 2, 2021 at 9:57
  • 1
    I think you actually want print("received notify event", self.txt) in the log() function. The param argument is actually a GParamSpec instance, which describes the property type, but not its current value. So I think the txt which is being printed is actually just the property name. Commented Jun 2, 2021 at 10:26
  • 1
    Note: I posted a link to this question in the GNOME discourse forum under "Bidirectional Bindings using PyGObject - cannot get it to work, help or working example needed" because I am pretty sure the GNOME community will have an answer. Commented Jun 6, 2021 at 15:41
  • 1
    Hi! Thank you Philip for you help and consideration! Zander Brown kindly pointed out the error in my code over in the GNOME discourse thread: The flags must be passed as positional argument when setting up the binding. Do you or Zander want to provide an answer here on stackoverflow as well? Commented Jun 7, 2021 at 7:16

1 Answer 1

1

I got an answer over at the gnome discourse thread about bidirectional property bindings in python.

To make it even more clear, the following code does not work because flags are not passed correctly:

# broken, flags are passed incorrectly as keywords argument:
self.model.bind_property("txt", self.widget, "txt", flags=GObject.BindingFlags.BIDIRECTIONAL)

Instead, flags must be passed as follows:

# functioning, flags are passed correctly as a positional argument:
self.model.bind_property("txt", self.widget, "txt", GObject.BindingFlags.BIDIRECTIONAL)

More example code: Proper use of bidirectional bindings is for example demonstrated in pygobject’s source code in the test_bidirectional_binding test case.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.