1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
2#
3# Copyright (c) 2008 - 2014 by Wilbert Berendsen
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18# See http://www.gnu.org/licenses/ for more information.
19
20"""
21A simple signal/slot implementation.
22
23Functions or methods can be connected to Signal instances, and when the
24Signal instance is called (or its emit() method is called, which is equivalent),
25all connected methods or function are automatically called.
26
27When a Signal is created as a class attribute and accessed via an instance of
28that class, it creates a Signal instance specifically for that object.
29
30When methods are connected, no reference is kept to the method's object. When
31the object is garbage collected, the signal is automatically disconnected.
32
33A special Signal variation is also available, the SignalContext. Methods or
34functions connected to this signal should return context managers which are
35entered when the signal is entered in a context (with) block.
36
37"""
38
39import bisect
40import contextlib
41import types
42import weakref
43import sys
44
45
46__all__ = ["Signal", "SignalContext"]
47
48
49class Signal(object):
50    """A Signal can be emitted and receivers (slots) can be connected to it.
51
52    An example:
53
54    class MyObject(object):
55
56        somethingChanged = Signal()
57
58        def __init__(self):
59            pass # etc
60
61        def doSomething(self):
62            ... do things ...
63            self.somethingChanged("Hi there!")     # emit the signal
64
65    def receiver(arg):
66        print("Received message:", arg)
67
68
69    >>> o = MyObject()
70    >>> o.somethingChanged.connect(receiver)
71    >>> o.doSomething()
72    Received message: Hi there!
73
74    A Signal() can be used directly or as a class attribute, but can also be
75    accessed as an attribute of an instance, in which case it creates a Signal
76    instance for that instance.
77
78    The signal is emitted by the emit() method or by simply invoking it.
79
80    It is currently not possible to enforce argument types that should be used
81    when emitting the signal. But if called methods or functions expect fewer
82    arguments than were given on emit(), the superfluous arguments are left out.
83
84    Methods or functions are connected using connect() and disconnected using
85    disconnect(). It is no problem to call connect() or disconnect() more than
86    once for the same function or method. Only one connection to the same method
87    or function can exist.
88
89    """
90
91    def __init__(self, owner=None):
92        """Creates the Signal.
93
94        If owner is given (must be a keyword argument) a weak reference to it is
95        kept, and this allows a Signal to be connected to another Signal. When
96        the owner dies, the connection is removed.
97
98        """
99        self.listeners = []
100        self._blocked = False
101        self._owner = weakref.ref(owner) if owner else lambda: None
102
103    def __get__(self, instance, cls):
104        """Called when accessing as a descriptor: returns another instance."""
105        if instance is None:
106            return self
107        try:
108            return self._instances[instance]
109        except AttributeError:
110            self._instances = weakref.WeakKeyDictionary()
111        except KeyError:
112            pass
113        ret = self._instances[instance] = type(self)(owner=instance)
114        return ret
115
116    def owner(self):
117        """Returns the owner of this Signal, if any."""
118        return self._owner()
119
120    def connect(self, slot, priority=0, owner=None):
121        """Connects a method or function ('slot') to this Signal.
122
123        The priority argument determines the order the connected slots are
124        called. A lower value calls the slot earlier.
125        If owner is given, the connection will be removed if owner is garbage
126        collected.
127
128        A slot that is already connected will not be connected twice.
129
130        If slot is an instance method (bound method), the Signal keeps no
131        reference to the object the method belongs to. So if the object is
132        garbage collected, the signal is automatically disconnected.
133
134        If slot is a (normal or lambda) function, the Signal will keep a
135        reference to the function. If you want to have the function disconnected
136        automatically when some object dies, you should provide that object
137        through the owner argument. Be sure that the connected function does not
138        keep a reference to that object in that case!
139
140        """
141        key = self.makeListener(slot, owner)
142        if key not in self.listeners:
143            key.add(self, priority)
144
145    def disconnect(self, func):
146        """Disconnects the method or function.
147
148        No exception is raised if there wasn't a connection.
149
150        """
151        key = self.makeListener(func)
152        try:
153            self.listeners.remove(key)
154        except ValueError:
155            pass
156
157    def clear(self):
158        """Removes all connected slots."""
159        del self.listeners[:]
160
161    @contextlib.contextmanager
162    def blocked(self):
163        """Returns a contextmanager that suppresses the signal.
164
165        An example (continued from the class documentation):
166
167        >>> o = MyObject()
168        >>> o.somethingChanged.connect(receiver)
169        >>> with o.somethingChanged.blocked():
170        ...     o.doSomething()
171        (no output)
172
173        The doSomething() method will emit the signal but the connected slots
174        will not be called.
175
176        """
177        blocked, self._blocked = self._blocked, True
178        try:
179            yield
180        finally:
181            self._blocked = blocked
182
183    def emit(self, *args, **kwargs):
184        """Emits the signal.
185
186        Unless blocked, all slots will be called with the supplied arguments.
187
188        """
189        if not self._blocked:
190            for l in self.listeners[:]:
191                l.call(args, kwargs)
192
193    __call__ = emit
194
195    def makeListener(self, func, owner=None):
196        """Returns a suitable listener for the given method or function."""
197        if isinstance(func, (types.MethodType, types.BuiltinMethodType)):
198            return MethodListener(func)
199        elif isinstance(func, Signal):
200            return FunctionListener(func, owner or func.owner())
201        else:
202            return FunctionListener(func, owner)
203
204
205class SignalContext(Signal):
206    """A Signal variant where the connected methods or functions should return
207    a context manager.
208
209    You should use the SignalContext itself also as a context manager, e.g.:
210
211    sig = signals.SignalContext()
212
213    with sig(args):
214        do_something()
215
216    This will first call all the connected methods or functions, and then
217    enter all the returned context managers. When the context ends,
218    all context managers will be exited.
219
220    """
221    def emit(self, *args, **kwargs):
222        if self._blocked:
223            managers = []
224        else:
225            managers = [l.call(args, kwargs) for l in self.listeners]
226        return self.signalcontextmanager(managers)
227
228    __call__ = emit
229
230    @contextlib.contextmanager
231    def signalcontextmanager(self, managers):
232        """A context manager handling all contextmanagers from the listeners."""
233        # ideas taken from Python's contextlib.nested()
234        exits = []
235        exc = (None, None, None)
236        try:
237            for m in managers:
238                m.__enter__()
239                exits.append(m.__exit__)
240            yield
241        except:
242            exc = sys.exc_info()
243        finally:
244            while exits:
245                exit = exits.pop()
246                try:
247                    if exit(*exc):
248                        exc = (None, None, None)
249                except:
250                    exc = sys.exc_info()
251            if exc != (None, None, None):
252                raise # exc[0], exc[1], exc[2]
253
254
255class ListenerBase(object):
256
257    removeargs = 0
258
259    def __init__(self, func, owner=None):
260        self.func = func
261        self.obj = owner
262
263    def __lt__(self, other):
264        return self.priority < other.priority
265
266    def add(self, signal, priority):
267        self.priority = priority
268        bisect.insort_right(signal.listeners, self)
269        if self.obj is not None:
270            def remove(wr, selfref=weakref.ref(self), sigref=weakref.ref(signal)):
271                self, signal = selfref(), sigref()
272                if self and signal:
273                    signal.listeners.remove(self)
274            self.obj = weakref.ref(self.obj, remove)
275
276        # determine the number of arguments allowed
277        end = None
278        try:
279            co = self.func.__code__
280            if not co.co_flags & 12:
281                # no *args or **kwargs are used, cut off the unwanted arguments
282                end = co.co_argcount - self.removeargs
283        except AttributeError:
284            pass
285        self.argslice = slice(0, end)
286
287
288class MethodListener(ListenerBase):
289
290    removeargs = 1
291
292    def __init__(self, meth):
293        obj = meth.__self__
294        self.objid = id(meth.__self__)
295        try:
296            func = meth.__func__
297        except AttributeError:
298            # c++ methods from PyQt5 object sometimes do not have the __func__ attribute
299            func = getattr(meth.__self__.__class__, meth.__name__)
300        super(MethodListener, self).__init__(func, obj)
301
302    def __eq__(self, other):
303        return self.__class__ is other.__class__ and self.objid == other.objid and self.func is other.func
304
305    def call(self, args, kwargs):
306        obj = self.obj()
307        if obj is not None:
308            return self.func(obj, *args[self.argslice], **kwargs)
309
310
311class FunctionListener(ListenerBase):
312
313    def __eq__(self, other):
314        return self.__class__ is other.__class__ and self.func is other.func
315
316    def call(self, args, kwargs):
317        return self.func(*args[self.argslice], **kwargs)
318
319
320