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