-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathPySignal.py
311 lines (253 loc) · 9.96 KB
/
PySignal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
__author__ = "Dhruv Govil"
__copyright__ = "Copyright 2016, Dhruv Govil"
__credits__ = ["Dhruv Govil", "John Hood", "Jason Viloria", "Adric Worley", "Alex Widener"]
__license__ = "MIT"
__version__ = "1.1.4"
__maintainer__ = "Dhruv Govil"
__email__ = "[email protected]"
__status__ = "Beta"
import inspect
import sys
import weakref
from functools import partial
# weakref.WeakMethod backport
try:
from weakref import WeakMethod
except ImportError:
import types
class WeakMethod(object):
"""Light WeakMethod backport compiled from various sources. Tested in 2.7"""
def __init__(self, func):
if inspect.ismethod(func):
self._obj = weakref.ref(func.__self__)
self._func = weakref.ref(func.__func__)
else:
self._obj = None
try:
self._func = weakref.ref(func.__func__)
# Rather than attempting to handle this, raise the same exception
# you get from WeakMethod.
except AttributeError:
raise TypeError("argument should be a bound method, not %s" % type(func))
def __call__(self):
if self._obj is not None:
obj = self._obj()
func = self._func()
if func is None or obj is None:
return None
else:
return types.MethodType(func, obj, obj.__class__)
elif self._func is not None:
return self._func()
else:
return None
def __eq__(self, other):
try:
return type(self) is type(other) and self() == other()
except Exception:
return False
def __ne__(self, other):
return not self.__eq__(other)
class Signal(object):
"""
The Signal is the core object that handles connection and emission .
"""
def __init__(self):
super(Signal, self).__init__()
self._block = False
self._sender = None
self._slots = []
def __call__(self, *args, **kwargs):
self.emit(*args, **kwargs)
def emit(self, *args, **kwargs):
"""
Calls all the connected slots with the provided args and kwargs unless block is activated
"""
if self._block:
return
def _get_sender():
"""Try to get the bound, class or module method calling the emit."""
prev_frame = sys._getframe(2)
func_name = prev_frame.f_code.co_name
# Faster to try/catch than checking for 'self'
try:
return getattr(prev_frame.f_locals['self'], func_name)
except KeyError:
return getattr(inspect.getmodule(prev_frame), func_name)
# Get the sender
try:
self._sender = WeakMethod(_get_sender())
# Account for when func_name is at '<module>'
except AttributeError:
self._sender = None
# Handle unsupported module level methods for WeakMethod.
# TODO: Support module level methods.
except TypeError:
self._sender = None
for slot in self._slots:
if not slot:
continue
elif isinstance(slot, partial):
slot(*args, **kwargs)
elif isinstance(slot, weakref.WeakKeyDictionary):
# For class methods, get the class object and call the method accordingly.
for obj, method in slot.items():
method(obj, *args, **kwargs)
elif isinstance(slot, weakref.ref):
# If it's a weakref, call the ref to get the instance and then call the func
# Don't wrap in try/except so we don't risk masking exceptions from the actual func call
tested_slot = slot()
if tested_slot is not None:
tested_slot(*args, **kwargs)
else:
# Else call it in a standard way. Should be just lambdas at this point
slot(*args, **kwargs)
def connect(self, slot):
"""
Connects the signal to any callable object
"""
if not callable(slot):
raise ValueError("Connection to non-callable '%s' object failed" % slot.__class__.__name__)
if isinstance(slot, (partial, Signal)) or '<' in slot.__name__:
# If it's a partial, a Signal or a lambda. The '<' check is the only py2 and py3 compatible way I could find
if slot not in self._slots:
self._slots.append(slot)
elif inspect.ismethod(slot):
# Check if it's an instance method and store it with the instance as the key
slotSelf = slot.__self__
slotDict = weakref.WeakKeyDictionary()
slotDict[slotSelf] = slot.__func__
if slotDict not in self._slots:
self._slots.append(slotDict)
else:
# If it's just a function then just store it as a weakref.
newSlotRef = weakref.ref(slot)
if newSlotRef not in self._slots:
self._slots.append(newSlotRef)
def disconnect(self, slot):
"""
Disconnects the slot from the signal
"""
if not callable(slot):
return
if inspect.ismethod(slot):
# If it's a method, then find it by its instance
slotSelf = slot.__self__
for s in self._slots:
if (isinstance(s, weakref.WeakKeyDictionary) and
(slotSelf in s) and
(s[slotSelf] is slot.__func__)):
self._slots.remove(s)
break
elif isinstance(slot, (partial, Signal)) or '<' in slot.__name__:
# If it's a partial, a Signal or lambda, try to remove directly
try:
self._slots.remove(slot)
except ValueError:
pass
else:
# It's probably a function, so try to remove by weakref
try:
self._slots.remove(weakref.ref(slot))
except ValueError:
pass
def clear(self):
"""Clears the signal of all connected slots"""
self._slots = []
def block(self, isBlocked):
"""Sets blocking of the signal"""
self._block = bool(isBlocked)
def sender(self):
"""Return the callable responsible for emitting the signal, if found."""
try:
return self._sender()
except TypeError:
return None
class ClassSignal(object):
"""
The class signal allows a signal to be set on a class rather than an instance.
This emulates the behavior of a PyQt signal
"""
_map = {}
def __get__(self, instance, owner):
if instance is None:
# When we access ClassSignal element on the class object without any instance,
# we return the ClassSignal itself
return self
tmp = self._map.setdefault(self, weakref.WeakKeyDictionary())
return tmp.setdefault(instance, Signal())
def __set__(self, instance, value):
raise RuntimeError("Cannot assign to a Signal object")
class SignalFactory(dict):
"""
The Signal Factory object lets you handle signals by a string based name instead of by objects.
"""
def register(self, name, *slots):
"""
Registers a given signal
:param name: the signal to register
"""
# setdefault initializes the object even if it exists. This is more efficient
if name not in self:
self[name] = Signal()
for slot in slots:
self[name].connect(slot)
def deregister(self, name):
"""
Removes a given signal
:param name: the signal to deregister
"""
self.pop(name, None)
def emit(self, signalName, *args, **kwargs):
"""
Emits a signal by name if it exists. Any additional args or kwargs are passed to the signal
:param signalName: the signal name to emit
"""
assert signalName in self, "%s is not a registered signal" % signalName
self[signalName].emit(*args, **kwargs)
def connect(self, signalName, slot):
"""
Connects a given signal to a given slot
:param signalName: the signal name to connect to
:param slot: the callable slot to register
"""
assert signalName in self, "%s is not a registered signal" % signalName
self[signalName].connect(slot)
def block(self, signals=None, isBlocked=True):
"""
Sets the block on any provided signals, or to all signals
:param signals: defaults to all signals. Accepts either a single string or a list of strings
:param isBlocked: the state to set the signal to
"""
if signals:
try:
if isinstance(signals, basestring):
signals = [signals]
except NameError:
if isinstance(signals, str):
signals = [signals]
signals = signals or self.keys()
for signal in signals:
if signal not in self:
raise RuntimeError("Could not find signal matching %s" % signal)
self[signal].block(isBlocked)
class ClassSignalFactory(object):
"""
The class signal allows a signal factory to be set on a class rather than an instance.
"""
_map = {}
_names = set()
def __get__(self, instance, owner):
tmp = self._map.setdefault(self, weakref.WeakKeyDictionary())
signal = tmp.setdefault(instance, SignalFactory())
for name in self._names:
signal.register(name)
return signal
def __set__(self, instance, value):
raise RuntimeError("Cannot assign to a Signal object")
def register(self, name):
"""
Registers a new signal with the given name
:param name: The signal to register
"""
self._names.add(name)