functions: revert the function init order to make pylint happy again. See #217
[pygobject.git] / gi / _signalhelper.py
blob8a1f0a48a33fcbc9d753efeb3a1591474c0122d1
1 # -*- Mode: Python; py-indent-offset: 4 -*-
2 # pygobject - Python bindings for the GObject library
3 # Copyright (C) 2012 Simon Feltman
5 # gi/_signalhelper.py: GObject signal binding decorator object
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # Lesser General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, see <http://www.gnu.org/licenses/>.
20 from . import _gi
23 class Signal(str):
24 """Object which gives a nice API for creating and binding signals.
26 :param name:
27 Name of signal or callable closure when used as a decorator.
28 :type name: str or callable
29 :param callable func:
30 Callable closure method.
31 :param GObject.SignalFlags flags:
32 Flags specifying when to run closure.
33 :param type return_type:
34 Return type of the Signal.
35 :param list arg_types:
36 List of argument types specifying the signals function signature
37 :param str doc:
38 Documentation of signal object.
39 :param callable accumulator:
40 Accumulator method with the signature:
41 func(ihint, return_accu, handler_return, accu_data) -> boolean
42 :param object accu_data:
43 User data passed to the accumulator.
45 :Example:
47 .. code-block:: python
49 class Spam(GObject.Object):
50 velocity = 0
52 @GObject.Signal
53 def pushed(self):
54 self.velocity += 1
56 @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST)
57 def pulled(self):
58 self.velocity -= 1
60 stomped = GObject.Signal('stomped', arg_types=(int,))
62 @GObject.Signal
63 def annotated_signal(self, a:int, b:str):
64 "Python3 annotation support for parameter types.
66 def on_pushed(obj):
67 print(obj)
69 spam = Spam()
70 spam.pushed.connect(on_pushed)
71 spam.pushed.emit()
72 """
73 class BoundSignal(str):
74 """
75 Temporary binding object which can be used for connecting signals
76 without specifying the signal name string to connect.
77 """
78 def __new__(cls, name, *args, **kargs):
79 return str.__new__(cls, name)
81 def __init__(self, signal, gobj):
82 str.__init__(self)
83 self.signal = signal
84 self.gobj = gobj
86 def __repr__(self):
87 return 'BoundSignal("%s")' % self
89 def __call__(self, *args, **kargs):
90 """Call the signals closure."""
91 return self.signal.func(self.gobj, *args, **kargs)
93 def connect(self, callback, *args, **kargs):
94 """Same as GObject.Object.connect except there is no need to specify
95 the signal name."""
96 return self.gobj.connect(self, callback, *args, **kargs)
98 def connect_detailed(self, callback, detail, *args, **kargs):
99 """Same as GObject.Object.connect except there is no need to specify
100 the signal name. In addition concats "::<detail>" to the signal name
101 when connecting; for use with notifications like "notify" when a property
102 changes.
104 return self.gobj.connect(self + '::' + detail, callback, *args, **kargs)
106 def disconnect(self, handler_id):
107 """Same as GObject.Object.disconnect."""
108 self.gobj.disconnect(handler_id)
110 def emit(self, *args, **kargs):
111 """Same as GObject.Object.emit except there is no need to specify
112 the signal name."""
113 return self.gobj.emit(str(self), *args, **kargs)
115 def __new__(cls, name='', *args, **kargs):
116 if callable(name):
117 name = name.__name__
118 return str.__new__(cls, name)
120 def __init__(self, name='', func=None, flags=_gi.SIGNAL_RUN_FIRST,
121 return_type=None, arg_types=None, doc='', accumulator=None, accu_data=None):
122 if func is None and callable(name):
123 func = name
125 if func and not doc:
126 doc = func.__doc__
128 str.__init__(self)
130 if func and not (return_type or arg_types):
131 return_type, arg_types = get_signal_annotations(func)
132 if arg_types is None:
133 arg_types = tuple()
135 self.func = func
136 self.flags = flags
137 self.return_type = return_type
138 self.arg_types = arg_types
139 self.__doc__ = doc
140 self.accumulator = accumulator
141 self.accu_data = accu_data
143 def __get__(self, instance, owner=None):
144 """Returns a BoundSignal when accessed on an object instance."""
145 if instance is None:
146 return self
147 return self.BoundSignal(self, instance)
149 def __call__(self, obj, *args, **kargs):
150 """Allows for instantiated Signals to be used as a decorator or calling
151 of the underlying signal method."""
153 # If obj is a GObject, than we call this signal as a closure otherwise
154 # it is used as a re-application of a decorator.
155 if isinstance(obj, _gi.GObject):
156 self.func(obj, *args, **kargs)
157 else:
158 # If self is already an allocated name, use it otherwise create a new named
159 # signal using the closure name as the name.
160 if str(self):
161 name = str(self)
162 else:
163 name = obj.__name__
164 # Return a new value of this type since it is based on an immutable string.
165 return type(self)(name=name, func=obj, flags=self.flags,
166 return_type=self.return_type, arg_types=self.arg_types,
167 doc=self.__doc__, accumulator=self.accumulator, accu_data=self.accu_data)
169 def copy(self, newName=None):
170 """Returns a renamed copy of the Signal."""
172 return type(self)(name=newName, func=self.func, flags=self.flags,
173 return_type=self.return_type, arg_types=self.arg_types,
174 doc=self.__doc__, accumulator=self.accumulator, accu_data=self.accu_data)
176 def get_signal_args(self):
177 """Returns a tuple of: (flags, return_type, arg_types, accumulator, accu_data)"""
178 return (self.flags, self.return_type, self.arg_types, self.accumulator, self.accu_data)
181 class SignalOverride(Signal):
182 """Specialized sub-class of Signal which can be used as a decorator for overriding
183 existing signals on GObjects.
185 :Example:
187 .. code-block:: python
189 class MyWidget(Gtk.Widget):
190 @GObject.SignalOverride
191 def configure_event(self):
192 pass
194 def get_signal_args(self):
195 """Returns the string 'override'."""
196 return 'override'
199 def get_signal_annotations(func):
200 """Attempt pulling python 3 function annotations off of 'func' for
201 use as a signals type information. Returns an ordered nested tuple
202 of (return_type, (arg_type1, arg_type2, ...)). If the given function
203 does not have annotations then (None, tuple()) is returned.
205 arg_types = tuple()
206 return_type = None
208 if hasattr(func, '__annotations__'):
209 # import inspect only when needed because it takes ~10 msec to load
210 import inspect
211 spec = inspect.getfullargspec(func)
212 arg_types = tuple(spec.annotations[arg] for arg in spec.args
213 if arg in spec.annotations)
214 if 'return' in spec.annotations:
215 return_type = spec.annotations['return']
217 return return_type, arg_types
220 def install_signals(cls):
221 """Adds Signal instances on a GObject derived class into the '__gsignals__'
222 dictionary to be picked up and registered as real GObject signals.
224 gsignals = cls.__dict__.get('__gsignals__', {})
225 newsignals = {}
226 for name, signal in cls.__dict__.items():
227 if isinstance(signal, Signal):
228 signalName = str(signal)
229 # Fixup a signal which is unnamed by using the class variable name.
230 # Since Signal is based on string which immutable,
231 # we must copy and replace the class variable.
232 if not signalName:
233 signalName = name
234 signal = signal.copy(name)
235 setattr(cls, name, signal)
236 if signalName in gsignals:
237 raise ValueError('Signal "%s" has already been registered.' % name)
238 newsignals[signalName] = signal
239 gsignals[signalName] = signal.get_signal_args()
241 cls.__gsignals__ = gsignals
243 # Setup signal closures by adding the specially named
244 # method to the class in the form of "do_<signal_name>".
245 for name, signal in newsignals.items():
246 if signal.func is not None:
247 funcName = 'do_' + name.replace('-', '_')
248 if not hasattr(cls, funcName):
249 setattr(cls, funcName, signal.func)