From ca5ec4693a8eebf41d5eae4de8060f50a0e189bb Mon Sep 17 00:00:00 2001
From: Simon Feltman <s.feltman@gmail.com>
Date: Wed, 14 Mar 2012 01:41:40 -0700
Subject: [PATCH] Addition of Signal class for decorating and binding custom
 signals.

The Signal class allows for easy creation of signals and removes the
need for __gsignals__ in client code. The Signal class can also be
used as a decorator for wrapping up the custom closure. As well as
providing a "BoundSignal" when accessed on an instance allowing for
connections without specifying a signal name string.

https://bugzilla.gnome.org/show_bug.cgi?id=434924
---
 gi/_gobject/Makefile.am |    3 +-
 gi/_gobject/__init__.py |   25 ++++++++
 gi/_gobject/signal.py   |  145 +++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_signal.py    |   89 +++++++++++++++++++++++++++++
 4 files changed, 261 insertions(+), 1 deletions(-)
 create mode 100644 gi/_gobject/signal.py

diff --git a/gi/_gobject/Makefile.am b/gi/_gobject/Makefile.am
index 3263889..4d37a55 100644
--- a/gi/_gobject/Makefile.am
+++ b/gi/_gobject/Makefile.am
@@ -29,7 +29,8 @@ pygobjectdir = $(pyexecdir)/gi/_gobject
 pygobject_PYTHON = \
 	__init__.py \
 	constants.py \
-	propertyhelper.py
+	propertyhelper.py \
+	signal.py
 
 pygobject_LTLIBRARIES = _gobject.la
 
diff --git a/gi/_gobject/__init__.py b/gi/_gobject/__init__.py
index 7576142..de63ffd 100644
--- a/gi/_gobject/__init__.py
+++ b/gi/_gobject/__init__.py
@@ -54,6 +54,7 @@ from ._gobject import *
 _PyGObject_API = _gobject._PyGObject_API
 
 from .propertyhelper import property
+from .signal import Signal
 
 sys.modules['gi._gobject.option'] = option
 
@@ -62,6 +63,7 @@ class GObjectMeta(type):
     def __init__(cls, name, bases, dict_):
         type.__init__(cls, name, bases, dict_)
         cls._install_properties()
+        cls._install_signals()
         cls._type_register(cls.__dict__)
 
     def _install_properties(cls):
@@ -106,6 +108,29 @@ class GObjectMeta(type):
                 prop.fset(self, value)
         cls.do_set_property = obj_set_property
 
+    def _install_signals(cls):
+        signals = getattr(cls, '__gsignals__', {})
+        newsignals = {}
+        for name, value in cls.__dict__.items():
+            if isinstance(value, Signal):
+                # Account for a potential discrepancy of the class variable named
+                # differently then the signal string.
+                if len(value) and str(value) != name:
+                    name = str(value)
+                if name in signals:
+                    raise ValueError('Signal "%s" already in __gsignals__ dict.' % name)
+                newsignals[name] = value
+                signals[name] = value.get_signal_args()
+
+        if not newsignals:
+            return
+        cls.__gsignals__ = signals
+
+        for name, signal in newsignals.items():
+            if signal.func is not None:
+                funcName = 'do_' + name.replace('-', '_')
+                setattr(cls, funcName, signal.func)
+
     def _type_register(cls, namespace):
         ## don't register the class if already registered
         if '__gtype__' in namespace:
diff --git a/gi/_gobject/signal.py b/gi/_gobject/signal.py
new file mode 100644
index 0000000..23dd52a
--- /dev/null
+++ b/gi/_gobject/signal.py
@@ -0,0 +1,145 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# pygobject - Python bindings for the GObject library
+# Copyright (C) 2012 Simon Feltman
+#
+#   gobject/signal.py: GObject signal binding decorator object
+#
+# 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 Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+from . import _gobject
+
+class Signal(str):
+    """
+    Object which gives a nice API for creating and binding signals.
+    
+    Examples:
+    class C(GObject.GObject):
+        value = 0
+        @GObject.Signal
+        def pushed(self):
+            self.value += 1
+        @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST)
+        def pulled(self):
+            self.value -= 1
+        stomped = GObject.Signal('stomped', arg_types=(int,))
+
+    def onPulled(obj):
+        print obj
+    obj = C()
+    obj.pulled.connect(onPulled)
+    obj.pulled.emit()
+    """
+    class BoundSignal(str):
+        """
+        Temporary binding object which can be used for connecting signals
+        without specifying the signal name string to connect.
+        """
+        def __new__(cls, name, *args, **kargs):
+            return str.__new__(cls, name)
+
+        def __init__(self, signal, instance):
+            str.__init__(self, signal)
+            self.signal = signal
+            self.instance = instance
+
+        def __repr__(self):
+            return 'BoundSignal("%s")' % self
+
+        def connect(self, callback, *args, **kargs):
+            """Same as GObject.GObject.connect except there is no need to specify
+            the signal name."""
+            return self.instance.connect(self, callback, *args, **kargs)
+
+        def connect_detailed(self, callback, detail, *args, **kargs):
+            """Same as GObject.GObject.connect except there is no need to specify
+            the signal name. In addition concats "::<detail>" to the signal name
+            when connecting; for use with notifications like "notify" when a property
+            changes.
+            """
+            return self.instance.connect(self+'::'+detail, callback, *args, **kargs)
+
+        def disconnect(self, handler_id):
+            """Same as GObject.GObject.disconnect."""
+            self.instance.disconnect(handler_id)
+
+        def emit(self, *args, **kargs):
+            """Same as GObject.GObject.emit except there is no need to specify
+            the signal name."""
+            self.instance.emit(self, *args, **kargs)
+
+    def __new__(cls, name='', *args, **kargs):
+        if callable(name):
+            name = name.__name__
+        return str.__new__(cls, name)
+
+    def __init__(self, name='', func=None, flags=_gobject.SIGNAL_RUN_FIRST,
+                 return_type=None, arg_types=tuple(), override=False, doc=''):
+        """
+        @param  name: name of signal or closure method when used as direct decorator.
+        @type   name: string or callable
+        @param  func: closure method.
+        @type   func: callable
+        @param  flags: flags specifying when to run closure
+        @type   flags: GObject.SignalFlags
+        @param  return_type: return type
+        @type   return_type: type
+        @param  arg_types: list of argument types specifying the signals function signature
+        @type   arg_types: tuple
+        @param  override: specify the signal as a closure override
+        @type   override: boolean
+        @param  doc: documentation of signal object
+        @type   doc: string
+        """
+        if func and not name:
+            name = func.__name__
+        elif callable(name):
+            func = name
+            name = func.__name__
+        if func and not doc:
+            doc = func.__doc__
+
+        str.__init__(self, name)
+
+        self.func = func
+        self.flags = flags
+        self.return_type = return_type
+        self.arg_types = arg_types
+        self.override = override
+        self.__doc__ = doc
+
+    def __get__(self, instance, owner=None):
+        """Returns a BoundSignal when accessed on an object instance."""
+        if instance is None:
+            return self
+        return self.BoundSignal(self, instance)
+
+    def __call__(self, func):
+        """Allows for instantiated Signals to be used as a decorator."""
+        if self:
+            name = str(self)
+        else:
+            name = func.__name__
+        # Return a new value of this type since it is based on an immutable string.
+        return type(self)(name, func, self.flags,
+                          self.return_type, self.arg_types, self.__doc__)
+
+    def get_signal_args(self):
+        """Returns the string 'override' if override is set otherwise a tuple
+        of: (flags, returnType, argTypes)"""
+        if self.override:
+            return 'override'
+        else:
+            return (self.flags, self.return_type, self.arg_types)
diff --git a/tests/test_signal.py b/tests/test_signal.py
index 8486adb..3000cc5 100644
--- a/tests/test_signal.py
+++ b/tests/test_signal.py
@@ -391,5 +391,94 @@ class TestPyGValue(unittest.TestCase):
         obj.emit('my-boxed-signal')
         assert not sys.last_type
 
+class TestSignalDecorator(unittest.TestCase):
+    class Decorated(GObject.GObject):
+        value = 0
+        @GObject.Signal
+        def pushed(self):
+            self.value += 1
+        @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST)
+        def pulled(self):
+            self.value -= 1
+        stomped = GObject.Signal('stomped', arg_types=(int,))
+
+    def testArgs(self):
+        self.assertEqual(self.Decorated.pushed.get_signal_args(),
+                         (GObject.SignalFlags.RUN_FIRST, None, tuple() ))
+        self.assertEqual(self.Decorated.pulled.get_signal_args(),
+                         (GObject.SignalFlags.RUN_LAST, None, tuple() ))
+        self.assertEqual(self.Decorated.stomped.get_signal_args(),
+                         (GObject.SignalFlags.RUN_FIRST, None, (int,)))
+
+    def testClosuresCalled(self):
+        decorated = self.Decorated()
+        self.assertEqual(decorated.value, 0)
+        decorated.pushed.emit()
+        self.assertEqual(decorated.value, 1)
+        decorated.pulled.emit()
+        self.assertEqual(decorated.value, 0)
+
+
+class TestSignalBinders(unittest.TestCase):
+    class CustomButton(GObject.GObject):
+        value = 0
+        @GObject.Signal(arg_types=(int,))
+        def clicked(self, value):
+            self.value = value
+
+    def setUp(self):
+        self.obj = None
+        self.value = None
+
+    def onClicked(self, obj, value):
+        self.obj = obj
+        self.value = value
+
+    def testSignalEmit(self):
+        # standard callback connection with different forms of emit.
+        obj = self.CustomButton()
+        obj.connect('clicked', self.onClicked)
+
+        # vanilla
+        obj.emit('clicked', 1)
+        self.assertEqual(obj.value, 1)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 1)
+
+        # using class signal as param
+        self.obj = None
+        self.value = None
+        obj.emit(self.CustomButton.clicked, 1)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 1)
+
+        # using bound signal as param
+        self.obj = None
+        self.value = None
+        obj.emit(obj.clicked, 1)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 1)
+
+        # using bound signal with emit
+        self.obj = None
+        self.value = None
+        obj.clicked.emit(1)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 1)
+
+    def testSignalClassConnect(self):
+        obj = self.CustomButton()
+        id = obj.connect(self.CustomButton.clicked, self.onClicked)
+        obj.emit('clicked', 2)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 2)
+
+    def testSignalBoundConnect(self):
+        obj = self.CustomButton()
+        id = obj.clicked.connect(self.onClicked)
+        obj.emit('clicked', 3)
+        self.assertEqual(obj, self.obj)
+        self.assertEqual(self.value, 3)
+
 if __name__ == '__main__':
     unittest.main()
-- 
1.7.5.4

